Runner
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→command Edit/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
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)”)
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:
” 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) -
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)¶
- [x] Export
BACKEND = EngineBackend(...)fromsrc/takopi/runners/claude.py. - [x] Add
src/takopi/runners/claude.pyimplementing theRunnerprotocol. - [x] Add tests + stub executable fixtures.
- [x] 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)