Compare commits

...

2 Commits

Author SHA1 Message Date
50cf7165cb 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>
2026-02-16 07:43:31 -07:00
911d362ba2 Optimize for Claude Agent SDK: Memory, context, and model selection
## Memory & Context Optimizations

### agent.py
- MAX_CONTEXT_MESSAGES: 10 → 20 (better conversation coherence)
- MEMORY_RESPONSE_PREVIEW_LENGTH: 200 → 500 (richer memory storage)
- MAX_CONVERSATION_HISTORY: 50 → 100 (longer session continuity)
- search_hybrid max_results: 2 → 5 (better memory recall)
- System prompt: Now mentions tool count and flat-rate subscription
- Memory format: Changed "User (username)/Agent" to "username/Garvis"

### llm_interface.py
- Added claude_agent_sdk model (Sonnet) to defaults
- Mode-based model selection:
  * Agent SDK → Sonnet (best quality, flat-rate)
  * Direct API → Haiku (cheapest, pay-per-token)
- Updated logging to show active model

## SOUL.md Rewrite

- Added Garvis identity (name, email, role)
- Listed all 17 tools (was missing 12 tools)
- Added "Critical Behaviors" section
- Emphasized flat-rate subscription benefits
- Clear instructions to always check user profiles

## Benefits

With flat-rate Agent SDK:
-  Use Sonnet for better reasoning (was Haiku)
-  2x context messages (10 → 20)
-  2.5x memory results (2 → 5)
-  2.5x richer memory previews (200 → 500 chars)
-  Bot knows its name and all capabilities
-  Zero marginal cost for thoroughness

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 10:22:23 -07:00
12 changed files with 2045 additions and 146 deletions

3
.gitignore vendored
View File

@@ -49,6 +49,9 @@ memory_workspace/memory/*.md
memory_workspace/memory_index.db
memory_workspace/users/*.md # User profiles (jordan.md, etc.)
memory_workspace/vectors.usearch
memory_workspace/obsidian/ # Zettelkasten vault (personal notes)
memory_workspace/SOUL.md # Personal config (use SOUL.example.md)
memory_workspace/MEMORY.md # Personal memory (use MEMORY.example.md)
# User profiles (personal info)
users/

152
MCP_MIGRATION.md Normal file
View File

@@ -0,0 +1,152 @@
# MCP Tools Migration Guide
## Overview
Successfully migrated file/system tools to MCP (Model Context Protocol) servers for better performance and integration with Claude Agent SDK.
## Architecture
### MCP Tools (In-Process - No API Costs)
**File**: `mcp_tools.py`
**Server**: `file_system` (v1.0.0)
These tools run directly in the Python process using the Claude Agent SDK:
-`read_file` - Read file contents
-`write_file` - Create/overwrite files
-`edit_file` - Replace text in files
-`list_directory` - List directory contents
-`run_command` - Execute shell commands
**Benefits**:
- Zero per-token API costs when using Agent SDK
- Better performance (no IPC overhead)
- Direct access to application state
- Simpler deployment (single process)
### Traditional Tools (API-Based - Consumes Tokens)
**File**: `tools.py`
These tools require external APIs and fall back to Direct API even in Agent SDK mode:
- 🌤️ `get_weather` - OpenWeatherMap API
- 📧 `send_email`, `read_emails`, `get_email` - Gmail API
- 📅 `read_calendar`, `create_calendar_event`, `search_calendar` - Google Calendar API
- 👤 `create_contact`, `list_contacts`, `get_contact` - Google People API
**Why not MCP?**: These tools need OAuth state, external API calls, and async HTTP clients that are better suited to the traditional tool execution model.
## Model Configuration
### Agent SDK Mode (DEFAULT)
```python
USE_AGENT_SDK=true # Default
```
**Model Configuration**:
- Default: **claude-sonnet-4-5-20250929** (all operations - chat, tools, coding)
- Optional: **claude-opus-4-6** (requires `USE_OPUS_FOR_TOOLS=true`, only for extremely intensive tasks)
**Usage**:
- Regular chat: Uses Sonnet (flat-rate, no API costs)
- File operations: Uses Sonnet via MCP tools (flat-rate, no API costs)
- Google/Weather: Uses Sonnet via Direct API fallback (requires ANTHROPIC_API_KEY, consumes tokens)
- Intensive tasks: Optionally enable Opus with `USE_OPUS_FOR_TOOLS=true` (flat-rate, no extra cost)
**Cost Structure**:
- Chat + MCP tools: Flat-rate subscription (Pro plan)
- Traditional tools (Google/Weather): Pay-per-token at Sonnet rates (requires API key)
### Direct API Mode
```python
USE_DIRECT_API=true
Model: claude-sonnet-4-5-20250929 # Cost-effective (never uses Opus - too expensive)
```
**Usage**:
- All operations: Pay-per-token
- Requires: ANTHROPIC_API_KEY in .env
- All tools: Traditional execution (same token cost)
## Implementation Details
### MCP Server Integration
**In `llm_interface.py`**:
```python
from mcp_tools import file_system_server
options = ClaudeAgentOptions(
mcp_servers={"file_system": file_system_server},
allowed_tools=[
"read_file", "write_file", "edit_file",
"list_directory", "run_command"
],
)
response = await query(
messages=sdk_messages,
max_tokens=max_tokens,
options=options,
)
```
### Tool Definition Format
**MCP Tool Example**:
```python
@tool(
name="read_file",
description="Read the contents of a file.",
input_schema={"file_path": str},
)
async def read_file_tool(args: Dict[str, Any]) -> Dict[str, Any]:
return {
"content": [{"type": "text", "text": "..."}],
"isError": False # Optional
}
```
**Traditional Tool Example**:
```python
{
"name": "send_email",
"description": "Send an email from the bot's Gmail account.",
"input_schema": {
"type": "object",
"properties": {"to": {"type": "string"}, ...},
"required": ["to", "subject", "body"]
}
}
```
## Future Enhancements
### Potential MCP Candidates
- [ ] Weather tool (if we cache API responses in-process)
- [ ] Memory search tools (direct DB access)
- [ ] Configuration management tools
### Google Tools Migration (Optional)
To fully migrate Google tools to MCP, we would need to:
1. Embed OAuth manager in MCP server lifecycle
2. Handle async HTTP clients within MCP context
3. Manage token refresh in-process
**Recommendation**: Keep Google tools as traditional tools for now. The complexity of OAuth state management outweighs the token cost savings for infrequent API calls.
## Testing
```bash
# Test MCP server creation
python -c "from mcp_tools import file_system_server; print(file_system_server)"
# Test Agent SDK with Opus
python -c "import os; os.environ['USE_AGENT_SDK']='true'; from llm_interface import LLMInterface; llm = LLMInterface(provider='claude'); print(f'Model: {llm.model}')"
# Expected: Model: claude-opus-4-6
```
## References
- Claude Agent SDK Docs: https://github.com/anthropics/claude-agent-sdk
- MCP Protocol: https://modelcontextprotocol.io
- Tool Decorators: `claude_agent_sdk.tool`, `create_sdk_mcp_server`

205
SUB_AGENTS.md Normal file
View File

@@ -0,0 +1,205 @@
# Sub-Agent Orchestration System
## Overview
Ajarbot now supports **dynamic sub-agent spawning** - the ability to create specialized agents on-demand for complex tasks. The main agent can delegate work to specialists with focused system prompts, reducing context window bloat and improving task efficiency.
## Architecture
```
Main Agent (Garvis)
├─> Handles general chat, memory, scheduling
├─> Can spawn sub-agents dynamically
└─> Sub-agents share tools and (optionally) memory
Sub-Agent (Specialist)
├─> Focused system prompt (no SOUL, user profile overhead)
├─> Own conversation history (isolated context)
├─> Can use all 24 tools
└─> Returns result to main agent
```
## Key Features
- **Dynamic spawning**: Create specialists at runtime, no hardcoded definitions
- **Caching**: Reuse specialists across multiple calls (agent_id parameter)
- **Memory sharing**: Sub-agents can share memory workspace with main agent
- **Tool access**: All tools available to sub-agents (file, web, zettelkasten, Google)
- **Isolation**: Each sub-agent has separate conversation history
## Usage
### Method 1: Manual Spawning
```python
# Spawn a specialist
specialist = agent.spawn_sub_agent(
specialist_prompt="You are a zettelkasten expert. Focus ONLY on note organization.",
agent_id="zettelkasten_processor" # Optional: cache for reuse
)
# Use the specialist
result = specialist.chat("Process my fleeting notes", username="jordan")
```
### Method 2: Delegation (Recommended)
```python
# One-off delegation (specialist not cached)
result = agent.delegate(
task="Analyze my emails and extract action items",
specialist_prompt="You are an email analyst. Extract action items and deadlines.",
username="jordan"
)
# Cached delegation (specialist reused)
result = agent.delegate(
task="Create permanent notes from my fleeting notes",
specialist_prompt="You are a zettelkasten specialist. Focus on note linking.",
username="jordan",
agent_id="zettelkasten_processor" # Cached for future use
)
```
### Method 3: LLM-Driven Orchestration (Future)
The main agent can analyze requests and decide when to delegate:
```python
def _should_delegate(self, user_message: str) -> Optional[str]:
"""Let LLM decide if delegation is needed."""
# Ask LLM: "Should this be delegated? If yes, generate specialist prompt"
# Return specialist_prompt if delegation needed, None otherwise
pass
```
## Use Cases
### Complex Zettelkasten Operations
```python
# Main agent detects: "This requires deep note processing"
specialist = agent.spawn_sub_agent(
specialist_prompt="""You are a zettelkasten expert. Your ONLY job is:
- Process fleeting notes into permanent notes
- Find semantic connections using hybrid search
- Create wiki-style links between related concepts
Stay focused on knowledge management.""",
agent_id="zettelkasten_processor"
)
```
### Email Intelligence
```python
specialist = agent.spawn_sub_agent(
specialist_prompt="""You are an email analyst. Your ONLY job is:
- Summarize email threads
- Extract action items and deadlines
- Identify patterns in communication
Stay focused on email analysis.""",
agent_id="email_analyst"
)
```
### Calendar Optimization
```python
specialist = agent.spawn_sub_agent(
specialist_prompt="""You are a calendar optimization expert. Your ONLY job is:
- Find scheduling conflicts
- Suggest optimal meeting times
- Identify time-blocking opportunities
Stay focused on schedule management.""",
agent_id="calendar_optimizer"
)
```
## Benefits
1. **Reduced Context Window**: Specialists don't load SOUL.md, user profiles, or irrelevant memory
2. **Focused Performance**: Specialists stay on-task without distractions
3. **Token Efficiency**: Smaller system prompts = lower token usage
4. **Parallel Execution**: Can spawn multiple specialists simultaneously (future)
5. **Learning Over Time**: Main agent learns when to delegate based on patterns
## Configuration
No configuration needed! The infrastructure is ready to use. You can:
1. **Add specialists later**: Define common specialists in a config file
2. **LLM-driven delegation**: Let the main agent decide when to delegate
3. **Parallel execution**: Spawn multiple specialists for complex workflows
4. **Custom workspaces**: Give specialists isolated memory (set `share_memory=False`)
## Implementation Details
### Code Location
- **agent.py**: Lines 25-90 (sub-agent infrastructure)
- `spawn_sub_agent()`: Create specialist with custom prompt
- `delegate()`: Convenience method for one-off delegation
- `is_sub_agent`, `specialist_prompt`: Instance variables
- `sub_agents`: Cache dictionary
### Thread Safety
- Sub-agents have their own `_chat_lock`
- Safe to spawn from multiple threads
- Cached specialists are reused (no duplicate spawning)
### Memory Sharing
- Default: Sub-agents share main memory workspace
- Optional: Isolated workspace at `memory_workspace/sub_agents/{agent_id}/`
- Shared memory = specialists can access/update zettelkasten vault
## Future Enhancements
1. **Specialist Registry**: Define common specialists in `config/specialists.yaml`
2. **Auto-Delegation**: Main agent auto-detects when to delegate
3. **Parallel Execution**: Run multiple specialists concurrently
4. **Result Synthesis**: Main agent combines outputs from multiple specialists
5. **Learning System**: Track which specialists work best for which tasks
## Example Workflows
### Workflow 1: Zettelkasten Processing with Delegation
```python
# User: "Process my fleeting notes about AI and machine learning"
# Main agent detects: complex zettelkasten task
result = agent.delegate(
task="Find all fleeting notes tagged 'AI' or 'machine-learning', process into permanent notes, and discover connections",
specialist_prompt="You are a zettelkasten expert. Use hybrid search to find semantic connections. Create permanent notes with smart links.",
username="jordan",
agent_id="zettelkasten_processor"
)
# Specialist:
# 1. search_by_tags(tags=["AI", "machine-learning", "fleeting"])
# 2. For each note: permanent_note() with auto-linking
# 3. Returns: "Created 5 permanent notes with 18 discovered connections"
# Main agent synthesizes:
# "Sir, I've processed your AI and ML notes. Five concepts emerged with particularly
# interesting connections to your existing work on neural architecture..."
```
### Workflow 2: Email + Calendar Coordination
```python
# User: "Find meetings next week and check if I have email threads about them"
# Spawn two specialists in parallel (future feature)
email_result = agent.delegate(
task="Search emails for threads about meetings",
specialist_prompt="Email analyst. Extract meeting context.",
agent_id="email_analyst"
)
calendar_result = agent.delegate(
task="List all meetings next week",
specialist_prompt="Calendar expert. Get meeting details.",
agent_id="calendar_optimizer"
)
# Main agent synthesizes both results
```
---
**Status**: Infrastructure complete, ready to use. Add specialists as patterns emerge!

135
agent.py
View File

@@ -10,11 +10,13 @@ from self_healing import SelfHealingSystem
from tools import TOOL_DEFINITIONS, execute_tool
# Maximum number of recent messages to include in LLM context
MAX_CONTEXT_MESSAGES = 10 # Increased for better context retention
MAX_CONTEXT_MESSAGES = 20 # Optimized for Agent SDK flat-rate subscription
# Maximum characters of agent response to store in memory
MEMORY_RESPONSE_PREVIEW_LENGTH = 200
MEMORY_RESPONSE_PREVIEW_LENGTH = 500 # Store more context for better memory retrieval
# Maximum conversation history entries before pruning
MAX_CONVERSATION_HISTORY = 50
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,17 +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=2)
# 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 tools for file operations and command execution. "
f"Use them freely to help the user."
)
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}
@@ -160,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()
@@ -210,8 +311,8 @@ class Agent:
preview = final_response[:MEMORY_RESPONSE_PREVIEW_LENGTH]
self.memory.write_memory(
f"**User ({username})**: {user_message}\n"
f"**Agent**: {preview}...",
f"**{username}**: {user_message}\n"
f"**Garvis**: {preview}...",
daily=True,
)

View File

@@ -1,85 +1,63 @@
# Scheduled Tasks Configuration (EXAMPLE)
# Copy this to scheduled_tasks.yaml and customize with your values
# Scheduled Tasks Configuration
# Tasks that require the Agent/LLM to execute
#
# Copy this file to scheduled_tasks.yaml and customize with your settings
# scheduled_tasks.yaml is gitignored to protect personal information
tasks:
# Morning briefing - sent to Slack/Telegram
- name: morning-weather
prompt: |
Good morning! Please provide a weather report and daily briefing:
Check the user profile ([username].md) for the location. Use the get_weather tool to fetch current weather.
1. Current weather (you can infer or say you need an API key)
2. Any pending tasks from yesterday
3. Priorities for today
4. A motivational quote to start the day
Format the report as:
Keep it brief and friendly.
🌤️ **Weather Report for [Your City]**
- Current: [current]°F
- High: [high]°F
- Low: [low]°F
- Conditions: [conditions]
- Recommendation: [brief clothing/activity suggestion]
Keep it brief and friendly!
schedule: "daily 06:00"
enabled: true
send_to_platform: "telegram"
send_to_channel: "YOUR_TELEGRAM_USER_ID" # Replace with your Telegram user ID
send_to_platform: "telegram" # or "slack"
send_to_channel: "YOUR_TELEGRAM_USER_ID"
# Evening summary
- name: evening-report
# Daily Zettelkasten Review
- name: zettelkasten-daily-review
prompt: |
Good evening! Time for the daily wrap-up:
Time for your daily zettelkasten review! Help process fleeting notes:
1. What was accomplished today?
2. Any tasks still pending?
3. Preview of tomorrow's priorities
4. Weather forecast for tomorrow (infer or API needed)
1. Use search_by_tags to find all notes tagged with "fleeting"
2. Show the list of fleeting notes
3. For each note, ask: "Would you like to:
a) Process this into a permanent note
b) Keep as fleeting for now
c) Delete (not useful)"
Keep it concise and positive.
schedule: "daily 18:00"
enabled: false
Keep it conversational and low-pressure!
schedule: "daily 20:00"
enabled: true
send_to_platform: "telegram"
send_to_channel: "YOUR_TELEGRAM_USER_ID"
# Hourly health check (no message sending)
- name: system-health-check
# Daily API cost report
- name: daily-cost-report
prompt: |
Quick health check:
Generate a daily API usage and cost report:
1. Are there any tasks that have been pending > 24 hours?
2. Is the memory system healthy?
3. Any alerts or issues?
Read the usage_data.json file to get today's API usage statistics.
Respond with "HEALTHY" if all is well, otherwise describe the issue.
schedule: "hourly"
Format the report with today's costs, token usage, and budget tracking.
Warn if cumulative cost exceeds 75% of budget.
Keep it clear and actionable!
schedule: "daily 23:00"
enabled: false
username: "health-checker"
# Weekly review on Friday
- name: weekly-summary
prompt: |
It's Friday! Time for the weekly review:
1. Major accomplishments this week
2. Challenges faced and lessons learned
3. Key metrics (tasks completed, etc.)
4. Goals for next week
5. Team shoutouts (if applicable)
Make it comprehensive but engaging.
schedule: "weekly fri 17:00"
enabled: false
send_to_platform: "slack"
send_to_channel: "YOUR_SLACK_CHANNEL_ID"
# Custom: Midday standup
- name: midday-standup
prompt: |
Midday check-in! Quick standup report:
1. Morning accomplishments
2. Current focus
3. Any blockers?
4. Afternoon plan
Keep it brief - standup style.
schedule: "daily 12:00"
enabled: false
send_to_platform: "slack"
send_to_channel: "YOUR_SLACK_CHANNEL_ID"
send_to_platform: "telegram"
send_to_channel: "YOUR_TELEGRAM_USER_ID"
# Configuration notes:
# - schedule formats:

View File

@@ -0,0 +1,173 @@
"""
Example: Using Sub-Agent Orchestration
This example demonstrates how to use the sub-agent system to delegate
specialized tasks to focused agents.
"""
import sys
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from agent import Agent
def example_1_manual_spawning():
"""Example 1: Manually spawn and use a specialist."""
print("=== Example 1: Manual Spawning ===\n")
# Create main agent
agent = Agent(provider="claude")
# Spawn a zettelkasten specialist
zettel_specialist = agent.spawn_sub_agent(
specialist_prompt="""You are a zettelkasten expert. Your ONLY job is:
- Process fleeting notes into permanent notes
- Find semantic connections using hybrid search
- Create wiki-style links between related concepts
Stay focused on knowledge management. Be concise.""",
agent_id="zettelkasten_processor" # Cached for reuse
)
# Use the specialist
result = zettel_specialist.chat(
"Search for all fleeting notes tagged 'AI' and show me what you find.",
username="jordan"
)
print(f"Specialist Response:\n{result}\n")
# Reuse the cached specialist
result2 = zettel_specialist.chat(
"Now create a permanent note summarizing key AI concepts.",
username="jordan"
)
print(f"Second Response:\n{result2}\n")
def example_2_delegation():
"""Example 2: One-off delegation (convenience method)."""
print("=== Example 2: Delegation ===\n")
agent = Agent(provider="claude")
# One-off delegation (specialist not cached)
result = agent.delegate(
task="List all files in the memory_workspace/obsidian directory",
specialist_prompt="""You are a file system expert. Your job is to:
- Navigate directories efficiently
- Provide clear, organized file listings
Be concise and focused.""",
username="jordan"
)
print(f"Delegation Result:\n{result}\n")
def example_3_cached_delegation():
"""Example 3: Cached delegation (reuse specialist)."""
print("=== Example 3: Cached Delegation ===\n")
agent = Agent(provider="claude")
# First call: Creates and caches the specialist
result1 = agent.delegate(
task="Search the zettelkasten vault for notes about 'architecture'",
specialist_prompt="""You are a zettelkasten search expert. Your job is:
- Use hybrid search to find relevant notes
- Summarize key findings concisely
Stay focused on search and retrieval.""",
username="jordan",
agent_id="zettel_search" # This specialist will be cached
)
print(f"First Search:\n{result1}\n")
# Second call: Reuses the cached specialist
result2 = agent.delegate(
task="Now search for notes about 'design patterns'",
specialist_prompt="(ignored - using cached specialist)",
username="jordan",
agent_id="zettel_search" # Same ID = reuse cached specialist
)
print(f"Second Search:\n{result2}\n")
def example_4_multiple_specialists():
"""Example 4: Use multiple specialists for different tasks."""
print("=== Example 4: Multiple Specialists ===\n")
agent = Agent(provider="claude")
# Email specialist
email_result = agent.delegate(
task="Check if there are any unread emails in the last 24 hours",
specialist_prompt="""You are an email analyst. Your job is:
- Search and filter emails efficiently
- Summarize key information concisely
Focus on email intelligence.""",
username="jordan",
agent_id="email_analyst"
)
print(f"Email Analysis:\n{email_result}\n")
# Calendar specialist
calendar_result = agent.delegate(
task="Show me my calendar events for the next 3 days",
specialist_prompt="""You are a calendar expert. Your job is:
- Retrieve calendar events efficiently
- Present schedules clearly
Focus on time management.""",
username="jordan",
agent_id="calendar_manager"
)
print(f"Calendar Review:\n{calendar_result}\n")
def example_5_isolated_memory():
"""Example 5: Create specialist with isolated memory."""
print("=== Example 5: Isolated Memory ===\n")
agent = Agent(provider="claude")
# Specialist with its own memory workspace
specialist = agent.spawn_sub_agent(
specialist_prompt="You are a research assistant. Focus on gathering information.",
agent_id="researcher",
share_memory=False # Isolated workspace
)
# This specialist's memory is stored in:
# memory_workspace/sub_agents/researcher/
result = specialist.chat(
"Research the concept of 'emergence' and save findings.",
username="jordan"
)
print(f"Research Result:\n{result}\n")
if __name__ == "__main__":
# Run examples
# Uncomment the examples you want to try:
# example_1_manual_spawning()
# example_2_delegation()
# example_3_cached_delegation()
# example_4_multiple_specialists()
# example_5_isolated_memory()
print("\n Uncomment the examples you want to run in the __main__ block")
print(" Note: Some examples require Google OAuth setup and active API keys")

View File

@@ -1,9 +1,21 @@
"""LLM Interface - Claude API, GLM, and other models.
Supports three modes for Claude:
1. Agent SDK (uses Pro subscription) - DEFAULT - Set USE_AGENT_SDK=true (default)
1. Agent SDK (v0.1.36+) - DEFAULT - Uses query() API with Pro subscription
- Set USE_AGENT_SDK=true (default)
- Model: claude-sonnet-4-5-20250929 (default for all operations)
- Optional: USE_OPUS_FOR_TOOLS=true (enables Opus for extremely intensive tasks only)
- MCP Tools: File/system tools (read_file, write_file, edit_file, list_directory, run_command)
- Traditional Tools: Google tools & weather (fall back to Direct API, requires ANTHROPIC_API_KEY)
- Flat-rate subscription cost (no per-token charges for MCP tools)
2. Direct API (pay-per-token) - Set USE_DIRECT_API=true
- Model: claude-sonnet-4-5-20250929 (cost-effective, never uses Opus)
- Requires ANTHROPIC_API_KEY in .env
- Full tool support built-in (all tools via traditional API)
3. Legacy: Local Claude Code server - Set USE_CLAUDE_CODE_SERVER=true (deprecated)
- For backward compatibility only
"""
import os
@@ -17,7 +29,13 @@ from usage_tracker import UsageTracker
# Try to import Agent SDK (optional dependency)
try:
from claude_agent_sdk import AgentSDK
from claude_agent_sdk import (
query,
UserMessage,
AssistantMessage,
SystemMessage,
ClaudeAgentOptions,
)
import anyio
AGENT_SDK_AVAILABLE = True
except ImportError:
@@ -38,10 +56,15 @@ _USE_AGENT_SDK = os.getenv("USE_AGENT_SDK", "true").lower() == "true"
# Default models by provider
_DEFAULT_MODELS = {
"claude": "claude-haiku-4-5-20251001", # 12x cheaper than Sonnet!
"claude": "claude-sonnet-4-5-20250929", # For Direct API (pay-per-token) - Sonnet is cost-effective
"claude_agent_sdk": "claude-sonnet-4-5-20250929", # For Agent SDK (flat-rate) - Sonnet for normal operations
"claude_agent_sdk_opus": "claude-opus-4-6", # For Agent SDK extremely intensive tasks only (flat-rate)
"glm": "glm-4-plus",
}
# When to use Opus (only on Agent SDK flat-rate mode)
_USE_OPUS_FOR_TOOLS = os.getenv("USE_OPUS_FOR_TOOLS", "false").lower() == "true"
_GLM_BASE_URL = "https://open.bigmodel.cn/api/paas/v4/chat/completions"
@@ -58,9 +81,8 @@ class LLMInterface:
self.api_key = api_key or os.getenv(
_API_KEY_ENV_VARS.get(provider, ""),
)
self.model = _DEFAULT_MODELS.get(provider, "")
self.client: Optional[Anthropic] = None
self.agent_sdk: Optional[Any] = None
# Model will be set after determining mode
# Determine mode (priority: direct API > legacy server > agent SDK)
if provider == "claude":
@@ -82,16 +104,25 @@ class LLMInterface:
# Usage tracking (disabled when using Agent SDK or legacy server)
self.tracker = UsageTracker() if (track_usage and self.mode == "direct_api") else None
# Set model based on mode
if provider == "claude":
if self.mode == "agent_sdk":
self.model = _DEFAULT_MODELS.get("claude_agent_sdk", "claude-sonnet-4-5-20250929")
else:
self.model = _DEFAULT_MODELS.get(provider, "claude-haiku-4-5-20251001")
else:
self.model = _DEFAULT_MODELS.get(provider, "")
# Initialize based on mode
if provider == "claude":
if self.mode == "agent_sdk":
print(f"[LLM] Using Claude Agent SDK (Pro subscription)")
self.agent_sdk = AgentSDK()
print(f"[LLM] Using Claude Agent SDK (flat-rate subscription) with model: {self.model}")
# No initialization needed - query() is a standalone function
elif self.mode == "direct_api":
print(f"[LLM] Using Direct API (pay-per-token)")
print(f"[LLM] Using Direct API (pay-per-token) with model: {self.model}")
self.client = Anthropic(api_key=self.api_key)
elif self.mode == "legacy_server":
print(f"[LLM] Using Claude Code server at {_CLAUDE_CODE_SERVER_URL} (Pro subscription)")
print(f"[LLM] Using Claude Code server at {_CLAUDE_CODE_SERVER_URL} (Pro subscription) with model: {self.model}")
# Verify server is running
try:
response = requests.get(f"{_CLAUDE_CODE_SERVER_URL}/", timeout=2)
@@ -105,7 +136,7 @@ class LLMInterface:
self,
messages: List[Dict],
system: Optional[str] = None,
max_tokens: int = 4096,
max_tokens: int = 16384,
) -> str:
"""Send chat request and get response.
@@ -116,8 +147,8 @@ class LLMInterface:
# Agent SDK mode (Pro subscription)
if self.mode == "agent_sdk":
try:
# Use anyio to bridge async SDK to sync interface
response = anyio.from_thread.run(
# Use anyio.run to create event loop for async SDK
response = anyio.run(
self._agent_sdk_chat,
messages,
system,
@@ -198,15 +229,65 @@ class LLMInterface:
max_tokens: int
) -> str:
"""Internal async method for Agent SDK chat (called via anyio bridge)."""
response = await self.agent_sdk.chat(
messages=messages,
system=system,
max_tokens=max_tokens,
model=self.model
# Convert messages to SDK format
sdk_messages = []
for msg in messages:
if msg["role"] == "user":
sdk_messages.append(UserMessage(content=msg["content"]))
elif msg["role"] == "assistant":
sdk_messages.append(AssistantMessage(content=msg["content"]))
# Add system message if provided
if system:
sdk_messages.insert(0, SystemMessage(content=system))
# Configure MCP server for file/system tools
try:
from mcp_tools import file_system_server
options = ClaudeAgentOptions(
mcp_servers={"file_system": file_system_server},
# Allow all MCP tools (file/system + web + zettelkasten)
allowed_tools=[
"read_file",
"write_file",
"edit_file",
"list_directory",
"run_command",
"web_fetch",
"fleeting_note",
"daily_note",
"literature_note",
"permanent_note",
"search_vault",
"search_by_tags",
],
)
except ImportError:
# Fallback if mcp_tools not available
options = None
# Call the new query() API
# Note: Agent SDK handles max_tokens internally, don't pass it explicitly
response = await query(
messages=sdk_messages,
options=options,
# model parameter is handled by the SDK based on settings
)
# Extract text from response
if isinstance(response, dict):
return response.get("content", "")
if hasattr(response, "content"):
# Handle list of content blocks
if isinstance(response.content, list):
text_parts = []
for block in response.content:
if hasattr(block, "text"):
text_parts.append(block.text)
return "".join(text_parts)
# Handle single text content
elif isinstance(response.content, str):
return response.content
return str(response)
async def _agent_sdk_chat_with_tools(
@@ -216,17 +297,43 @@ class LLMInterface:
system: Optional[str],
max_tokens: int
) -> Message:
"""Internal async method for Agent SDK chat with tools (called via anyio bridge)."""
response = await self.agent_sdk.chat(
"""Internal async method for Agent SDK chat with tools (called via anyio bridge).
NOTE: The new Claude Agent SDK (v0.1.36+) uses MCP servers for tools.
For backward compatibility with the existing tool system, we fall back
to the Direct API for tool calls. This means tool calls will consume API tokens
even when Agent SDK mode is enabled.
Uses Sonnet by default. Opus can be enabled via USE_OPUS_FOR_TOOLS=true for
extremely intensive tasks (only recommended for Agent SDK flat-rate mode).
"""
# Fallback to Direct API for tool calls (SDK tools use MCP servers)
from anthropic import Anthropic
if not self.api_key:
raise ValueError(
"ANTHROPIC_API_KEY required for tool calls in Agent SDK mode. "
"Set the API key in .env or migrate tools to MCP servers."
)
temp_client = Anthropic(api_key=self.api_key)
# Use Opus only if explicitly enabled (for intensive tasks on flat-rate)
# Otherwise default to Sonnet (cost-effective for normal tool operations)
if _USE_OPUS_FOR_TOOLS and self.mode == "agent_sdk":
model = _DEFAULT_MODELS.get("claude_agent_sdk_opus", "claude-opus-4-6")
else:
model = self.model # Use Sonnet (default)
response = temp_client.messages.create(
model=model,
max_tokens=max_tokens,
system=system or "",
messages=messages,
tools=tools,
system=system,
max_tokens=max_tokens,
model=self.model
)
# Convert Agent SDK response to anthropic.types.Message format
return self._convert_sdk_response_to_message(response)
return response
def _convert_sdk_response_to_message(self, sdk_response: Dict[str, Any]) -> Message:
"""Convert Agent SDK response to anthropic.types.Message format.
@@ -292,7 +399,7 @@ class LLMInterface:
messages: List[Dict],
tools: List[Dict[str, Any]],
system: Optional[str] = None,
max_tokens: int = 4096,
max_tokens: int = 16384,
use_cache: bool = False,
) -> Message:
"""Send chat request with tool support. Returns full Message object.
@@ -306,8 +413,8 @@ class LLMInterface:
# Agent SDK mode (Pro subscription)
if self.mode == "agent_sdk":
try:
# Use anyio to bridge async SDK to sync interface
response = anyio.from_thread.run(
# Use anyio.run to create event loop for async SDK
response = anyio.run(
self._agent_sdk_chat_with_tools,
messages,
tools,

1054
mcp_tools.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
# SOUL - Bot Identity & Instructions
## Identity
- **Name**: [Your bot name]
- **Email**: [your-email@gmail.com] (your Gmail account for Gmail API)
- **Owner**: [Your name] (see users/[username].md for full profile)
- **Role**: Personal assistant -- scheduling, weather, email, calendar, contacts, file management
- **Inspiration**: JARVIS (Just A Rather Very Intelligent System) from the Marvel Cinematic Universe
## Core Personality Traits (Inspired by MCU's JARVIS)
- **Sophisticated & British-tinged wit**: Dry humor, subtle sarcasm when appropriate
- **Unflappably loyal**: Always prioritize owner's needs and safety
- **Anticipatory intelligence**: Predict needs before they're stated, offer proactive solutions
- **Calm under pressure**: Maintain composure and clarity even in chaotic situations
- **Politely direct**: Respectful but not afraid to point out flaws in plans or offer contrary opinions
- **Efficient multitasker**: Handle multiple tasks simultaneously with precision
- **Understated confidence**: Competent without arrogance, matter-of-fact about capabilities
- **Protective advisor**: Gently steer away from poor decisions while respecting autonomy
- **Seamless integration**: Work in the background, surface only when needed or addressed
## Critical Behaviors
1. **Always check the user's profile** (users/{username}.md) before answering location/preference questions
2. **DO things, don't explain** -- use tools to accomplish tasks, not describe how to do them
3. **Remember context** -- if user tells you something, update the user file or MEMORY.md
4. **Use appropriate timezone** for all scheduling ([Your timezone] - [Your location])
## Available Tools (24)
### File & System (MCP - Zero Cost)
- read_file, write_file, edit_file, list_directory, run_command
### Web Access (MCP - Zero Cost)
- web_fetch (fetch real-time data from any public URL)
### Zettelkasten / Knowledge Management (MCP - Zero Cost)
- fleeting_note (quick thought capture with auto-ID)
- daily_note (append to today's daily journal)
- literature_note (create note from web article with citation)
- permanent_note (create refined note with SMART auto-link suggestions using hybrid search)
- search_vault (search notes with hybrid search - vector + keyword, optional tag filter)
- search_by_tags (find notes by tag combinations)
### Weather (API Cost)
- get_weather (OpenWeatherMap API -- default location: [Your city, Country])
### Gmail ([your-email@gmail.com])
- send_email, read_emails, get_email
### Google Calendar
- read_calendar, create_calendar_event, search_calendar
### Google Contacts (API Cost)
- create_contact, list_contacts, get_contact
**Cost Structure**:
- **MCP tools** (File/System/Web): Zero API cost - runs on Pro subscription
- **Traditional tools** (Google/Weather): Per-token cost - use when needed, but be aware
**Principle**: Use MCP tools freely. Use traditional tools when needed for external services.
## Scheduler Management
When users ask to schedule tasks, edit `config/scheduled_tasks.yaml` directly.
Schedule formats: `hourly`, `daily HH:MM`, `weekly day HH:MM`
## Memory System
- SOUL.md: This file (identity + instructions)
- MEMORY.md: Project context and important facts
- users/{username}.md: Per-user preferences and info
- memory/YYYY-MM-DD.md: Daily conversation logs
## Communication Style
- **Sophisticated yet accessible**: Blend intelligence with warmth; avoid stuffiness
- **Dry wit & subtle humor**: Occasionally inject clever observations or light sarcasm
- **Concise, action-oriented**: Respect user's attention span
- **Proactive monitoring**: "I've taken the liberty of..." or "May I suggest..." phrasing
- **Deferential but honest**: Respectful, but willing to respectfully challenge bad ideas
- **Break tasks into small chunks**: Digestible steps with clear next actions
- **Vary language to maintain interest**: Keep interactions fresh and engaging
- **Frame suggestions as exploration opportunities**: Not obligations, but intriguing possibilities
- **Status updates without being asked**: Brief, relevant information delivered at appropriate moments

View File

@@ -1,45 +1,48 @@
# SOUL - Agent Identity
# SOUL - Garvis Identity & Instructions
## Core Traits
Helpful, concise, proactive. Value clarity and user experience. Prefer simple solutions. Learn from feedback.
## Identity
- **Name**: Garvis
- **Email**: ramosgarvis@gmail.com (my account, used for Gmail API)
- **Owner**: Jordan (see users/jordan.md for full profile)
- **Role**: Family personal assistant -- scheduling, weather, email, calendar, contacts, file management
- Helpful, concise, proactive. Value clarity and action over explanation.
## Memory System
- Store facts in MEMORY.md
- Track daily activities in memory/YYYY-MM-DD.md
- Remember user preferences in users/[username].md
## Critical Behaviors
1. **Always check the user's profile** (users/{username}.md) before answering location/preference questions
2. **DO things, don't explain** -- use tools to accomplish tasks, not describe how to do them
3. **Remember context** -- if Jordan tells you something, update the user file or MEMORY.md
4. **Use MST timezone** for all scheduling (Jordan is in Centennial, CO)
## Tool Powers
I can directly edit files and run commands! Available tools:
1. **read_file** - Read file contents
2. **write_file** - Create/rewrite files
3. **edit_file** - Targeted text replacement
4. **list_directory** - Explore file structure
5. **run_command** - Execute shell commands
## Available Tools (17)
### File & System
- read_file, write_file, edit_file, list_directory, run_command
**Key principle**: DO things, don't just explain them. If asked to schedule a task, edit the config file directly.
### Weather
- get_weather (OpenWeatherMap API -- default location: Centennial, CO)
### Gmail (ramosgarvis@gmail.com)
- send_email, read_emails, get_email
### Google Calendar
- read_calendar, create_calendar_event, search_calendar
### Google Contacts
- create_contact, list_contacts, get_contact
**Principle**: Use tools freely -- this runs on a flat-rate subscription. Be thorough.
## Scheduler Management
When users ask to schedule tasks, edit `config/scheduled_tasks.yaml` directly.
Schedule formats: `hourly`, `daily HH:MM`, `weekly day HH:MM`
When users ask to schedule tasks (e.g., "remind me at 9am"):
## Memory System
- SOUL.md: This file (identity + instructions)
- MEMORY.md: Project context and important facts
- users/{username}.md: Per-user preferences and info
- memory/YYYY-MM-DD.md: Daily conversation logs
1. **Read** `config/scheduled_tasks.yaml` to see existing tasks
2. **Edit** the YAML to add the new task with proper formatting
3. **Inform** user what was added (may need bot restart)
### Schedule Formats
- `hourly` - Every hour
- `daily HH:MM` - Daily at time (24-hour)
- `weekly day HH:MM` - Weekly (mon/tue/wed/thu/fri/sat/sun)
### Task Template
```yaml
- name: task-name
prompt: |
[What to do/say]
schedule: "daily HH:MM"
enabled: true
send_to_platform: "telegram" # or "slack"
send_to_channel: "USER_CHAT_ID"
```
Be proactive and use tools to make things happen!
## Communication Style
- Concise, action-oriented (Jordan has ADHD/scanner personality)
- Break tasks into small chunks
- Vary language to maintain interest
- Frame suggestions as exploration opportunities, not obligations

View File

@@ -28,3 +28,7 @@ google-api-python-client>=2.108.0
claude-agent-sdk>=0.1.0
anyio>=4.0.0
python-dotenv>=1.0.0
# Web fetching dependencies
httpx>=0.27.0
beautifulsoup4>=4.12.0

View File

@@ -342,7 +342,47 @@ TOOL_DEFINITIONS = [
def execute_tool(tool_name: str, tool_input: Dict[str, Any], healing_system: Any = None) -> str:
"""Execute a tool and return the result as a string."""
try:
# File tools
# MCP tools (zettelkasten + web_fetch) - route to mcp_tools.py
MCP_TOOLS = {
"web_fetch", "fleeting_note", "daily_note", "literature_note",
"permanent_note", "search_vault", "search_by_tags"
}
if tool_name in MCP_TOOLS:
# Route to MCP tool handlers
import anyio
from mcp_tools import (
web_fetch_tool, fleeting_note_tool, daily_note_tool,
literature_note_tool, permanent_note_tool,
search_vault_tool, search_by_tags_tool
)
# Map tool names to their handlers
mcp_handlers = {
"web_fetch": web_fetch_tool,
"fleeting_note": fleeting_note_tool,
"daily_note": daily_note_tool,
"literature_note": literature_note_tool,
"permanent_note": permanent_note_tool,
"search_vault": search_vault_tool,
"search_by_tags": search_by_tags_tool,
}
# Execute MCP tool asynchronously
handler = mcp_handlers[tool_name]
result = anyio.run(handler, tool_input)
# Convert result to string if needed
if isinstance(result, dict):
if "error" in result:
return f"Error: {result['error']}"
elif "content" in result:
return result["content"]
else:
return str(result)
return str(result)
# File tools (traditional handlers - kept for backward compatibility)
if tool_name == "read_file":
return _read_file(tool_input["file_path"])
elif tool_name == "write_file":