mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 09:01:17 +00:00
191 lines
6.4 KiB
Markdown
191 lines
6.4 KiB
Markdown
# 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/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`)
|
|
|
|
```rust
|
|
#[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)
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
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:
|
|
|
|
```json
|
|
{
|
|
"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) — 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/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 |
|