sandbox-agent/research/acp/missing-features-spec/17-error-termination-metadata.md
Nathan Flurry 2ba630c180 acp spec
2026-02-11 06:43:36 -08:00

6.4 KiB

Feature 17: Error Termination Metadata

Implementation approach: Enrich ACP notifications and session info

Summary

v1 captured exit_code, structured StderrOutput (head/tail/truncated) when a session ended due to error. v2 loses this metadata. Need to capture and expose process termination details.

Current v2 State

  • Agent process lifecycle is managed in acp_runtime/mod.rs
  • Process exit is detected but error metadata (exit code, stderr) is not captured or forwarded
  • The _sandboxagent/agent/unparsed notification exists for parse errors, but not for process crashes
  • No structured error termination data is emitted to clients

v1 Reference (source commit)

Port behavior and payload shape from commit 8ecd27bc24e62505d7aa4c50cbdd1c9dbb09f836.

v1 Types (exact, from universal-agent-schema/src/lib.rs)

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct SessionEndedData {
    pub reason: SessionEndReason,
    pub terminated_by: TerminatedBy,
    /// Error message when reason is Error
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    /// Process exit code when reason is Error
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub exit_code: Option<i32>,
    /// Agent stderr output when reason is Error
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stderr: Option<StderrOutput>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct StderrOutput {
    /// First N lines of stderr (if truncated) or full stderr (if not truncated)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub head: Option<String>,
    /// Last N lines of stderr (only present if truncated)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub tail: Option<String>,
    /// Whether the output was truncated
    pub truncated: bool,
    /// Total number of lines in stderr
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub total_lines: Option<usize>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum SessionEndReason {
    Completed,
    Error,
    Terminated,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum TerminatedBy {
    Agent,
    Daemon,
}

v1 Implementation (exact)

mark_session_ended (SessionManager)

async fn mark_session_ended(
    &self,
    session_id: &str,
    exit_code: Option<i32>,
    message: &str,
    reason: SessionEndReason,
    terminated_by: TerminatedBy,
    stderr: Option<StderrOutput>,
) {
    let mut sessions = self.sessions.lock().await;
    if let Some(session) = Self::session_mut(&mut sessions, session_id) {
        if session.ended { return; }
        session.mark_ended(exit_code, message.to_string(), reason.clone(), terminated_by.clone());
        let (error_message, error_exit_code, error_stderr) =
            if reason == SessionEndReason::Error {
                (Some(message.to_string()), exit_code, stderr)
            } else {
                (None, None, None)
            };
        let ended = EventConversion::new(
            UniversalEventType::SessionEnded,
            UniversalEventData::SessionEnded(SessionEndedData {
                reason, terminated_by,
                message: error_message,
                exit_code: error_exit_code,
                stderr: error_stderr,
            }),
        ).synthetic().with_native_session(session.native_session_id.clone());
        session.record_conversions(vec![ended]);
    }
}

Stderr capture on error exit

// Called from consume_spawn when agent process exits with error:
Ok(Ok(status)) => {
    let message = format!("agent exited with status {:?}", status);
    if !terminate_early {
        self.record_error(&session_id, message.clone(),
            Some("process_exit".to_string()), None).await;
    }
    let logs = self.read_agent_stderr(agent);
    self.mark_session_ended(
        &session_id, status.code(), &message,
        SessionEndReason::Error, TerminatedBy::Agent, logs,
    ).await;
}

Stderr reading

fn read_agent_stderr(&self, agent: AgentId) -> Option<StderrOutput> {
    let logs = AgentServerLogs::new(self.server_manager.log_base_dir.clone(), agent.as_str());
    logs.read_stderr()
}

Implementation Plan

Stderr Capture in ACP Runtime

When an agent process exits (especially abnormally):

  1. Capture stderr: Buffer the agent process's stderr stream with head/tail logic (~50 lines each)
  2. Capture exit code: Get the process exit status
  3. Store in session: Record termination info in the session registry
  4. Emit notification: Send error notification to all connected clients

ACP Notification Shape

When an agent process terminates with an error:

{
  "jsonrpc": "2.0",
  "method": "_sandboxagent/session/ended",
  "params": {
    "session_id": "session-uuid",
    "data": {
      "reason": "error",
      "terminated_by": "agent",
      "message": "agent exited with status ExitStatus(unix_wait_status(256))",
      "exit_code": 1,
      "stderr": {
        "head": "Error: module not found\n  at ...",
        "tail": "  at process.exit\nnode exited",
        "truncated": true,
        "total_lines": 250
      }
    }
  }
}

Session Info Integration

Termination metadata should be accessible via:

  • GET /v2/sessions/{id} (Feature #16) — include terminationInfo in response when session has ended
  • session/list ACP response — include termination status in session entries

Files to Modify

File Change
server/packages/sandbox-agent/src/acp_runtime/mod.rs Add stderr capture (head/tail buffer) on agent process; capture exit code; emit _sandboxagent/session/ended; store v1-shaped termination info in MetaSession
server/packages/sandbox-agent/src/acp_runtime/mock.rs Add mock error termination scenario (e.g., when prompt contains "crash")
sdks/typescript/src/client.ts Add TerminationInfo type; expose on session events and session info
server/packages/sandbox-agent/tests/v2_api.rs Add error termination metadata test

Docs to Update

Doc Change
docs/sdks/typescript.mdx Document TerminationInfo type and how to handle error termination
research/acp/spec.md Document _sandboxagent/session/ended extension and payload