diff --git a/Cargo.toml b/Cargo.toml index 0fc4dc8f..d66ba9b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["server/packages/*", "gigacode"] exclude = ["factory/packages/desktop/src-tauri", "foundry/packages/desktop/src-tauri"] [workspace.package] -version = "0.4.2" +version = "0.5.0-rc.3" edition = "2021" authors = [ "Rivet Gaming, LLC " ] license = "Apache-2.0" @@ -13,13 +13,13 @@ description = "Universal API for automatic coding agents in sandboxes. Supports [workspace.dependencies] # Internal crates -sandbox-agent = { version = "0.4.2", path = "server/packages/sandbox-agent" } -sandbox-agent-error = { version = "0.4.2", path = "server/packages/error" } -sandbox-agent-agent-management = { version = "0.4.2", path = "server/packages/agent-management" } -sandbox-agent-agent-credentials = { version = "0.4.2", path = "server/packages/agent-credentials" } -sandbox-agent-opencode-adapter = { version = "0.4.2", path = "server/packages/opencode-adapter" } -sandbox-agent-opencode-server-manager = { version = "0.4.2", path = "server/packages/opencode-server-manager" } -acp-http-adapter = { version = "0.4.2", path = "server/packages/acp-http-adapter" } +sandbox-agent = { version = "0.5.0-rc.3", path = "server/packages/sandbox-agent" } +sandbox-agent-error = { version = "0.5.0-rc.3", path = "server/packages/error" } +sandbox-agent-agent-management = { version = "0.5.0-rc.3", path = "server/packages/agent-management" } +sandbox-agent-agent-credentials = { version = "0.5.0-rc.3", path = "server/packages/agent-credentials" } +sandbox-agent-opencode-adapter = { version = "0.5.0-rc.3", path = "server/packages/opencode-adapter" } +sandbox-agent-opencode-server-manager = { version = "0.5.0-rc.3", path = "server/packages/opencode-server-manager" } +acp-http-adapter = { version = "0.5.0-rc.3", path = "server/packages/acp-http-adapter" } # Serialization serde = { version = "1.0", features = ["derive"] } diff --git a/sdks/acp-http-client/package.json b/sdks/acp-http-client/package.json index 0d61dc35..1f157497 100644 --- a/sdks/acp-http-client/package.json +++ b/sdks/acp-http-client/package.json @@ -1,6 +1,6 @@ { "name": "acp-http-client", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "Protocol-faithful ACP JSON-RPC over streamable HTTP client.", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli-shared/package.json b/sdks/cli-shared/package.json index 4b9a0aef..534302d2 100644 --- a/sdks/cli-shared/package.json +++ b/sdks/cli-shared/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli-shared", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "Shared helpers for sandbox-agent CLI and SDK", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli/package.json b/sdks/cli/package.json index a7e42c1e..c46bd604 100644 --- a/sdks/cli/package.json +++ b/sdks/cli/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "CLI for sandbox-agent - run AI coding agents in sandboxes", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli/platforms/darwin-arm64/package.json b/sdks/cli/platforms/darwin-arm64/package.json index 9ed1a85e..4962bb66 100644 --- a/sdks/cli/platforms/darwin-arm64/package.json +++ b/sdks/cli/platforms/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli-darwin-arm64", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "sandbox-agent CLI binary for macOS ARM64", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli/platforms/darwin-x64/package.json b/sdks/cli/platforms/darwin-x64/package.json index 6379cdf0..2a9b79aa 100644 --- a/sdks/cli/platforms/darwin-x64/package.json +++ b/sdks/cli/platforms/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli-darwin-x64", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "sandbox-agent CLI binary for macOS x64", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli/platforms/linux-arm64/package.json b/sdks/cli/platforms/linux-arm64/package.json index bbd677a6..db0dcb91 100644 --- a/sdks/cli/platforms/linux-arm64/package.json +++ b/sdks/cli/platforms/linux-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli-linux-arm64", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "sandbox-agent CLI binary for Linux arm64", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli/platforms/linux-x64/package.json b/sdks/cli/platforms/linux-x64/package.json index 9793e983..d204f537 100644 --- a/sdks/cli/platforms/linux-x64/package.json +++ b/sdks/cli/platforms/linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli-linux-x64", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "sandbox-agent CLI binary for Linux x64", "license": "Apache-2.0", "repository": { diff --git a/sdks/cli/platforms/win32-x64/package.json b/sdks/cli/platforms/win32-x64/package.json index 0fec6cd4..48b8b458 100644 --- a/sdks/cli/platforms/win32-x64/package.json +++ b/sdks/cli/platforms/win32-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/cli-win32-x64", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "sandbox-agent CLI binary for Windows x64", "license": "Apache-2.0", "repository": { diff --git a/sdks/gigacode/package.json b/sdks/gigacode/package.json index 80ed1103..7358ce0c 100644 --- a/sdks/gigacode/package.json +++ b/sdks/gigacode/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/gigacode", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "Gigacode CLI (sandbox-agent with OpenCode attach by default)", "license": "Apache-2.0", "repository": { diff --git a/sdks/gigacode/platforms/darwin-arm64/package.json b/sdks/gigacode/platforms/darwin-arm64/package.json index 5a347ba2..6d8ade55 100644 --- a/sdks/gigacode/platforms/darwin-arm64/package.json +++ b/sdks/gigacode/platforms/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/gigacode-darwin-arm64", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "gigacode CLI binary for macOS arm64", "license": "Apache-2.0", "repository": { diff --git a/sdks/gigacode/platforms/darwin-x64/package.json b/sdks/gigacode/platforms/darwin-x64/package.json index 976bdb46..24aca9e4 100644 --- a/sdks/gigacode/platforms/darwin-x64/package.json +++ b/sdks/gigacode/platforms/darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/gigacode-darwin-x64", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "gigacode CLI binary for macOS x64", "license": "Apache-2.0", "repository": { diff --git a/sdks/gigacode/platforms/linux-arm64/package.json b/sdks/gigacode/platforms/linux-arm64/package.json index 94ee741a..c2338f78 100644 --- a/sdks/gigacode/platforms/linux-arm64/package.json +++ b/sdks/gigacode/platforms/linux-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/gigacode-linux-arm64", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "gigacode CLI binary for Linux arm64", "license": "Apache-2.0", "repository": { diff --git a/sdks/gigacode/platforms/linux-x64/package.json b/sdks/gigacode/platforms/linux-x64/package.json index e6c8f369..73a18609 100644 --- a/sdks/gigacode/platforms/linux-x64/package.json +++ b/sdks/gigacode/platforms/linux-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/gigacode-linux-x64", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "gigacode CLI binary for Linux x64", "license": "Apache-2.0", "repository": { diff --git a/sdks/gigacode/platforms/win32-x64/package.json b/sdks/gigacode/platforms/win32-x64/package.json index 4458d3b6..9fc7580b 100644 --- a/sdks/gigacode/platforms/win32-x64/package.json +++ b/sdks/gigacode/platforms/win32-x64/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/gigacode-win32-x64", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "gigacode CLI binary for Windows x64", "license": "Apache-2.0", "repository": { diff --git a/sdks/persist-indexeddb/package.json b/sdks/persist-indexeddb/package.json index 98c59c79..7f72752c 100644 --- a/sdks/persist-indexeddb/package.json +++ b/sdks/persist-indexeddb/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/persist-indexeddb", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "IndexedDB persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)", "license": "Apache-2.0", "repository": { diff --git a/sdks/persist-postgres/package.json b/sdks/persist-postgres/package.json index 3ffba1ba..58accece 100644 --- a/sdks/persist-postgres/package.json +++ b/sdks/persist-postgres/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/persist-postgres", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "PostgreSQL persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)", "license": "Apache-2.0", "repository": { diff --git a/sdks/persist-rivet/package.json b/sdks/persist-rivet/package.json index a8ea332d..82a41d3f 100644 --- a/sdks/persist-rivet/package.json +++ b/sdks/persist-rivet/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/persist-rivet", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "Rivet Actor persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)", "license": "Apache-2.0", "repository": { diff --git a/sdks/persist-sqlite/package.json b/sdks/persist-sqlite/package.json index c0a3133c..667b3119 100644 --- a/sdks/persist-sqlite/package.json +++ b/sdks/persist-sqlite/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/persist-sqlite", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "SQLite persistence driver for the Sandbox Agent TypeScript SDK (DEPRECATED)", "license": "Apache-2.0", "repository": { diff --git a/sdks/react/package.json b/sdks/react/package.json index cb4cf7b7..5206d933 100644 --- a/sdks/react/package.json +++ b/sdks/react/package.json @@ -1,6 +1,6 @@ { "name": "@sandbox-agent/react", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "React components for Sandbox Agent frontend integrations", "license": "Apache-2.0", "repository": { diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index dc22ca73..e132b03d 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -1,6 +1,6 @@ { "name": "sandbox-agent", - "version": "0.4.2", + "version": "0.5.0-rc.3", "description": "Universal API for automatic coding agents in sandboxes. Supports Claude Code, Codex, OpenCode, and Amp.", "license": "Apache-2.0", "repository": { diff --git a/sdks/typescript/src/client.ts b/sdks/typescript/src/client.ts index a52e933e..ae8d52b9 100644 --- a/sdks/typescript/src/client.ts +++ b/sdks/typescript/src/client.ts @@ -609,7 +609,9 @@ export class LiveAcpConnection { if (replayText) { // TODO: Replace this synthesized replay text with ACP-native restore once standardized. this.pendingReplayByLocalSessionId.delete(localSessionId); - injectReplayPrompt(mappedParams, replayText); + prefixPromptText(mappedParams, replayText); + } else { + prefixPromptText(mappedParams, "msg:"); } if (options.notification) { @@ -2686,12 +2688,29 @@ function mapSessionParams(params: Record, agentSessionId: strin }; } -function injectReplayPrompt(params: Record, replayText: string): void { +function prefixPromptText(params: Record, prefix: string): void { const prompt = Array.isArray(params.prompt) ? [...params.prompt] : []; - prompt.unshift({ - type: "text", - text: replayText, - }); + let prefixed = false; + + for (let i = 0; i < prompt.length; i += 1) { + const part = prompt[i]; + if (isRecord(part) && part.type === "text" && typeof part.text === "string") { + prompt[i] = { + ...part, + text: `${prefix}${part.text}`, + }; + prefixed = true; + break; + } + } + + if (!prefixed) { + prompt.unshift({ + type: "text", + text: prefix, + }); + } + params.prompt = prompt; } @@ -2700,25 +2719,16 @@ function buildReplayText(events: SessionEvent[], maxChars: number): string | nul return null; } - const prefix = "Previous session history is replayed below as JSON-RPC envelopes. Use it as context before responding to the latest user prompt.\n"; - let text = prefix; - - for (const event of events) { - const line = JSON.stringify({ - createdAt: event.createdAt, - sender: event.sender, - payload: event.payload, - }); - - if (text.length + line.length + 1 > maxChars) { - text += "\n[history truncated]"; - break; + let start = 0; + while (start < events.length) { + const json = JSON.stringify(events.slice(start)); + if (json.length <= maxChars) { + return `restore-history:${json.length}:${json}`; } - - text += `${line}\n`; + start += 1; } - return text; + return null; } function envelopeMethod(message: AnyMessage): string | null { diff --git a/sdks/typescript/tests/integration.test.ts b/sdks/typescript/tests/integration.test.ts index d5ae278a..28b17380 100644 --- a/sdks/typescript/tests/integration.test.ts +++ b/sdks/typescript/tests/integration.test.ts @@ -538,7 +538,7 @@ describe("Integration: TypeScript SDK flat session API", () => { const params = payload.params as Record | undefined; const prompt = Array.isArray(params?.prompt) ? params?.prompt : []; const firstBlock = prompt[0] as Record | undefined; - return method === "session/prompt" && typeof firstBlock?.text === "string" && firstBlock.text.includes("Previous session history is replayed below"); + return method === "session/prompt" && typeof firstBlock?.text === "string" && firstBlock.text.startsWith("restore-history:"); }); expect(replayInjected).toBeTruthy(); diff --git a/server/packages/opencode-adapter/src/lib.rs b/server/packages/opencode-adapter/src/lib.rs index b4e04d8a..b2dbd61f 100644 --- a/server/packages/opencode-adapter/src/lib.rs +++ b/server/packages/opencode-adapter/src/lib.rs @@ -472,6 +472,36 @@ impl AdapterState { Ok(()) } + async fn persist_replay_event( + &self, + session_id: &str, + created_at: i64, + connection_id: &str, + sender: &str, + payload: &Value, + ) -> Result<(), String> { + let pool = self.pool().await?; + let id = format!("evt_{}", self.next_id("")); + sqlx::query( + r#"INSERT INTO events (id, session_id, created_at, connection_id, sender, payload_json) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#, + ) + .bind(id) + .bind(session_id) + .bind(created_at) + .bind(connection_id) + .bind(sender) + .bind(serde_json::to_string(payload).map_err(|err| err.to_string())?) + .execute(pool) + .await + .map_err(|err| err.to_string())?; + + let mut projection = self.projection.lock().await; + apply_envelope(&mut projection, session_id, sender, payload); + + Ok(()) + } + async fn collect_replay_events( &self, session_id: &str, @@ -1946,7 +1976,31 @@ async fn oc_session_prompt( meta.agent = agent.clone(); } - let parts_input = body.parts.unwrap_or_default(); + let mut parts_input = body.parts.unwrap_or_default(); + let mut replay_events: Option> = None; + if let Some(first_part) = parts_input.first_mut() { + if let Some(text) = first_part + .get("text") + .and_then(Value::as_str) + .map(str::to_owned) + { + if !has_messages { + if let Some((parsed, remainder)) = parse_replay_header(&text) { + replay_events = Some(parsed); + *first_part = json!({ + "type": "text", + "text": remainder, + }); + } + } + if replay_events.is_none() && text.starts_with(MSG_PREFIX) { + *first_part = json!({ + "type": "text", + "text": text.strip_prefix(MSG_PREFIX).unwrap_or(&text), + }); + } + } + } if parts_input.is_empty() { return bad_request("parts are required"); } @@ -2007,8 +2061,46 @@ async fn oc_session_prompt( ); let user_parts = normalize_parts(&session_id, &user_message_id, &parts_input); + let mut replay_text: Option = None; + if let Some(mut replay_events) = replay_events { + if replay_events.len() > state.config.replay_max_events { + let start = replay_events.len() - state.config.replay_max_events; + replay_events = replay_events.split_off(start); + } + + for event in &replay_events { + if let Err(err) = state + .persist_replay_event( + &session_id, + event.created_at, + event + .connection_id + .as_deref() + .unwrap_or(&meta.last_connection_id), + &event.sender, + &event.payload, + ) + .await + { + return internal_error(err); + } + } + + let replay_source = replay_events + .iter() + .map(|event| { + json!({ + "createdAt": event.created_at, + "sender": event.sender, + "payload": event.payload, + }) + }) + .collect::>(); + replay_text = build_replay_text(&replay_source, state.config.replay_max_chars); + } + let replay_injected = state.pending_replay.lock().await.remove(&session_id); - let outbound_prompt_parts = if let Some(replay_text) = replay_injected { + let outbound_prompt_parts = if let Some(replay_text) = replay_text.or(replay_injected) { let mut prompt = vec![json!({"type":"text", "text": replay_text})]; prompt.extend(parts_input.clone()); prompt @@ -3604,6 +3696,68 @@ fn build_replay_text(events: &[Value], max_chars: usize) -> Option { Some(text) } +const REPLAY_PREFIX: &str = "restore-history:"; +const MSG_PREFIX: &str = "msg:"; + +struct ReplayHeaderEvent { + created_at: i64, + sender: String, + payload: Value, + connection_id: Option, +} + +fn split_utf16_prefix(text: &str, utf16_len: usize) -> Option<(&str, &str)> { + if utf16_len == 0 { + return Some(("", text)); + } + + let mut count = 0usize; + let mut idx = 0usize; + for (byte_idx, ch) in text.char_indices() { + let units = ch.len_utf16(); + if count + units > utf16_len { + return None; + } + count += units; + idx = byte_idx + ch.len_utf8(); + if count == utf16_len { + return Some((&text[..idx], &text[idx..])); + } + } + + None +} + +fn parse_replay_header(text: &str) -> Option<(Vec, &str)> { + let rest = text.strip_prefix(REPLAY_PREFIX)?; + let mut splitter = rest.splitn(2, ':'); + let len_str = splitter.next()?; + let payload = splitter.next()?; + let expected_len: usize = len_str.parse().ok()?; + let (json, remainder) = split_utf16_prefix(payload, expected_len)?; + + let value: Value = serde_json::from_str(json).ok()?; + let items = value.as_array()?; + let mut events = Vec::with_capacity(items.len()); + for item in items { + let obj = item.as_object()?; + let created_at = obj.get("createdAt")?.as_i64()?; + let sender = obj.get("sender")?.as_str()?.to_string(); + let payload = obj.get("payload")?.clone(); + let connection_id = obj + .get("connectionId") + .and_then(Value::as_str) + .map(|value| value.to_string()); + events.push(ReplayHeaderEvent { + created_at, + sender, + payload, + connection_id, + }); + } + Some((events, remainder)) +} + fn parse_last_event_id(headers: &HeaderMap) -> Option { headers .get("last-event-id")