chore: release api v0.3.4
Build and Push API / release (push) Successful in 4s
Build and Push API / build (push) Successful in 1m18s

This commit is contained in:
2026-01-11 20:21:50 +01:00
parent b5b2346db5
commit 470b7448ac
8 changed files with 909 additions and 1 deletions
+174
View File
@@ -0,0 +1,174 @@
"""
Task Agent implementation using PydanticAI.
Full orchestrator agent that can:
- Execute multi-step tasks autonomously
- Use all tools (read + write)
- Spawn sub-agents (Explore, Plan) for focused work
"""
import os
from collections.abc import AsyncIterator
from dataclasses import dataclass
from typing import Any
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from src.domains.agents.base import BaseAgent, AgentContext, register_agent
from src.domains.agents.task.prompts import TASK_SYSTEM_PROMPT
from src.ollama.provider import get_ollama_provider
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger, trace_span
logger = get_logger(__name__)
@dataclass
class TaskContext(AgentContext):
"""
Context for task agent tools.
Passed to all tool functions via RunContext.
Uses the same fields as base AgentContext.
"""
pass
class TaskAgentImpl(BaseAgent):
"""
Full orchestrator agent for autonomous task execution.
Has access to ALL tools:
- Read-only: read_file, glob_files, grep_content, bash_readonly
- Write: edit_file, write_file, bash
- External: web_search
- Orchestration: spawn_agent (launch sub-agents)
Can spawn Explore and Plan agents to offload focused tasks,
keeping context efficient across complex multi-step work.
"""
name = "task"
description = "Autonomous multi-step task execution with sub-agent orchestration"
def __init__(self):
"""Initialize the task agent."""
self._agent: Agent[TaskContext, str] | None = None
self._settings = get_settings()
def _create_agent(self) -> Agent[TaskContext, str]:
"""Create the PydanticAI agent with Ollama backend."""
# Use sanitized Ollama provider to fix content: null issues
model = OpenAIModel(
model_name=self._settings.ollama_agent_model,
provider=get_ollama_provider(),
)
agent: Agent[TaskContext, str] = Agent(
model=model,
system_prompt=TASK_SYSTEM_PROMPT,
deps_type=TaskContext,
output_type=str,
# Mistral Nemo settings:
# - temperature 0.3 (Nemo needs slightly higher than 0.0)
# - tool_choice "required" forces tool use
model_settings={
"temperature": 0.3,
"extra_body": {"tool_choice": "required"},
},
)
# Register all tools including orchestration
self._register_tools(agent)
return agent
def _register_tools(self, agent: Agent[TaskContext, str]) -> None:
"""Register all tools with the agent."""
from src.domains.agents.task.tools import register_task_tools
register_task_tools(agent)
@logged()
async def run(
self,
prompt: str,
working_dir: str | None = None,
allowed_paths: list[str] | None = None,
**kwargs: Any
) -> str:
"""
Run the task agent to execute a multi-step task.
Args:
prompt: Description of the task to execute
working_dir: Working directory for the agent
allowed_paths: Restrict tool access to these paths
Returns:
Consolidated task summary with results
"""
ctx = TaskContext(
working_dir=working_dir or os.getcwd(),
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
timeout_seconds=self._settings.tool_timeout_seconds,
)
async with trace_span("task_agent_run"):
try:
# Use run() not run_stream() - Ollama has bugs with streaming + tools
result = await self.agent.run(prompt, deps=ctx)
return result.output
except Exception as e:
logger.exception(f"Task agent error: {e}")
raise
async def run_stream(
self,
prompt: str,
working_dir: str | None = None,
allowed_paths: list[str] | None = None,
**kwargs: Any
) -> AsyncIterator[str]:
"""
Run the task agent with streaming output.
Yields text chunks as they become available.
"""
ctx = TaskContext(
working_dir=working_dir or os.getcwd(),
allowed_paths=allowed_paths or self._settings.effective_allowed_paths,
timeout_seconds=self._settings.tool_timeout_seconds,
)
async with trace_span("task_agent_stream"):
try:
async with self.agent.run_stream(prompt, deps=ctx) as result:
async for chunk in result.stream_text():
yield chunk
except Exception as e:
logger.exception(f"Task agent stream error: {e}")
raise
# Create and register the singleton instance
task_agent = TaskAgentImpl()
register_agent(task_agent)
async def task(
prompt: str,
working_dir: str | None = None,
**kwargs: Any
) -> str:
"""Run task execution."""
return await task_agent.run(prompt, working_dir=working_dir, **kwargs)
async def task_stream(
prompt: str,
working_dir: str | None = None,
**kwargs: Any
) -> AsyncIterator[str]:
"""Run task execution with streaming."""
async for chunk in task_agent.run_stream(prompt, working_dir=working_dir, **kwargs):
yield chunk