Files
Codewalkers/docs/agent.md
Lukas May a6371e156a feat: Add inter-agent conversation system (listen, ask, answer)
Enables parallel agents to communicate through a CLI-based conversation
mechanism coordinated via tRPC. Agents can ask questions to peers and
receive answers, with target resolution by agent ID, task ID, or phase ID.
2026-02-10 13:43:30 +01:00

7.5 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

  1. tRPC procedure calls agentManager.spawn(options)
  2. Manager generates alias (adjective-animal), creates DB record
  3. AgentProcessManager.createWorktree() — creates git worktree at .cw-worktrees/agent/<alias>/
  4. file-io.writeInputFiles() — writes .cw/input/ with assignment files (initiative, pages, phase, task) and read-only context dirs (context/phases/, context/tasks/)
  5. Provider config builds spawn command via buildSpawnCommand()
  6. spawnDetached() — launches detached child process with file output redirection
  7. FileTailer watches output file, fires onEvent (parsed stream events) and onRawContent (raw JSONL chunks) callbacks
  8. onRawContent → DB insert via createLogChunkCallback()agent:output event emitted (single emission point)
  9. OutputHandler.handleStreamEvent() processes parsed events (session tracking, result capture — no event emission)
  10. DB record updated with PID, output file path, session ID
  11. agent:spawned event emitted

Completion Detection

  1. Polling detects process exit, FileTailer.stop() flushes remaining output
  2. OutputHandler.handleCompletion() triggered
  3. Primary path: Reads .cw/output/signal.json from agent worktree
  4. Signal contains { status: "done"|"questions"|"error", result?, questions?, error? }
  5. Agent DB status updated accordingly (idle, waiting_for_input, crashed)
  6. For done: proposals created from structured output; agent:stopped emitted
  7. For questions: parsed and stored as pendingQuestions; agent:waiting emitted
  8. Fallback: If signal.json missing, lifecycle controller retries with instruction injection

Account Failover

  1. On usage-limit error, markAccountExhausted(id, until) called
  2. findNextAvailable(provider) returns least-recently-used non-exhausted account
  3. Agent re-spawned with new account's credentials
  4. agent:account_switched event emitted

Resume Flow

  1. tRPC resumeAgent called with answers: Record<string, string>
  2. Manager looks up agent's session ID and provider config
  3. buildResumeCommand() creates resume command with session flag
  4. formatAnswersAsPrompt(answers) converts answers to prompt text
  5. 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:

  • init event → session ID extracted and persisted
  • text_delta events → no-op in handler (output streaming handled by DB log chunks)
  • result event → final result with structured data captured on ActiveAgent
  • 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() — extracts claudeAiOauth from .credentials.json. Only accessToken is required; refreshToken and expiresAt may be null (setup tokens).
  • isExpired() — returns false when expiresAt is null (setup tokens never "expire" from our perspective).
  • ensureValid() — if expired and refreshToken exists, refreshes. If expired with no refreshToken, 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}.

Log Chunks

Agent output is persisted to agent_log_chunks table and drives all live streaming:

  • onRawContent callback fires for every raw JSONL chunk from FileTailer
  • DB insert → agent:output event emission (single source of truth for UI)
  • No FK to agents — survives agent deletion
  • Session tracking: spawn=1, resume=previousMax+1
  • Read path (getAgentOutput tRPC): concatenates all DB chunks (no file fallback)
  • Live path (onAgentOutput subscription): listens for agent:output events
  • 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

INTER_AGENT_COMMUNICATION constant in prompts/shared.ts is appended to all 5 agent mode prompts. It instructs agents to:

  1. Start cw listen --agent-id <ID> & as a background process at session start
  2. Handle incoming questions by answering via cw answer and restarting the listener
  3. Ask questions to peers via cw ask --from <ID> --agent-id|--phase-id|--task-id
  4. Kill the listener before writing signal.json

Agent Identity

manifest.json now includes agentId and agentName fields. The manager passes these from the DB record after agent creation.

CLI Commands

  • cw listen: Polls getPendingConversations, prints first pending as JSON, exits
  • cw ask: Creates conversation, polls getConversation until answered, prints answer
  • cw answer: Calls answerConversation, prints confirmation JSON