You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Today, tt agent-loop spawns a fresh coding-CLI process per round (e.g. cat prompt.md | claude --print). Each round pays a full cold-start cost: process startup, system-prompt re-ingestion, MCP tool re-discovery, and a cold LLM prompt cache.
Propose switching the agent loop to a long-lived CLI subprocess per agent, where the Rust worker bridges Redis ⇄ the CLI's stdin/stdout using the CLI's streaming-JSON protocol. Messages arriving in Redis get written to the CLI's stdin; assistant events and completion markers are parsed from stdout and mapped onto the existing event stream (#53).
This preserves the round / turn abstraction but eliminates cold-start-per-round latency, preserves prompt cache across turns, and lets urgent messages interrupt in near-real-time instead of waiting for the next loop cycle.
Full reprocessing of system prompt, repo context, and MCP tool list
Cold LLM KV / prompt cache
Redis inbox is read only at the top of each round — urgent messages that arrive while the CLI is running wait until the next iteration
tt interrupt / tt kill are only honored at round boundaries
Proposed Behavior
Introduce a CodingAgent abstraction with a streaming I/O lifecycle:
traitCodingAgent:Send{asyncfnsend(&mutself,msg:AgentInput) -> Result<()>;fnevents(&mutself) -> implStream<Item = AgentEvent>;asyncfnshutdown(self,mode:ShutdownMode) -> Result<ExitStatus>;}enumAgentInput{UserMessage(String),// batched inbox turnUrgentMessage(String),// injected mid-turn if supportedCancel,// interrupt current turn}enumAgentEvent{TurnStarted,AssistantDelta(String),ToolCall{name:String,args: serde_json::Value},TurnCompleted{summary:Option<String>},AwaitingInput,SessionError(String),Exited(ExitStatus),}
Two backends:
OneShotAgent — wraps the current sh -c "cat prompt | cli --print" behavior. Default for CLIs without a streaming protocol. Preserves today's semantics exactly.
StreamingAgent — keeps the CLI alive as a child process with stdin/stdout piped. Uses the CLI's JSON protocol where available:
Claude Code: --input-format stream-json --output-format stream-json
Codex: codex proto (JSON-line protocol)
Auggie: --output-format json (+ stdin continuous mode — to validate)
Aider / Gemini / Copilot / Cursor: stay on OneShotAgent for now
Parse bugs / hangs: waiting for a sentinel event can deadlock the worker → hard timeouts + protocol versioning
Backpressure: high-volume stdout must be consumed eagerly to avoid filling the pipe
rounds_completed semantics shift: today a "round" = one CLI invocation; in the new model a "round" = one assistant turn. Keep the counter and the per-turn log file ({name}_round_{N}.log) to preserve tt status --deep, docs, tests.
Tests heavily exercise the one-shot path; both backends must be covered.
Phased Plan
Phase A — Adapter abstraction (no behavior change)
Add src/agent_runtime/mod.rs with CodingAgent trait, AgentInput, AgentEvent, ShutdownMode
Implement OneShotAgent that wraps today's build_cli_command + sh -c path
Refactor Commands::AgentLoop to drive a CodingAgent instead of calling Command::status() directly
Summary
Today,
tt agent-loopspawns a fresh coding-CLI process per round (e.g.cat prompt.md | claude --print). Each round pays a full cold-start cost: process startup, system-prompt re-ingestion, MCP tool re-discovery, and a cold LLM prompt cache.Propose switching the agent loop to a long-lived CLI subprocess per agent, where the Rust worker bridges Redis ⇄ the CLI's stdin/stdout using the CLI's streaming-JSON protocol. Messages arriving in Redis get written to the CLI's stdin; assistant events and completion markers are parsed from stdout and mapped onto the existing event stream (#53).
This preserves the
round / turnabstraction but eliminates cold-start-per-round latency, preserves prompt cache across turns, and lets urgent messages interrupt in near-real-time instead of waiting for the next loop cycle.Current Behavior
src/main.rs—Commands::AgentLoop:Per round this pays:
tt interrupt/tt killare only honored at round boundariesProposed Behavior
Introduce a
CodingAgentabstraction with a streaming I/O lifecycle:Two backends:
OneShotAgent— wraps the currentsh -c "cat prompt | cli --print"behavior. Default for CLIs without a streaming protocol. Preserves today's semantics exactly.StreamingAgent— keeps the CLI alive as a child process withstdin/stdoutpiped. Uses the CLI's JSON protocol where available:--input-format stream-json --output-format stream-jsoncodex proto(JSON-line protocol)--output-format json(+ stdin continuous mode — to validate)OneShotAgentfor nowThe agent loop becomes event-driven:
Benefits
max_rounds)tt interrupt/tt killtake effect immediately, not at round boundaryDraining/Coldstates added in Adopt RAR worker lifecycle state machine for agents #54 become backed by an actual process lifecycle.logfilesCosts / Risks
--resume <session-id>, persisted on the agent hashrounds_completedsemantics shift: today a "round" = one CLI invocation; in the new model a "round" = one assistant turn. Keep the counter and the per-turn log file ({name}_round_{N}.log) to preservett status --deep, docs, tests.Phased Plan
Phase A — Adapter abstraction (no behavior change)
src/agent_runtime/mod.rswithCodingAgenttrait,AgentInput,AgentEvent,ShutdownModeOneShotAgentthat wraps today'sbuild_cli_command+sh -cpathCommands::AgentLoopto drive aCodingAgentinstead of callingCommand::status()directly[agent].persistent = false(default)Phase B — Streaming Claude adapter
StreamingAgentfor Claude Code using--input-format stream-json --output-format stream-json--resumeon worker restart[agent].persistent = trueon a per-CLI basisPhase C — Event-driven agent loop
for round in 0..max_roundsloop with atokio::select!over inbox / stdout / control / idleTurnCompletedevent"; keep therounds_completedcounter and--max-roundscap.tt/logs/{name}_round_{N}.logby capturing per-turn stdout/stderr into per-turn filesPhase D — Event stream mapping
AgentEvent::ToolCall,TurnCompleted,AssistantDelta, etc. ontoEventTypeentries on the Add Redis Stream-based event log for real-time progress #53 Redis streamPhase E — Scale-to-zero & drain alignment (#57, #54)
ShutdownMode::Graceful= close stdin, waitdrain_timeout_secs, SIGTERM, SIGKILLWorking → Draining → Stoppedvia the real process lifecyclePhase F — Additional adapters
codex protoOneShotAgentAcceptance Criteria
CodingAgenttrait +OneShotAgentbackend land with zero behavior change under default configStreamingAgentClaude backend works end-to-end (spawn → multiple turns → clean shutdown)tt interrupttakes effect within 1s instead of at round boundaryrounds_completed,--max-rounds, and.tt/logs/{name}_round_{N}.logkeep working (redefined as "turns")EventTypes--resume[agent].persistentconfig toggle per CLI; defaultfalseduring rolloutOpen Questions
--max-roundsstay, become a token/cost cap, or both?--resume? Agent hash vs. a dedicated key namespace?Commands::Conductorpath (whichexecs the CLI for interactive use) adoptCodingAgentor stay separate?input-required(A2A Add A2A (Agent-to-Agent) protocol endpoint to townhall #62) before the A2A endpoint lands — temporary REST probe or defer?Related Issues
input-required)