//! Claude Code runtime — launches Claude Code CLI sessions as agent backends. use std::sync::{Arc, Mutex}; use tokio::sync::broadcast; use crate::agent_log::AgentLogWriter; use crate::io::watcher::WatcherEvent; use crate::slog_warn; use super::{AgentEvent, AgentRuntime, RuntimeContext, RuntimeResult, RuntimeStatus}; /// Agent runtime that spawns the `claude` CLI in a PTY and streams JSON events. /// /// This is the default runtime (`runtime = "claude-code"` in project.toml). /// It wraps the existing PTY-based execution logic, preserving all streaming, /// token tracking, and inactivity timeout behaviour. pub struct ClaudeCodeRuntime { watcher_tx: broadcast::Sender, } impl ClaudeCodeRuntime { /// Create a new Claude Code runtime with a shared event channel. pub fn new(watcher_tx: broadcast::Sender) -> Self { Self { watcher_tx } } } impl AgentRuntime for ClaudeCodeRuntime { async fn start( &self, ctx: RuntimeContext, tx: broadcast::Sender, event_log: Arc>>, log_writer: Option>>, ) -> Result { let eager_record = ctx .model .as_ref() .map(|m| (ctx.project_root.clone(), m.as_str().to_string())); let pty_result = super::super::pty::run_agent_pty_streaming( &ctx.story_id, &ctx.agent_name, &ctx.command, &ctx.args, &ctx.prompt, &ctx.cwd, &tx, &event_log, log_writer.clone(), ctx.inactivity_timeout_secs, self.watcher_tx.clone(), ctx.session_id_to_resume.as_deref(), eager_record.clone(), ) .await; match pty_result { Ok(result) => Ok(RuntimeResult { // Abort+no-session: CLI crashed (e.g. SIGABRT) before emitting its // first "system" event. Detected by: non-zero exit AND no session. aborted_signal: !result.exit_ok && result.session_id.is_none(), exit_ok: result.exit_ok, session_id: result.session_id, token_usage: result.token_usage, rate_limit_exit: result.rate_limit_exit, rate_limit_reset_at: result.rate_limit_reset_at, }), Err(e) if ctx.session_id_to_resume.is_some() && ctx.fresh_prompt.is_some() => { // Resume failed — fall back to a fresh session without --resume. slog_warn!( "[agents] Resume failed for {}:{}, retrying without --resume: {}", ctx.story_id, ctx.agent_name, e ); let fresh = ctx.fresh_prompt.unwrap(); let fallback_result = super::super::pty::run_agent_pty_streaming( &ctx.story_id, &ctx.agent_name, &ctx.command, &ctx.args, &fresh, &ctx.cwd, &tx, &event_log, log_writer, ctx.inactivity_timeout_secs, self.watcher_tx.clone(), None, // no --resume on fallback eager_record, ) .await?; Ok(RuntimeResult { aborted_signal: !fallback_result.exit_ok && fallback_result.session_id.is_none(), exit_ok: fallback_result.exit_ok, session_id: fallback_result.session_id, token_usage: fallback_result.token_usage, rate_limit_exit: fallback_result.rate_limit_exit, rate_limit_reset_at: fallback_result.rate_limit_reset_at, }) } Err(e) => Err(e), } } fn stop(&self) { // Stopping is handled externally by the pool via kill_child_for_key(). } fn get_status(&self) -> RuntimeStatus { // Lifecycle status is tracked by the pool; the runtime itself is stateless. RuntimeStatus::Idle } }