13 KiB
Below is a concrete implementation spec for the Anthropic Claude Code (“claude” CLI / Agent SDK runtime) runner shipped in Takopi (v0.3.0).
Scope
Goal
Provide the claude engine backend so Takopi can:
- Run Claude Code non-interactively via the Agent SDK CLI (
claude -p). (Claude Code) - Stream progress in Telegram by parsing
--output-format stream-json --verbose(newline-delimited JSON). Note:--output-formatonly works with-p/--print. (Claude Code) - Support resumable sessions via
--resume <session_id>(Takopi emits a canonical resume line the user can reply with). (Claude Code)
Non-goals (v1)
- Interactive Q&A inside a single run (e.g., answering
AskUserQuestionprompts mid-flight). - Full “slash commands” integration (Claude Code docs note many slash commands are interactive-only). (Claude Code)
- MCP prompt-handling for permissions (use allow rules instead).
UX and behavior
Engine selection
- Default:
takopi(auto-router usesdefault_enginefrom config) - Override:
takopi claude
Takopi runs in auto-router mode by default; takopi claude or /claude selects
Claude for new threads.
Resume UX (canonical line)
Takopi appends a single backticked resume line at the end of the message, like:
`claude --resume 8b2d2b30-...`
Rationale:
- Claude Code supports resuming a specific conversation by session ID with
--resume. (Claude Code) - The CLI reference also documents
--resume/-ras the resume mechanism.
Takopi should parse either:
claude --resume <id>claude -r <id>(short form from docs)
Note: Claude session IDs should be treated as opaque strings. Do not assume UUID format.
Permissions / non-interactive runs
In -p mode, Claude Code can require tool approvals. Takopi cannot click/answer interactive prompts, so users must preconfigure permissions (via Claude Code settings or --allowedTools). Claude’s settings system supports allow/deny tool rules. (Claude Code)
Safety note: -p/--print skips the workspace trust dialog; only use this flag in trusted directories.
Takopi should document this clearly: if permissions aren’t configured and Claude tries to use a gated tool, the run may block or fail.
Config additions
Takopi config lives at ~/.takopi/takopi.toml.
Add a new optional [claude] section.
Recommended v1 schema:
# ~/.takopi/takopi.toml
default_engine = "claude"
[claude]
model = "claude-sonnet-4-5-20250929" # optional (Claude Code supports model override in settings too)
allowed_tools = ["Bash", "Read", "Edit", "Write"] # optional but strongly recommended for automation
dangerously_skip_permissions = false # optional (high risk; prefer sandbox use only)
use_api_billing = false # optional (keep ANTHROPIC_API_KEY for API billing)
Notes:
--allowedToolsexists specifically to auto-approve tools in programmatic runs. (Claude Code)- Claude Code tools (Bash/Edit/Write/WebSearch/etc.) and whether permission is required are documented. (Claude Code)
- If
allowed_toolsis omitted, Takopi defaults to["Bash", "Read", "Edit", "Write"]. - Takopi only reads
model,allowed_tools,dangerously_skip_permissions, anduse_api_billingfrom[claude]. - By default Takopi strips
ANTHROPIC_API_KEYfrom the subprocess environment so Claude uses subscription billing. Setuse_api_billing = trueto keep the key.
Code changes (by file)
1) New file: src/takopi/runners/claude.py
Backend export
Expose a module-level BACKEND = EngineBackend(...) (from takopi.backends).
Takopi auto-discovers runners by importing takopi.runners.* and looking for
BACKEND.
BACKEND should provide:
-
Engine id:
"claude" -
install_cmd:-
Install command for
claude(used by onboarding when missing on PATH). -
Error message should include official install options and “run
claudeonce to authenticate”.- Install methods include install scripts, Homebrew, and npm. (Claude Code)
- Agent SDK / CLI can use Claude Code authentication from running
claude, or API key auth. (Claude)
-
-
build_runner()should parse[claude]config and instantiateClaudeRunner.
Runner implementation
Implement a new Runner:
Public API
engine: EngineId = "claude"format_resume(token) -> str: returns`claude --resume {token}`extract_resume(text) -> ResumeToken | None: parse last match of--resume/-ris_resume_line(line) -> bool: matches the above patternsrun(prompt, resume)async generator ofTakopiEvent
Subprocess invocation
Use Agent SDK CLI non-interactively:
Core invocation:
claude -p --output-format stream-json --verbose(Claude Code)--verboseoverrides config and is required for full stream-json output.
Resume:
- add
--resume <session_id>if resuming. (Claude Code)
Model:
- add
--model <name>if configured. (Claude Code)
Permissions:
- add
--allowedTools "<rules>"if configured. (Claude Code) - add
--dangerously-skip-permissionsonly if explicitly enabled (high risk; document clearly).
Prompt passing:
- Pass the prompt as the final positional argument after
--(CLI expectspromptas an argument). This also protects prompts that begin with-. (Claude Code)
Other flags:
- Claude exposes more CLI flags, but Takopi does not surface them in config.
Stream parsing
In stream-json mode, Claude emits newline-delimited JSON objects. (Claude Code)
Per the official Agent SDK TypeScript reference, message types include:
-
systemwithsubtype: 'init'and fields likesession_id,cwd,tools,model,permissionMode,output_style. (Claude Code) -
assistant/usermessages with Anthropic SDK message objects. (Claude Code) -
final
resultmessage with:subtype: 'success'or error subtype(s),is_error,result(string on success),usage,total_cost_usd,modelUsage,errorslist on failures,permission_denials. (Claude Code)
Takopi should:
- Parse each line as JSON; on decode error emit a warning ActionEvent (like CodexRunner does) and continue.
- Prefer stdout for JSON; log stderr separately (do not merge).
- Treat unknown top-level fields (e.g.,
parent_tool_use_id) as optional metadata and ignore them unless needed.
Mapping to Takopi events
StartedEvent
-
Emit upon first
system/initmessage:resume = ResumeToken(engine="claude", value=session_id)(treatsession_idas opaque; do not validate as UUID)title = model(or user-specified config title; default"claude")metashould includecwd,tools,permissionMode,output_stylefor debugging.
Action events (progress) The core useful progress comes from tool usage.
Claude Code tools list is documented (Bash/Edit/Write/WebSearch/WebFetch/TodoWrite/Task/etc.). (Claude Code)
Strategy:
-
When you see an assistant message with a content block
type: "tool_use":-
Emit
ActionEvent(phase="started")with:-
action.id = tool_use.id -
action.kindbased on tool name (complete mapping):Bash→commandEdit/Write/NotebookEdit→file_change(best-effort path extraction)Read→toolGlob/Grep→toolWebSearch/WebFetch→web_searchTodoWrite/TodoRead→noteAskUserQuestion→noteTask/Agent→toolKillShell→command- otherwise →
tool
-
action.title:- Bash: use
input.commandif present - Read/Write/Edit/NotebookEdit: use file path (best-effort; field may be
file_pathorpath) - Glob/Grep: use pattern
- WebSearch: use query
- WebFetch: use URL
- TodoWrite/TodoRead: short summary (e.g., “update todos”)
- AskUserQuestion: short summary (e.g., “ask user”)
- otherwise: tool name
- Bash: use
-
detailincludes a compacted copy of input (or a safe summary).
-
-
-
When you see a user message with a content block
type: "tool_result":- Emit
ActionEvent(phase="completed")fortool_use_id ok = not is_errorcontentmay be a string or an array of content blocks; normalize to a string for summariesdetailincludes a small summary (char count / first line / “(truncated)”)
- Emit
This mirrors CodexRunner’s “started → completed” item tracking and renders well in existing TakopiProgressRenderer.
CompletedEvent
-
Emit on
resultmessage:-
ok = (is_error == false)(treatis_erroras authoritative;subtypeis informational) -
answer = resulton success; on error, a concise message usingerrorsand/or denials -
usageattach:total_cost_usd,usage,modelUsage,duration_ms,duration_api_ms,num_turns(Claude Code)
-
Always include
resume(same session_id).
-
-
Emit exactly one completed event per run. After emitting it, ignore any trailing JSON lines (do not emit a second completion).
-
We do not use an idle-timeout completion; completion is driven by Claude’s
resultevent or process exit handling.
Permission denials
Because result includes permission_denials, optionally emit warning ActionEvent(s) before CompletedEvent (CompletedEvent must be final):
- kind:
warning - title: “permission denied: <tool_name>” This preserves the “warnings before started/completed” ordering principle Takopi already tests for CodexRunner.
Session serialization / locks
Must match Takopi runner contract:
-
Lock key:
claude:<session_id>(string) in aWeakValueDictionaryofanyio.Lock. -
When resuming:
- acquire lock before spawning subprocess.
-
When starting a new session:
-
you don’t know session_id until
system/init, so:- spawn process,
- wait until the first
system/init, - acquire lock for that session id before yielding StartedEvent,
- then continue yielding.
-
This mirrors CodexRunner’s correct behavior and ensures “new run + resume run” serialize once the session is known.
Assumption: Claude emits a single system/init per run. If multiple init
events arrive, ignore the subsequent ones (do not attempt to re-lock).
Cancellation / termination
Reuse the existing subprocess lifecycle pattern (like CodexRunner.manage_subprocess):
- Kill the process group on cancellation
- Drain stderr concurrently (log-only)
- Ensure locks release in
finally
Documentation updates
README
Add a “Claude Code engine” section that covers:
-
Installation (install script / brew / npm). (Claude Code)
-
Authentication:
- run
claudeonce and follow prompts, or use API key auth (Agent SDK docs mentionANTHROPIC_API_KEY). (Claude)
- run
-
Non-interactive permission caveat + how to configure:
- settings allow/deny rules,
- or
--allowedTools/[claude].allowed_tools. (Claude Code)
-
Resume format:
`claude --resume <id>`.
docs/developing.md
Extend “Adding a Runner” with:
- “ClaudeRunner parses Agent SDK stream-json output”
- Mention key message types and the init/result messages.
Test plan
Mirror the existing CodexRunner tests patterns.
New tests: tests/test_claude_runner.py
- Contract & locking
-
test_run_serializes_same_session(stubrun_impllike Codex tests) -
test_run_allows_parallel_new_sessions -
test_run_serializes_new_session_after_session_is_known:-
Provide a fake
claudeexecutable in tmp_path that:- prints system/init with session_id,
- then waits on a file gate,
- a second invocation with
--resumewrites a marker file and exits, - assert the resume invocation doesn’t run until gate opens.
-
- Resume parsing
format_resumereturnsclaude --resume <id>extract_resumehandles both--resumeand-r
- Translation / event ordering
-
Fake
claudeoutputs:- system/init
- assistant tool_use (Bash)
- user tool_result
- result success with
result: "ok"
-
Assert Takopi yields:
- StartedEvent
- ActionEvent started
- ActionEvent completed
- CompletedEvent(ok=True, answer="ok")
- Failure modes
-
resultsubtype error witherrors: [...]:- CompletedEvent(ok=False)
-
permission_denials exist:
- warning ActionEvent(s) emitted before CompletedEvent
- Cancellation
- Stub
claudethat sleeps; ensure cancellation kills it (pattern already used for codex subprocess cancellation tests).
Implementation checklist (v0.3.0)
- Export
BACKEND = EngineBackend(...)fromsrc/takopi/runners/claude.py. - Add
src/takopi/runners/claude.pyimplementing theRunnerprotocol. - Add tests + stub executable fixtures.
- Update README and developing docs.
- Run full test suite before release.
If you want, I can also propose the exact event-to-action mapping table (tool → kind/title/detail rules) you should start with, based on Claude Code’s documented tool list (Bash/Edit/Write/WebSearch/etc.). (Claude Code)