mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 14:03:52 +00:00
6.4 KiB
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. v1 loses this metadata. Need to capture and expose process termination details.
Current v1 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/unparsednotification 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):
- Capture stderr: Buffer the agent process's stderr stream with head/tail logic (~50 lines each)
- Capture exit code: Get the process exit status
- Store in session: Record termination info in the session registry
- 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 /v1/sessions/{id}(Feature #16) — includeterminationInfoin response when session has endedsession/listACP 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/v1_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 |