Add sub-agent orchestration, MCP tools, and critical bug fixes
Major Features: - Sub-agent orchestration system with dynamic specialist spawning * spawn_sub_agent(): Create specialists with custom prompts * delegate(): Convenience method for task delegation * Cached specialists for reuse * Separate conversation histories and focused context - MCP (Model Context Protocol) tool integration * Zettelkasten: fleeting_note, daily_note, permanent_note, literature_note * Search: search_vault (hybrid search), search_by_tags * Web: web_fetch for real-time data * Zero-cost file/system operations on Pro subscription Critical Bug Fixes: - Fixed max tool iterations (15 → 30, configurable) - Fixed max_tokens error in Agent SDK query() call - Fixed MCP tool routing in execute_tool() * Routes zettelkasten + web tools to async handlers * Prevents "Unknown tool" errors Documentation: - SUB_AGENTS.md: Complete guide to sub-agent system - MCP_MIGRATION.md: Agent SDK migration details - SOUL.example.md: Sanitized bot identity template - scheduled_tasks.example.yaml: Sanitized task config template Security: - Added obsidian vault to .gitignore - Protected SOUL.md and MEMORY.md (personal configs) - Sanitized example configs with placeholders Dependencies: - Added beautifulsoup4, httpx, lxml for web scraping - Updated requirements.txt Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
127
agent.py
127
agent.py
@@ -15,6 +15,8 @@ MAX_CONTEXT_MESSAGES = 20 # Optimized for Agent SDK flat-rate subscription
|
||||
MEMORY_RESPONSE_PREVIEW_LENGTH = 500 # Store more context for better memory retrieval
|
||||
# Maximum conversation history entries before pruning
|
||||
MAX_CONVERSATION_HISTORY = 100 # Higher limit with flat-rate subscription
|
||||
# Maximum tool execution iterations (generous limit for complex operations like zettelkasten)
|
||||
MAX_TOOL_ITERATIONS = 30 # Allows complex multi-step workflows with auto-linking, hybrid search, etc.
|
||||
|
||||
|
||||
class Agent:
|
||||
@@ -24,6 +26,8 @@ class Agent:
|
||||
self,
|
||||
provider: str = "claude",
|
||||
workspace_dir: str = "./memory_workspace",
|
||||
is_sub_agent: bool = False,
|
||||
specialist_prompt: Optional[str] = None,
|
||||
) -> None:
|
||||
self.memory = MemorySystem(workspace_dir)
|
||||
self.llm = LLMInterface(provider)
|
||||
@@ -32,8 +36,93 @@ class Agent:
|
||||
self._chat_lock = threading.Lock()
|
||||
self.healing_system = SelfHealingSystem(self.memory, self)
|
||||
|
||||
# Sub-agent orchestration
|
||||
self.is_sub_agent = is_sub_agent
|
||||
self.specialist_prompt = specialist_prompt
|
||||
self.sub_agents: dict = {} # Cache for spawned sub-agents
|
||||
|
||||
self.memory.sync()
|
||||
self.hooks.trigger("agent", "startup", {"workspace_dir": workspace_dir})
|
||||
if not is_sub_agent: # Only trigger hooks for main agent
|
||||
self.hooks.trigger("agent", "startup", {"workspace_dir": workspace_dir})
|
||||
|
||||
def spawn_sub_agent(
|
||||
self,
|
||||
specialist_prompt: str,
|
||||
agent_id: Optional[str] = None,
|
||||
share_memory: bool = True,
|
||||
) -> 'Agent':
|
||||
"""Spawn a sub-agent with specialized system prompt.
|
||||
|
||||
Args:
|
||||
specialist_prompt: Custom system prompt for the specialist
|
||||
agent_id: Optional ID for caching (reuse same specialist)
|
||||
share_memory: Whether to share memory workspace with main agent
|
||||
|
||||
Returns:
|
||||
Agent instance configured as a specialist
|
||||
|
||||
Example:
|
||||
sub = agent.spawn_sub_agent(
|
||||
specialist_prompt="You are a zettelkasten expert. Focus ONLY on note-taking.",
|
||||
agent_id="zettelkasten_processor"
|
||||
)
|
||||
result = sub.chat("Process my fleeting notes", username="jordan")
|
||||
"""
|
||||
# Check cache if agent_id provided
|
||||
if agent_id and agent_id in self.sub_agents:
|
||||
return self.sub_agents[agent_id]
|
||||
|
||||
# Create new sub-agent
|
||||
workspace = self.memory.workspace_dir if share_memory else f"{self.memory.workspace_dir}/sub_agents/{agent_id}"
|
||||
sub_agent = Agent(
|
||||
provider=self.llm.provider,
|
||||
workspace_dir=workspace,
|
||||
is_sub_agent=True,
|
||||
specialist_prompt=specialist_prompt,
|
||||
)
|
||||
|
||||
# Cache if ID provided
|
||||
if agent_id:
|
||||
self.sub_agents[agent_id] = sub_agent
|
||||
|
||||
return sub_agent
|
||||
|
||||
def delegate(
|
||||
self,
|
||||
task: str,
|
||||
specialist_prompt: str,
|
||||
username: str = "default",
|
||||
agent_id: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Delegate a task to a specialist sub-agent (convenience method).
|
||||
|
||||
Args:
|
||||
task: The task/message to send to the specialist
|
||||
specialist_prompt: System prompt defining the specialist's role
|
||||
username: Username for context
|
||||
agent_id: Optional ID for caching the specialist
|
||||
|
||||
Returns:
|
||||
Response from the specialist
|
||||
|
||||
Example:
|
||||
# One-off delegation
|
||||
result = agent.delegate(
|
||||
task="Process my fleeting notes and find connections",
|
||||
specialist_prompt="You are a zettelkasten expert. Focus on note organization and linking.",
|
||||
username="jordan"
|
||||
)
|
||||
|
||||
# Cached specialist (reused across multiple calls)
|
||||
result = agent.delegate(
|
||||
task="Summarize my emails from today",
|
||||
specialist_prompt="You are an email analyst. Focus on extracting key information.",
|
||||
username="jordan",
|
||||
agent_id="email_analyst"
|
||||
)
|
||||
"""
|
||||
sub_agent = self.spawn_sub_agent(specialist_prompt, agent_id=agent_id)
|
||||
return sub_agent.chat(task, username=username)
|
||||
|
||||
def _get_context_messages(self, max_messages: int) -> List[dict]:
|
||||
"""Get recent messages without breaking tool_use/tool_result pairs.
|
||||
@@ -140,19 +229,29 @@ class Agent:
|
||||
|
||||
def _chat_inner(self, user_message: str, username: str) -> str:
|
||||
"""Inner chat logic, called while holding _chat_lock."""
|
||||
soul = self.memory.get_soul()
|
||||
user_profile = self.memory.get_user(username)
|
||||
relevant_memory = self.memory.search_hybrid(user_message, max_results=5)
|
||||
# Use specialist prompt if this is a sub-agent, otherwise use full context
|
||||
if self.specialist_prompt:
|
||||
# Sub-agent: Use focused specialist prompt
|
||||
system = (
|
||||
f"{self.specialist_prompt}\n\n"
|
||||
f"You have access to {len(TOOL_DEFINITIONS)} tools. Use them to accomplish your specialized task. "
|
||||
f"Stay focused on your specialty and complete the task efficiently."
|
||||
)
|
||||
else:
|
||||
# Main agent: Use full SOUL, user profile, and memory context
|
||||
soul = self.memory.get_soul()
|
||||
user_profile = self.memory.get_user(username)
|
||||
relevant_memory = self.memory.search_hybrid(user_message, max_results=5)
|
||||
|
||||
memory_lines = [f"- {mem['snippet']}" for mem in relevant_memory]
|
||||
system = (
|
||||
f"{soul}\n\nUser Profile:\n{user_profile}\n\n"
|
||||
f"Relevant Memory:\n" + "\n".join(memory_lines) +
|
||||
f"\n\nYou have access to {len(TOOL_DEFINITIONS)} tools for file operations, "
|
||||
f"command execution, and Google services. Use them freely to help the user. "
|
||||
f"Note: You're running on a flat-rate Agent SDK subscription, so don't worry "
|
||||
f"about API costs when making multiple tool calls or processing large contexts."
|
||||
)
|
||||
memory_lines = [f"- {mem['snippet']}" for mem in relevant_memory]
|
||||
system = (
|
||||
f"{soul}\n\nUser Profile:\n{user_profile}\n\n"
|
||||
f"Relevant Memory:\n" + "\n".join(memory_lines) +
|
||||
f"\n\nYou have access to {len(TOOL_DEFINITIONS)} tools for file operations, "
|
||||
f"command execution, and Google services. Use them freely to help the user. "
|
||||
f"Note: You're running on a flat-rate Agent SDK subscription, so don't worry "
|
||||
f"about API costs when making multiple tool calls or processing large contexts."
|
||||
)
|
||||
|
||||
self.conversation_history.append(
|
||||
{"role": "user", "content": user_message}
|
||||
@@ -162,7 +261,7 @@ class Agent:
|
||||
self._prune_conversation_history()
|
||||
|
||||
# Tool execution loop
|
||||
max_iterations = 15 # Increased for complex multi-step operations
|
||||
max_iterations = MAX_TOOL_ITERATIONS
|
||||
# Enable caching for Sonnet to save 90% on repeated system prompts
|
||||
use_caching = "sonnet" in self.llm.model.lower()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user