- INTER_AGENT_COMMUNICATION constant → buildInterAgentCommunication(agentId) function - Manager injects actual agent ID into prompt after DB record creation - Agent ID hardcoded in cw listen/ask commands — no manifest.json indirection - cw listen now uses onPendingConversation SSE subscription instead of polling - CLI trpc-client upgraded with splitLink for subscription support - All CLI flags (--agent-id, --from, --timeout, --poll-interval) documented in prompt - conversation:created/answered added to ALL_EVENT_TYPES
9.4 KiB
Agent Module
src/agent/ — Agent lifecycle management, output parsing, multi-provider support, and account failover.
File Inventory
| File | Purpose |
|---|---|
types.ts |
Core types: AgentInfo, AgentManager interface, SpawnOptions, StreamEvent |
manager.ts |
MultiProviderAgentManager — main orchestrator class |
process-manager.ts |
AgentProcessManager — worktree creation, command building, detached spawn |
output-handler.ts |
OutputHandler — JSONL stream parsing, completion detection, proposal creation, task dedup |
file-tailer.ts |
FileTailer — watches output files, fires parser + raw content callbacks |
file-io.ts |
Input/output file I/O: frontmatter writing, signal.json reading, tiptap conversion |
markdown-to-tiptap.ts |
Markdown to Tiptap JSON conversion using MarkdownManager |
index.ts |
Public exports, ClaudeAgentManager deprecated alias |
Sub-modules
| Directory | Purpose |
|---|---|
providers/ |
Provider registry, presets (7 providers), config types |
providers/parsers/ |
Provider-specific output parsers (Claude JSONL, generic line) |
accounts/ |
Account discovery, config dir setup, credential management, usage API |
credentials/ |
AccountCredentialManager — credential injection per account |
lifecycle/ |
LifecycleController — retry policy, signal recovery, missing signal instructions |
prompts/ |
Mode-specific prompt builders (execute, discuss, plan, detail, refine) + shared inter-agent communication instructions |
Key Flows
Spawning an Agent
- tRPC procedure calls
agentManager.spawn(options) - Manager generates alias (adjective-animal), creates DB record
AgentProcessManager.createWorktree()— creates git worktree at.cw-worktrees/agent/<alias>/file-io.writeInputFiles()— writes.cw/input/with assignment files (initiative, pages, phase, task) and read-only context dirs (context/phases/,context/tasks/)- Provider config builds spawn command via
buildSpawnCommand() spawnDetached()— launches detached child process with file output redirectionFileTailerwatches output file, firesonEvent(parsed stream events) andonRawContent(raw JSONL chunks) callbacksonRawContent→ DB insert viacreateLogChunkCallback()→agent:outputevent emitted (single emission point)OutputHandler.handleStreamEvent()processes parsed events (session tracking, result capture — no event emission)- DB record updated with PID, output file path, session ID
agent:spawnedevent emitted
Completion Detection
- Polling detects process exit,
FileTailer.stop()flushes remaining output OutputHandler.handleCompletion()triggered- Primary path: Reads
.cw/output/signal.jsonfrom agent worktree - Signal contains
{ status: "done"|"questions"|"error", result?, questions?, error? } - Agent DB status updated accordingly (idle, waiting_for_input, crashed)
- For
done: proposals created from structured output;agent:stoppedemitted - For
questions: parsed and stored aspendingQuestions;agent:waitingemitted - Fallback: If signal.json missing, lifecycle controller retries with instruction injection
Account Failover
- On usage-limit error,
markAccountExhausted(id, until)called findNextAvailable(provider)returns least-recently-used non-exhausted account- Agent re-spawned with new account's credentials
agent:account_switchedevent emitted
Resume Flow
- tRPC
resumeAgentcalled withanswers: Record<string, string> - Manager looks up agent's session ID and provider config
buildResumeCommand()creates resume command with session flagformatAnswersAsPrompt(answers)converts answers to prompt text- New detached process spawned, same worktree, incremented session number
Provider Configuration
Providers defined in providers/presets.ts:
| Provider | Command | Resume | Prompt Mode |
|---|---|---|---|
| claude | claude |
--resume <id> |
native (-p) |
| claude-code | claude |
--resume <id> |
native |
| codex | codex |
none | flag (--prompt) |
| aider | aider |
none | flag (--message) |
| cline | cline |
none | flag |
| continue | continue |
none | flag |
| cursor-agent | cursor |
none | flag |
Each provider config specifies: command, args, resumeStyle, promptMode, structuredOutput, sessionId extraction, nonInteractive options.
Output Parsing
The OutputHandler processes JSONL streams from Claude CLI:
initevent → session ID extracted and persistedtext_deltaevents → no-op in handler (output streaming handled by DB log chunks)resultevent → final result with structured data captured onActiveAgent- Signal file (
signal.json) → authoritative completion status
Output event flow: FileTailer.onRawContent() → DB insertChunk() → EventBus.emit('agent:output'). This is the single emission point — no events from handleStreamEvent() or processLine().
For providers without structured output, the generic line parser accumulates raw text.
Credential Management
AccountCredentialManager in credentials/ handles OAuth token lifecycle:
read()— extractsclaudeAiOauthfrom.credentials.json. OnlyaccessTokenis required;refreshTokenandexpiresAtmay be null (setup tokens).isExpired()— returns false whenexpiresAtis null (setup tokens never "expire" from our perspective).ensureValid()— if expired andrefreshTokenexists, refreshes. If expired with norefreshToken, returns invalid with error.
Setup Tokens
Setup tokens (from claude setup-token) are long-lived OAuth access tokens with no refresh token or expiry. Register via:
cw account add --token <token> --email user@example.com
Stored as credentials: {"claudeAiOauth":{"accessToken":"<token>"}} and configJson: {"hasCompletedOnboarding":true}.
Auto-Cleanup & Commit Retries
After an agent completes (status → idle), tryAutoCleanup checks if its project worktrees have uncommitted changes:
CleanupManager.getDirtyWorktreePaths()runsgit status --porcelainin each project subdirectory (not the parentagent-workdirs/<alias>/dir)- If all clean → worktrees and logs removed immediately
- If dirty →
resumeForCommit()resumes the agent's session with a prompt listing the specific dirty subdirectories (e.g.- \my-project/``) - The agent
cds into each listed directory and commits - On next completion, cleanup runs again.
MAX_COMMIT_RETRIES(1) limits retries — after that the workdir is left in place with a warning
The retry counter is cleaned up on: successful removal, max retries exceeded, or unexpected error. It is not cleaned up when a commit retry is successfully launched (so the counter persists across the retry cycle).
Log Chunks
Agent output is persisted to agent_log_chunks table and drives all live streaming:
onRawContentcallback fires for every raw JSONL chunk fromFileTailer- DB insert →
agent:outputevent emission (single source of truth for UI) - No FK to agents — survives agent deletion
- Session tracking: spawn=1, resume=previousMax+1
- Read path (
getAgentOutputtRPC): concatenates all DB chunks (no file fallback) - Live path (
onAgentOutputsubscription): listens foragent:outputevents - Frontend: initial query loads from DB, subscription accumulates raw JSONL, both parsed via
parseAgentOutput()
Inter-Agent Communication
Agents can communicate with each other via the conversations table, coordinated through CLI commands.
Prompt Integration
buildInterAgentCommunication(agentId) function in prompts/shared.ts generates per-agent communication instructions. Called in manager.ts after agent record creation — the actual agent ID is injected directly into the prompt (no manifest.json indirection). Appended to the prompt regardless of mode. Instructions include:
- Set up a background listener via temp-file redirect:
cw listen > $CW_LISTEN_FILE & - Periodically check the temp file for incoming questions between work steps
- Answer via
cw answer, clear the file, restart the listener - Ask questions to peers via
cw ask --from <agentId> --agent-id|--task-id|--phase-id - Kill the listener and clean up the temp file before writing
signal.json
Agent Identity
manifest.json includes agentId and agentName fields. The manager passes these from the DB record after agent creation. The agent ID is also injected directly into the prompt's communication instructions.
CLI Commands
cw listen --agent-id <id>
- Subscribes to
onPendingConversationSSE subscription, prints first pending as JSON, exits with code 0 - First yields any existing pending conversations from DB, then listens for
conversation:createdevents - Output:
{ conversationId, fromAgentId, question, phaseId?, taskId? }
cw ask <question> --from <agentId> --agent-id|--task-id|--phase-id <target> [--timeout <ms>] [--poll-interval <ms>]
- Creates conversation, polls
getConversationuntil answered, prints answer text to stdout - Target resolution:
--agent-id(direct),--task-id(find agent running task),--phase-id(find agent in phase) --timeout: max wait in ms (default 0=forever),--poll-interval: polling frequency in ms (default 2000)
cw answer <answer> --conversation-id <id>
- Calls
answerConversation, prints{ conversationId, status: "answered" }