feat(chat): add agent orchestration system with plan_and_execute
Introduces TaskPlanner and AgentSpec so Pyra can decompose multi-step tasks into sequential steps, each executed with a focused sub-agent context rather than the full conversation history. - plugins/base.py: AgentSpec dataclass + agent_spec() on Protocol/BasePlugin - plugins/registry.py: register_builtin, get_agent, list_agents - chat/planner.py: TaskPlanner with plan approval, per-step tool-use loop, verification call, and agent-aware routing - chat/session.py: wires plan_and_execute as a built-in tool after load_all - chat/history.py: planning hint in system prompt + dynamic agents listing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,12 @@ class Tool:
|
||||
requires_approval: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentSpec:
|
||||
description: str # one-liner shown in orchestrator's system prompt
|
||||
system_prompt: str # full context injected when this agent executes a step
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class PyraPlugin(Protocol):
|
||||
name: str
|
||||
@@ -26,6 +32,7 @@ class PyraPlugin(Protocol):
|
||||
def tools(self) -> list[Tool]: ...
|
||||
def slash_commands(self) -> dict[str, Callable[[], None]]: ...
|
||||
def system_prompt_addition(self) -> str: ...
|
||||
def agent_spec(self) -> AgentSpec | None: ...
|
||||
def setup(self, console: Console, vault_writer: Callable[[str, str], None]) -> None: ...
|
||||
def daemon_tasks(self) -> list[Coroutine]: ... # type: ignore[type-arg]
|
||||
|
||||
@@ -49,6 +56,9 @@ class BasePlugin:
|
||||
def system_prompt_addition(self) -> str:
|
||||
return ""
|
||||
|
||||
def agent_spec(self) -> AgentSpec | None:
|
||||
return None
|
||||
|
||||
def setup(self, console: Any, vault_writer: Callable[[str, str], None]) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Callable, Coroutine
|
||||
|
||||
from pyra.plugins.base import PyraPlugin, Tool
|
||||
from pyra.plugins.base import AgentSpec, PyraPlugin, Tool
|
||||
from pyra.plugins.loader import _log_error, load_plugins
|
||||
from pyra.vault.reader import get_key
|
||||
|
||||
@@ -77,3 +77,25 @@ class PluginRegistry:
|
||||
|
||||
def find_tool(self, name: str) -> Tool | None:
|
||||
return self._tools.get(name)
|
||||
|
||||
def register_builtin(self, tool: Tool) -> None:
|
||||
"""Register a built-in tool independent of plugins. Call after load_all."""
|
||||
self._tools[tool.name] = tool
|
||||
|
||||
def get_agent(self, name: str) -> tuple[AgentSpec, list[Tool]] | None:
|
||||
"""Return (AgentSpec, tools) for a named plugin agent, or None."""
|
||||
plugin = self._plugins.get(name)
|
||||
if plugin is None:
|
||||
return None
|
||||
spec = plugin.agent_spec()
|
||||
if spec is None:
|
||||
return None
|
||||
return (spec, plugin.tools())
|
||||
|
||||
def list_agents(self) -> list[tuple[str, AgentSpec]]:
|
||||
"""Return (plugin_name, AgentSpec) for all plugins that have agents."""
|
||||
return [
|
||||
(name, plugin.agent_spec())
|
||||
for name, plugin in self._plugins.items()
|
||||
if plugin.agent_spec() is not None
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user