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:
2026-02-16 07:43:31 -07:00
parent 911d362ba2
commit 50cf7165cb
11 changed files with 1987 additions and 103 deletions

127
agent.py
View File

@@ -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()