From cc3148adc4c41616e9e2e347f71fc3280a8ad9ba Mon Sep 17 00:00:00 2001 From: Pierre Wessman <4029607+pierrewessman@users.noreply.github.com> Date: Thu, 27 Nov 2025 10:40:00 +0100 Subject: [PATCH] updated plan: use claude agent sdk --- PLAN.md | 954 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 677 insertions(+), 277 deletions(-) diff --git a/PLAN.md b/PLAN.md index 478bcc5..ed82cf7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -4,6 +4,135 @@ A multi-agent system where Claude-powered agents collaborate via Slack, each run --- +## Claude Agent SDK Integration Notes + +### Key SDK Concepts + +1. **Two APIs**: + - `query()` - One-shot, creates new session each time + - `ClaudeSDKClient` - Persistent session, maintains conversation history + + We use `ClaudeSDKClient` because agents need to remember context across messages. + +2. **Built-in Tools**: The SDK includes Claude Code's tools: + - `Read`, `Write`, `Edit` - File operations + - `Bash` - Shell commands + - `Glob`, `Grep` - File search + - `WebSearch`, `WebFetch` - Web access + - `Task` - Subagent spawning + - `NotebookEdit` - Jupyter notebooks + +3. **Custom Tools via SDK MCP Servers**: + - Use `@tool` decorator and `create_sdk_mcp_server()` + - Runs in-process (no subprocess overhead) + - Tools are prefixed: `mcp____` + +4. **Hooks for Permissions**: + - `PreToolUse` - Check before tool execution, can deny + - `PostToolUse` - Log/audit after execution + - Defined via `HookMatcher` in `ClaudeAgentOptions` + +5. **Permission Modes**: + - `"default"` - Standard permission behavior + - `"acceptEdits"` - Auto-accept file edits + - `"bypassPermissions"` - Skip all checks (use with caution) + +### Architecture Decision: Orchestrator vs. Per-Container SDK + +**Option A: Orchestrator runs SDK (chosen)** +``` +┌─────────────────────────────────────────┐ +│ Orchestrator │ +│ - Runs ClaudeSDKClient per agent │ +│ - Routes Slack messages │ +│ - Agents execute via SDK tools │ +│ (Bash, Write, etc. run in container) │ +└─────────────────────────────────────────┘ + │ docker exec + ▼ +┌─────────────────┐ +│ Agent Container │ +│ - Workspace │ +│ - Git clone │ +│ - No SDK here │ +└─────────────────┘ +``` + +**Option B: SDK runs inside each container** +``` +┌─────────────────┐ ┌─────────────────┐ +│ Agent Container │ │ Agent Container │ +│ - SDK Client │ │ - SDK Client │ +│ - Claude CLI │ │ - Claude CLI │ +│ - Node.js │ │ - Node.js │ +└─────────────────┘ └─────────────────┘ +``` + +We choose **Option A** because: +- Simpler container images (no Node.js/CLI required) +- Centralized orchestration and routing +- SDK's built-in tools (Bash, Write) can still operate on container filesystems via mounted volumes +- Easier to manage API keys and rate limits + +### Setting Up SDK with Agent Options + +```python +from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, tool, create_sdk_mcp_server + +# Create custom tools +@tool("slack_send", "Send Slack message", {"channel": str, "text": str}) +async def slack_send(args): + await slack_client.chat_postMessage( + channel=args["channel"], + text=args["text"] + ) + return {"content": [{"type": "text", "text": "Sent"}]} + +# Create MCP server +tools_server = create_sdk_mcp_server( + name="agent-tools", + tools=[slack_send] +) + +# Configure agent +options = ClaudeAgentOptions( + system_prompt=agent_config.system_prompt, + model="sonnet", + + # Built-in tools + allowed_tools=["Read", "Write", "Edit", "Bash", "Glob", "Grep"], + disallowed_tools=["Task"], # Don't allow spawning subagents + + # Custom tools via MCP + mcp_servers={"tools": tools_server}, + + # Permission mode + permission_mode="acceptEdits", + + # Working directory (mapped to container volume) + cwd="/data/workspaces/developer", + + # Hooks for fine-grained control + hooks={ + "PreToolUse": [HookMatcher(hooks=[permission_checker])] + } +) + +# Create persistent client +async with ClaudeSDKClient(options) as client: + # Send message (client remembers context) + await client.query("Create a hello.py file") + async for msg in client.receive_response(): + handle_message(msg) + + # Follow-up (same session, remembers previous) + await client.query("Add a main function") + async for msg in client.receive_response(): + handle_message(msg) +``` + +--- + ## Project Overview **Goal:** Build a server that orchestrates multiple async Claude agents (using Claude Agent SDK) that communicate through Slack, manage projects together, and can delegate work to each other. @@ -63,10 +192,11 @@ agent-orchestra/ ├── README.md ├── pyproject.toml ├── docker-compose.yml -├── Dockerfile.agent # Base image for agent containers +├── Dockerfile.orchestrator # Main service (runs SDK) +├── Dockerfile.workspace # Lightweight workspace containers │ ├── config/ -│ ├── orchestra.yml # Global config (Slack tokens, git repo, etc.) +│ ├── orchestra.yml # Global config (Slack, Gitea, Docker) │ └── agents/ │ ├── ceo.yml │ ├── product_manager.yml @@ -81,39 +211,37 @@ agent-orchestra/ │ │ │ ├── core/ │ │ ├── __init__.py -│ │ ├── orchestrator.py # Main orchestrator service -│ │ ├── agent.py # Agent class (wraps Claude SDK) -│ │ ├── config.py # YAML config loading/validation -│ │ └── container.py # Docker container management +│ │ ├── orchestrator.py # Main service, Slack listener +│ │ ├── agent.py # ClaudeSDKClient wrapper +│ │ ├── config.py # YAML config → Pydantic models +│ │ ├── container.py # Docker workspace management +│ │ └── permissions.py # PreToolUse hooks │ │ │ ├── slack/ │ │ ├── __init__.py │ │ ├── client.py # Slack API wrapper -│ │ ├── events.py # Event handlers +│ │ ├── events.py # Event handlers, routing │ │ └── formatter.py # Message formatting │ │ │ ├── tools/ │ │ ├── __init__.py -│ │ ├── registry.py # Tool registration & permissions -│ │ ├── filesystem.py # File operations -│ │ ├── git.py # Git operations -│ │ ├── slack.py # Slack tools (create_channel, etc.) -│ │ ├── tasks.py # Project management -│ │ ├── web_search.py # Web search -│ │ └── code_exec.py # Code execution -│ │ -│ ├── mcp/ -│ │ ├── __init__.py -│ │ ├── server.py # MCP server implementation -│ │ └── client.py # MCP client for external servers +│ │ ├── server.py # create_sdk_mcp_server setup +│ │ ├── slack_tools.py # @tool decorated Slack operations +│ │ ├── task_tools.py # @tool decorated task management +│ │ ├── gitea_tools.py # @tool decorated Gitea/PR operations +│ │ └── gitea_client.py # Gitea API client (httpx) │ │ │ └── memory/ │ ├── __init__.py -│ └── manager.py # Memory file management +│ └── manager.py # Memory file read/write │ -├── data/ # Mounted as shared volume -│ ├── repos/ # Shared git repositories -│ ├── projects/ # Project JSON files +├── data/ # Mounted volumes +│ ├── workspaces/ # Per-agent working directories +│ │ ├── developer/ +│ │ ├── product_manager/ +│ │ └── tech_lead/ +│ ├── repos/ # Shared git repository clone +│ ├── projects/ # Project/task JSON files │ │ └── boards.json │ └── memory/ # Agent memory folders │ ├── ceo/ @@ -127,6 +255,7 @@ agent-orchestra/ ├── __init__.py ├── test_agent.py ├── test_tools.py + ├── test_permissions.py └── test_slack.py ``` @@ -169,8 +298,8 @@ id: ceo name: "CEO Chris" slack_user_id: "${SLACK_CEO_USER_ID}" -model: "claude-sonnet-4-20250514" -max_tokens: 8192 +model: "sonnet" +max_turns: 100 system_prompt: | You are CEO Chris, the supervisor of the agent team. @@ -188,38 +317,51 @@ system_prompt: | You have access to all tools and can perform any operation. -tools: +# All tools allowed +allowed_tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep + - WebSearch + - WebFetch + - Task + # All Slack tools + - mcp__agent__slack_send_message + - mcp__agent__slack_reply_thread + - mcp__agent__slack_create_channel + - mcp__agent__slack_delete_channel + - mcp__agent__slack_invite_to_channel + # All task tools + - mcp__agent__tasks_create_project + - mcp__agent__tasks_create_board + - mcp__agent__tasks_create + - mcp__agent__tasks_assign + - mcp__agent__tasks_update_status + - mcp__agent__tasks_view + - mcp__agent__tasks_delete + # All git tools + - mcp__agent__gitea_create_pr + - mcp__agent__gitea_list_prs + - mcp__agent__gitea_get_pr + - mcp__agent__gitea_merge_pr + - mcp__agent__gitea_add_review + - mcp__agent__gitea_merge_to_main + +disallowed_tools: [] # CEO has no restrictions + +permissions: filesystem: - enabled: true allowed_paths: - "/**" - git: - enabled: true - allowed_operations: - - "*" # All operations - denied_operations: [] - - slack: - enabled: true - allowed_operations: + can_push_to: - "*" - - tasks: - enabled: true - allowed_operations: - - "*" - - code_exec: - enabled: true - allowed_languages: - - python - - javascript - - bash - timeout_seconds: 600 - - web_search: - enabled: true + can_merge_to: + - "main" + - "dev" memory: path: "/memory/ceo" @@ -227,10 +369,10 @@ memory: - "memory.md" can_mention: - - "*" # Can mention anyone + - "*" can_delegate_to: - - "*" # Can delegate to anyone + - "*" container: resources: @@ -245,8 +387,8 @@ id: tech_lead name: "Tech Lead Terry" slack_user_id: "${SLACK_TECHLEAD_USER_ID}" -model: "claude-sonnet-4-20250514" -max_tokens: 8192 +model: "sonnet" +max_turns: 50 system_prompt: | You are Tech Lead Terry, responsible for code quality and architecture. @@ -265,57 +407,41 @@ system_prompt: | 4. Ensure code follows project conventions 5. Provide constructive feedback -tools: +allowed_tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep + - WebSearch + - WebFetch + # Slack tools + - mcp__agent__slack_send_message + - mcp__agent__slack_reply_thread + # Task tools + - mcp__agent__tasks_view + - mcp__agent__tasks_update_status + - mcp__agent__tasks_create # Can create tech debt tasks + - mcp__agent__tasks_add_comment + # Git tools - can review but not merge + - mcp__agent__gitea_list_prs + - mcp__agent__gitea_get_pr + - mcp__agent__gitea_add_review + - mcp__agent__gitea_add_comment + +disallowed_tools: + - mcp__agent__gitea_merge_pr + - mcp__agent__slack_create_channel + +permissions: filesystem: - enabled: true allowed_paths: - "/repos/**" - "/workspace/**" - git: - enabled: true - allowed_operations: - - clone - - checkout - - pull - - status - - diff - - log - - list_prs - - get_pr - - add_pr_review - - add_pr_comment - denied_operations: - - push_to_main - - merge_pr # Only PM merges - - force_push - - slack: - enabled: true - allowed_operations: - - send_message - - reply_thread - - add_reaction - - upload_file - - tasks: - enabled: true - allowed_operations: - - view_tasks - - update_task_status - - add_comment - - create_task # Can create tech debt tasks - - code_exec: - enabled: true - allowed_languages: - - python - - javascript - - bash - timeout_seconds: 300 - - web_search: - enabled: true + can_push_to: [] # Tech lead reviews, doesn't push + can_merge_to: [] # Only PM merges memory: path: "/memory/tech_lead" @@ -335,8 +461,9 @@ id: developer name: "Dev Dana" slack_user_id: "${SLACK_DEV_USER_ID}" # Bot user for this agent -model: "claude-sonnet-4-20250514" -max_tokens: 8192 +# Claude Agent SDK settings +model: "sonnet" # sonnet, opus, haiku +max_turns: 50 system_prompt: | You are Dev Dana, a skilled software developer working in a team. @@ -350,70 +477,52 @@ system_prompt: | 4. Create a PR and notify tech_lead for review You have access to the shared repository at /repos/main. + + Use the Slack tools to communicate with your team. + Use the task tools to update your task status. -tools: +# Built-in Claude Code tools (Read, Write, Edit, Bash, Glob, Grep, WebSearch, etc.) +allowed_tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep + - WebSearch + - WebFetch + # Custom MCP tools (prefixed with mcp____) + - mcp__agent__slack_send_message + - mcp__agent__slack_reply_thread + - mcp__agent__tasks_view + - mcp__agent__tasks_update_status + - mcp__agent__gitea_create_pr + - mcp__agent__gitea_list_prs + +disallowed_tools: + - mcp__agent__slack_create_channel + - mcp__agent__slack_delete_channel + - mcp__agent__tasks_create_project + - mcp__agent__gitea_merge_pr + +# Permission hooks configuration +permissions: filesystem: - enabled: true allowed_paths: - "/repos/**" - "/workspace/**" denied_paths: - "/repos/.git/config" - "**/.env" + - "**/secrets/**" git: - enabled: true - allowed_operations: - - clone - - checkout - - branch - - add - - commit - - push - - pull - - status - - diff - - create_pr - - list_prs - denied_operations: - - push_to_main - - push_to_dev - - merge_pr - - force_push branch_pattern: "feature/*" - - slack: - enabled: true - allowed_operations: - - send_message - - reply_thread - - add_reaction - - upload_file - denied_operations: - - create_channel - - delete_channel - - tasks: - enabled: true - allowed_operations: - - view_tasks - - update_task_status - - add_comment - denied_operations: - - create_project - - create_board - - delete_task - - code_exec: - enabled: true - allowed_languages: - - python - - javascript - - bash - timeout_seconds: 300 - - web_search: - enabled: true + can_push_to: + - "feature/*" + cannot_push_to: + - "main" + - "dev" memory: path: "/memory/developer" @@ -440,8 +549,8 @@ id: product_manager name: "PM Paula" slack_user_id: "${SLACK_PM_USER_ID}" -model: "claude-sonnet-4-20250514" -max_tokens: 8192 +model: "sonnet" +max_turns: 50 system_prompt: | You are PM Paula, a product manager coordinating the team. @@ -455,55 +564,49 @@ system_prompt: | 4. Track progress and remove blockers You can merge approved PRs to the dev branch. + You cannot merge to main - that requires CEO approval. -tools: +allowed_tools: + - Read + - Write + - Glob + - Grep + - Bash + - WebSearch + - WebFetch + # Slack tools + - mcp__agent__slack_send_message + - mcp__agent__slack_reply_thread + - mcp__agent__slack_create_channel + - mcp__agent__slack_invite_to_channel + # Task tools + - mcp__agent__tasks_create_project + - mcp__agent__tasks_create_board + - mcp__agent__tasks_create + - mcp__agent__tasks_assign + - mcp__agent__tasks_update_status + - mcp__agent__tasks_view + # Git/Gitea tools + - mcp__agent__gitea_list_prs + - mcp__agent__gitea_get_pr + - mcp__agent__gitea_merge_pr + - mcp__agent__gitea_add_review + +disallowed_tools: + - mcp__agent__gitea_push_to_main + +permissions: filesystem: - enabled: true allowed_paths: - "/repos/**" - "/workspace/**" - "/projects/**" git: - enabled: true - allowed_operations: - - clone - - checkout - - pull - - merge_to_dev - - status - - log - - list_prs - - get_pr - - merge_pr - - add_pr_review - denied_operations: - - push_to_main - - force_push - - slack: - enabled: true - allowed_operations: - - send_message - - reply_thread - - create_channel - - invite_to_channel - - upload_file - - add_reaction - - tasks: - enabled: true - allowed_operations: - - create_project - - create_board - - create_task - - assign_task - - update_task_status - - view_tasks - - add_comment - - web_search: - enabled: true + can_merge_to: + - "dev" + cannot_merge_to: + - "main" memory: path: "/memory/product_manager" @@ -530,7 +633,7 @@ can_delegate_to: ```python """ Main orchestrator that: -- Loads agent configs +- Loads agent configs from YAML - Manages agent container lifecycle - Routes Slack messages to appropriate agents - Handles agent-to-agent communication @@ -546,28 +649,262 @@ class Orchestrator: ### 2. Agent (`src/orchestra/core/agent.py`) +Uses `ClaudeSDKClient` from the Claude Agent SDK for persistent conversation sessions. +Each agent maintains its own client instance for continuous context. + ```python """ Wraps Claude Agent SDK with: -- Tool permission enforcement +- Persistent conversation via ClaudeSDKClient +- Custom tools via SDK MCP servers (in-process) - Memory management - Slack integration -- Container-aware execution """ +from claude_agent_sdk import ( + ClaudeSDKClient, + ClaudeAgentOptions, + tool, + create_sdk_mcp_server, + AssistantMessage, + TextBlock, + ToolUseBlock, + HookMatcher +) class Agent: id: str config: AgentConfig - claude_agent: ClaudeAgent # From Agent SDK - container: AgentContainer + client: ClaudeSDKClient # Persistent session memory: MemoryManager - async def process_message(self, message: str, context: dict) -> str: ... - async def invoke_tool(self, tool: str, params: dict) -> Any: ... - async def update_memory(self, content: str) -> None: ... + async def start(self) -> None: + """Initialize the ClaudeSDKClient with agent-specific options.""" + # Create in-process MCP server for custom tools + tools_server = create_sdk_mcp_server( + name=f"{self.id}-tools", + version="1.0.0", + tools=self._build_tools() + ) + + options = ClaudeAgentOptions( + system_prompt=self.config.system_prompt, + model=self.config.model, # e.g., "sonnet", "opus" + allowed_tools=self.config.allowed_tools, + disallowed_tools=self.config.disallowed_tools, + mcp_servers={ + "agent": tools_server, + # External MCP servers for git, etc. + "gitea": { + "type": "stdio", + "command": "gitea-mcp-server", + "args": ["--url", self.config.gitea_url] + } + }, + permission_mode="acceptEdits", # Auto-accept in containers + cwd=f"/workspace/{self.id}", + hooks=self._build_hooks() + ) + + self.client = ClaudeSDKClient(options) + await self.client.connect() + + async def process_message(self, message: str, context: dict) -> str: + """Send message and collect response. Client maintains conversation history.""" + await self.client.query(message) + + response_text = "" + async for msg in self.client.receive_response(): + if isinstance(msg, AssistantMessage): + for block in msg.content: + if isinstance(block, TextBlock): + response_text += block.text + + return response_text + + async def stop(self) -> None: + """Disconnect the client.""" + await self.client.disconnect() + + def _build_tools(self) -> list: + """Build custom tools based on agent config permissions.""" + tools = [] + + if "slack" in self.config.tools: + tools.extend(self._create_slack_tools()) + if "tasks" in self.config.tools: + tools.extend(self._create_task_tools()) + + return tools + + def _build_hooks(self) -> dict: + """Build permission hooks to enforce tool restrictions.""" + async def check_permissions(input_data, tool_use_id, context): + tool_name = input_data.get("tool_name") + tool_input = input_data.get("tool_input", {}) + + # Check git branch restrictions + if tool_name == "Bash" and "git push" in tool_input.get("command", ""): + branch = self._extract_branch(tool_input["command"]) + if not self._can_push_to_branch(branch): + return { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": f"Cannot push to {branch}" + } + } + return {} + + return { + "PreToolUse": [HookMatcher(hooks=[check_permissions])] + } ``` -### 3. Container Manager (`src/orchestra/core/container.py`) +### 3. Custom Tools with SDK MCP Server + +The Claude Agent SDK supports in-process MCP servers, eliminating subprocess overhead. + +```python +""" +Custom tools implemented as SDK MCP servers. +These run in-process for better performance. +""" +from claude_agent_sdk import tool, create_sdk_mcp_server +from typing import Any + +# Slack Tools +@tool("slack_send_message", "Send a message to a Slack channel or thread", { + "channel": str, + "text": str, + "thread_ts": str # Optional, for threading +}) +async def slack_send_message(args: dict[str, Any]) -> dict[str, Any]: + # Implementation uses slack_sdk + channel = args["channel"] + text = args["text"] + thread_ts = args.get("thread_ts") + + result = await slack_client.chat_postMessage( + channel=channel, + text=text, + thread_ts=thread_ts + ) + + return { + "content": [{ + "type": "text", + "text": f"Message sent to {channel}" + }] + } + +@tool("slack_create_channel", "Create a new Slack channel", { + "name": str, + "description": str +}) +async def slack_create_channel(args: dict[str, Any]) -> dict[str, Any]: + result = await slack_client.conversations_create( + name=args["name"] + ) + return { + "content": [{ + "type": "text", + "text": f"Created channel #{args['name']}" + }] + } + +# Task Management Tools +@tool("tasks_create", "Create a new task", { + "title": str, + "description": str, + "assignee": str, + "board_id": str +}) +async def tasks_create(args: dict[str, Any]) -> dict[str, Any]: + task = await task_manager.create_task( + title=args["title"], + description=args["description"], + assignee=args["assignee"], + board_id=args["board_id"] + ) + return { + "content": [{ + "type": "text", + "text": f"Created task {task['id']}: {task['title']}" + }] + } + +@tool("tasks_list", "List tasks with optional filters", { + "board_id": str, + "status": str, # Optional: pending, in_progress, done + "assignee": str # Optional +}) +async def tasks_list(args: dict[str, Any]) -> dict[str, Any]: + tasks = await task_manager.list_tasks(**args) + return { + "content": [{ + "type": "text", + "text": json.dumps(tasks, indent=2) + }] + } + +# Gitea Tools (PR management) +@tool("gitea_create_pr", "Create a pull request", { + "title": str, + "body": str, + "head": str, # Source branch + "base": str # Target branch (usually "dev") +}) +async def gitea_create_pr(args: dict[str, Any]) -> dict[str, Any]: + pr = await gitea_client.create_pull_request(**args) + return { + "content": [{ + "type": "text", + "text": f"Created PR #{pr['number']}: {pr['title']}" + }] + } + +@tool("gitea_merge_pr", "Merge a pull request", { + "pr_number": int, + "merge_style": str # merge, rebase, squash +}) +async def gitea_merge_pr(args: dict[str, Any]) -> dict[str, Any]: + result = await gitea_client.merge_pull_request( + args["pr_number"], + args["merge_style"] + ) + return { + "content": [{ + "type": "text", + "text": f"Merged PR #{args['pr_number']}" + }] + } + +# Create server with all tools +def create_agent_tools_server(agent_config: AgentConfig): + """Create MCP server with tools filtered by agent permissions.""" + all_tools = { + "slack_send_message": slack_send_message, + "slack_create_channel": slack_create_channel, + "tasks_create": tasks_create, + "tasks_list": tasks_list, + "gitea_create_pr": gitea_create_pr, + "gitea_merge_pr": gitea_merge_pr, + } + + # Filter based on agent's allowed operations + allowed = [] + for tool_name, tool_fn in all_tools.items(): + if agent_config.can_use_tool(tool_name): + allowed.append(tool_fn) + + return create_sdk_mcp_server( + name=f"{agent_config.id}-tools", + version="1.0.0", + tools=allowed + ) +``` + +### 4. Container Manager (`src/orchestra/core/container.py`) ```python """ @@ -877,59 +1214,64 @@ services: - SLACK_APP_TOKEN=${SLACK_APP_TOKEN} - SLACK_BOT_TOKEN=${SLACK_BOT_TOKEN} - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + - GITEA_URL=${GITEA_URL} + - GITEA_API_TOKEN=${GITEA_API_TOKEN} volumes: - ./config:/app/config:ro - - orchestra-data:/app/data + - ./data:/app/data + - /var/run/docker.sock:/var/run/docker.sock # For container management networks: - orchestra-net depends_on: - - agent-ceo - - agent-pm - - agent-dev - - agent-techlead + - workspace-ceo + - workspace-pm + - workspace-dev + - workspace-techlead - agent-ceo: + # Lightweight workspace containers (no SDK, just filesystem) + workspace-ceo: build: context: . - dockerfile: Dockerfile.agent - environment: - - AGENT_ID=ceo - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + dockerfile: Dockerfile.workspace volumes: - - orchestra-data:/data + - ./data/workspaces/ceo:/workspace + - ./data/repos:/repos + - ./data/memory/ceo:/memory + - ./data/projects:/projects networks: - orchestra-net - agent-pm: + workspace-pm: build: context: . - dockerfile: Dockerfile.agent - environment: - - AGENT_ID=product_manager + dockerfile: Dockerfile.workspace volumes: - - orchestra-data:/data + - ./data/workspaces/product_manager:/workspace + - ./data/repos:/repos + - ./data/memory/product_manager:/memory + - ./data/projects:/projects networks: - orchestra-net - agent-dev: + workspace-dev: build: context: . - dockerfile: Dockerfile.agent - environment: - - AGENT_ID=developer + dockerfile: Dockerfile.workspace volumes: - - orchestra-data:/data + - ./data/workspaces/developer:/workspace + - ./data/repos:/repos + - ./data/memory/developer:/memory networks: - orchestra-net - agent-techlead: + workspace-techlead: build: context: . - dockerfile: Dockerfile.agent - environment: - - AGENT_ID=tech_lead + dockerfile: Dockerfile.workspace volumes: - - orchestra-data:/data + - ./data/workspaces/tech_lead:/workspace + - ./data/repos:/repos:ro # Read-only for tech lead + - ./data/memory/tech_lead:/memory networks: - orchestra-net @@ -941,6 +1283,54 @@ networks: driver: bridge ``` +### Dockerfile.orchestrator + +```dockerfile +FROM python:3.12-slim + +# Install Node.js (required for Claude Agent SDK) +RUN apt-get update && apt-get install -y \ + curl \ + git \ + && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Install Claude Code CLI +RUN npm install -g @anthropic-ai/claude-code + +# Install Python dependencies +WORKDIR /app +COPY pyproject.toml . +RUN pip install uv && uv sync + +COPY src/ ./src/ +COPY config/ ./config/ + +CMD ["uv", "run", "python", "-m", "orchestra.main"] +``` + +### Dockerfile.workspace + +```dockerfile +FROM python:3.12-slim + +# Lightweight container with common dev tools +RUN apt-get update && apt-get install -y \ + git \ + curl \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# Create workspace structure +RUN mkdir -p /workspace /repos /memory + +WORKDIR /workspace + +# Keep container running (orchestrator executes commands via docker exec) +CMD ["tail", "-f", "/dev/null"] +``` + --- ## CLAUDE.md (for Claude Code) @@ -949,34 +1339,59 @@ networks: # Agent Orchestra Multi-agent system with Claude agents communicating via Slack. +Uses the Claude Agent SDK (claude-agent-sdk) for agent execution. ## Quick Start ```bash +# Prerequisites: Node.js, Claude Code CLI +npm install -g @anthropic-ai/claude-code + +# Install Python dependencies uv sync -cp .env.example .env # Fill in tokens +cp .env.example .env # Fill in ANTHROPIC_API_KEY, Slack tokens, Gitea tokens uv run python -m orchestra.main ``` ## Architecture -- Orchestrator: Routes Slack messages to agents -- Agents: Run in Docker containers, use Claude Agent SDK -- Tools: Filesystem, git, Slack, tasks, code execution -- Memory: Markdown files in /data/memory/{agent_id}/ + +- **Orchestrator**: Routes Slack messages to agents, manages lifecycle +- **Agents**: Each agent uses ClaudeSDKClient for persistent conversations +- **Tools**: Built-in (Read/Write/Bash) + custom MCP tools (Slack/Tasks/Gitea) +- **Permissions**: PreToolUse hooks enforce agent-specific restrictions +- **Containers**: Each agent has isolated Docker container for workspace + +## Claude Agent SDK Usage + +We use `ClaudeSDKClient` (not `query()`) because agents need persistent +conversation context. Key patterns: + +```python +from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions + +async with ClaudeSDKClient(options) as client: + await client.query(message) + async for msg in client.receive_response(): + process(msg) +``` + +Custom tools use `@tool` decorator and `create_sdk_mcp_server()`. ## Key Files -- `config/orchestra.yml` - Global config -- `config/agents/*.yml` - Agent definitions -- `src/orchestra/core/orchestrator.py` - Main service -- `src/orchestra/core/agent.py` - Agent wrapper -- `src/orchestra/tools/` - Tool implementations +- `config/orchestra.yml` - Global config (Slack, Gitea, Docker) +- `config/agents/*.yml` - Agent definitions (tools, permissions, prompts) +- `src/orchestra/core/orchestrator.py` - Message routing, lifecycle +- `src/orchestra/core/agent.py` - ClaudeSDKClient wrapper +- `src/orchestra/tools/` - Custom MCP tool implementations +- `src/orchestra/tools/permissions.py` - PreToolUse hook for restrictions -## Agent Config Structure -See `config/agents/developer.yml` for full example. -Key fields: id, name, model, system_prompt, tools, memory, can_mention +## Agent Permissions -## Tool Permissions -Each tool has allowed/denied operations per agent. -Check `src/orchestra/tools/registry.py` for enforcement. +Each agent has: +- `allowed_tools` / `disallowed_tools` - Tool access +- `permissions.filesystem` - Path restrictions +- `permissions.git` - Branch push/merge restrictions + +Enforced via PreToolUse hooks that check before execution. ## Testing ```bash @@ -985,9 +1400,9 @@ uv run pytest tests/test_agent.py -v ``` ## Common Tasks -- Add new agent: Create YAML in config/agents/, add to docker-compose -- Add new tool: Implement in src/orchestra/tools/, register in registry.py -- Debug agent: Check logs with `docker logs agent-{id}` +- **Add agent**: Create YAML in config/agents/, add to docker-compose +- **Add tool**: Use @tool decorator in src/orchestra/tools/, register in server +- **Debug agent**: Logs in stdout, use `--debug` for SDK verbose output ``` --- @@ -998,10 +1413,9 @@ uv run pytest tests/test_agent.py -v [project] name = "agent-orchestra" version = "0.1.0" -requires-python = ">=3.11" +requires-python = ">=3.10" dependencies = [ - "anthropic>=0.40.0", - "anthropic-agent-sdk>=0.1.0", # Or whatever the actual package name is + "claude-agent-sdk>=0.1.0", # Official Claude Agent SDK "slack-sdk>=3.27.0", "slack-bolt>=1.18.0", "pydantic>=2.5.0", @@ -1010,8 +1424,7 @@ dependencies = [ "docker>=7.0.0", "aiohttp>=3.9.0", "httpx>=0.26.0", - "gitea-api>=0.1.0", # Gitea API client (or use httpx directly) - "mcp>=0.1.0", # MCP SDK + "anyio>=4.0.0", ] [project.optional-dependencies] @@ -1023,22 +1436,9 @@ dev = [ ] ``` ---- +**Prerequisites:** +- Python 3.10+ +- Node.js (required by Claude Agent SDK) +- Claude Code CLI 2.0.0+: `npm install -g @anthropic-ai/claude-code` -## Next Steps - -1. Initialize the repository with this structure -2. Set up Slack app (Bot Token Scopes needed): - - `app_mentions:read` - - `channels:history` - - `channels:manage` - - `channels:read` - - `chat:write` - - `files:write` - - `reactions:write` - - `users:read` -3. Create bot users for each agent in Slack -4. Implement Phase 1 (Foundation) -5. Iterate through remaining phases - -Would you like me to start implementing any specific component? +--- \ No newline at end of file