Stabilize SDK mode integration test

This commit is contained in:
Nathan Flurry 2026-03-10 22:37:27 -07:00
parent 24e99ac5e7
commit ec8b6afea9
274 changed files with 5412 additions and 7893 deletions

View file

@ -195,9 +195,7 @@ export class AcpHttpClient {
return wrapRpc(this.connection.setSessionMode(request));
}
async setSessionConfigOption(
request: SetSessionConfigOptionRequest,
): Promise<SetSessionConfigOptionResponse> {
async setSessionConfigOption(request: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse> {
return wrapRpc(this.connection.setSessionConfigOption(request));
}
@ -213,9 +211,7 @@ export class AcpHttpClient {
return wrapRpc(this.connection.unstable_resumeSession(request));
}
async unstableSetSessionModel(
request: SetSessionModelRequest,
): Promise<SetSessionModelResponse | void> {
async unstableSetSessionModel(request: SetSessionModelRequest): Promise<SetSessionModelResponse | void> {
return wrapRpc(this.connection.unstable_setSessionModel(request));
}
@ -281,9 +277,7 @@ class StreamableHttpAcpTransport {
this.token = options.token;
this.defaultHeaders = options.defaultHeaders;
this.onEnvelope = options.onEnvelope;
this.bootstrapQuery = options.transport?.bootstrapQuery
? buildQueryParams(options.transport.bootstrapQuery)
: null;
this.bootstrapQuery = options.transport?.bootstrapQuery ? buildQueryParams(options.transport.bootstrapQuery) : null;
this.stream = {
readable: new ReadableStream<AnyMessage>({

View file

@ -14,10 +14,7 @@ function findBinary(): string | null {
return process.env.SANDBOX_AGENT_BIN;
}
const cargoPaths = [
resolve(__dirname, "../../../target/debug/sandbox-agent"),
resolve(__dirname, "../../../target/release/sandbox-agent"),
];
const cargoPaths = [resolve(__dirname, "../../../target/debug/sandbox-agent"), resolve(__dirname, "../../../target/release/sandbox-agent")];
for (const p of cargoPaths) {
if (existsSync(p)) {
@ -30,9 +27,7 @@ function findBinary(): string | null {
const BINARY_PATH = findBinary();
if (!BINARY_PATH) {
throw new Error(
"sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.",
);
throw new Error("sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.");
}
if (!process.env.SANDBOX_AGENT_BIN) {
process.env.SANDBOX_AGENT_BIN = BINARY_PATH;
@ -42,11 +37,7 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitFor<T>(
fn: () => T | undefined | null,
timeoutMs = 5000,
stepMs = 25,
): Promise<T> {
async function waitFor<T>(fn: () => T | undefined | null, timeoutMs = 5000, stepMs = 25): Promise<T> {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
const value = fn();

View file

@ -1,34 +1,34 @@
export type InstallCommandBlock = {
label: string;
commands: string[];
label: string;
commands: string[];
};
export type NonExecutableBinaryMessageOptions = {
binPath: string;
trustPackages: string;
bunInstallBlocks: InstallCommandBlock[];
genericInstallCommands?: string[];
binaryName?: string;
binPath: string;
trustPackages: string;
bunInstallBlocks: InstallCommandBlock[];
genericInstallCommands?: string[];
binaryName?: string;
};
export type FsSubset = {
accessSync: (path: string, mode?: number) => void;
chmodSync: (path: string, mode: number) => void;
constants: { X_OK: number };
accessSync: (path: string, mode?: number) => void;
chmodSync: (path: string, mode: number) => void;
constants: { X_OK: number };
};
export function isBunRuntime(): boolean {
if (typeof process?.versions?.bun === "string") return true;
const userAgent = process?.env?.npm_config_user_agent || "";
return userAgent.includes("bun/");
if (typeof process?.versions?.bun === "string") return true;
const userAgent = process?.env?.npm_config_user_agent || "";
return userAgent.includes("bun/");
}
const PERMISSION_ERRORS = new Set(["EACCES", "EPERM", "ENOEXEC"]);
function isPermissionError(error: unknown): boolean {
if (!error || typeof error !== "object") return false;
const code = (error as { code?: unknown }).code;
return typeof code === "string" && PERMISSION_ERRORS.has(code);
if (!error || typeof error !== "object") return false;
const code = (error as { code?: unknown }).code;
return typeof code === "string" && PERMISSION_ERRORS.has(code);
}
/**
@ -39,68 +39,56 @@ function isPermissionError(error: unknown): boolean {
* Requires fs to be passed in to avoid static imports that break browser builds.
*/
export function assertExecutable(binPath: string, fs: FsSubset): boolean {
if (process.platform === "win32") {
return true;
}
if (process.platform === "win32") {
return true;
}
try {
fs.accessSync(binPath, fs.constants.X_OK);
return true;
} catch {
// Not executable, try to fix
}
try {
fs.accessSync(binPath, fs.constants.X_OK);
return true;
} catch {
// Not executable, try to fix
}
try {
fs.chmodSync(binPath, 0o755);
return true;
} catch (error) {
if (isPermissionError(error)) {
return false;
}
throw error;
}
try {
fs.chmodSync(binPath, 0o755);
return true;
} catch (error) {
if (isPermissionError(error)) {
return false;
}
throw error;
}
}
export function formatNonExecutableBinaryMessage(
options: NonExecutableBinaryMessageOptions,
): string {
const {
binPath,
trustPackages,
bunInstallBlocks,
genericInstallCommands,
binaryName,
} = options;
export function formatNonExecutableBinaryMessage(options: NonExecutableBinaryMessageOptions): string {
const { binPath, trustPackages, bunInstallBlocks, genericInstallCommands, binaryName } = options;
const label = binaryName ?? "sandbox-agent";
const lines = [`${label} binary is not executable: ${binPath}`];
const label = binaryName ?? "sandbox-agent";
const lines = [`${label} binary is not executable: ${binPath}`];
if (isBunRuntime()) {
lines.push(
"Allow Bun to run postinstall scripts for native binaries and reinstall:",
);
for (const block of bunInstallBlocks) {
lines.push(`${block.label}:`);
for (const command of block.commands) {
lines.push(` ${command}`);
}
}
lines.push(`Or run: chmod +x "${binPath}"`);
return lines.join("\n");
}
if (isBunRuntime()) {
lines.push("Allow Bun to run postinstall scripts for native binaries and reinstall:");
for (const block of bunInstallBlocks) {
lines.push(`${block.label}:`);
for (const command of block.commands) {
lines.push(` ${command}`);
}
}
lines.push(`Or run: chmod +x "${binPath}"`);
return lines.join("\n");
}
lines.push(
"Postinstall scripts for native packages did not run, so the binary was left non-executable.",
);
if (genericInstallCommands && genericInstallCommands.length > 0) {
lines.push("Reinstall with scripts enabled:");
for (const command of genericInstallCommands) {
lines.push(` ${command}`);
}
} else {
lines.push("Reinstall with scripts enabled for:");
lines.push(` ${trustPackages}`);
}
lines.push(`Or run: chmod +x "${binPath}"`);
return lines.join("\n");
lines.push("Postinstall scripts for native packages did not run, so the binary was left non-executable.");
if (genericInstallCommands && genericInstallCommands.length > 0) {
lines.push("Reinstall with scripts enabled:");
for (const command of genericInstallCommands) {
lines.push(` ${command}`);
}
} else {
lines.push("Reinstall with scripts enabled for:");
lines.push(` ${trustPackages}`);
}
lines.push(`Or run: chmod +x "${binPath}"`);
return lines.join("\n");
}

View file

@ -14,10 +14,7 @@ function findBinary(): string | null {
}
// Check cargo build output
const cargoPaths = [
resolve(__dirname, "../../../target/debug/sandbox-agent"),
resolve(__dirname, "../../../target/release/sandbox-agent"),
];
const cargoPaths = [resolve(__dirname, "../../../target/debug/sandbox-agent"), resolve(__dirname, "../../../target/release/sandbox-agent")];
for (const p of cargoPaths) {
if (existsSync(p)) {

View file

@ -1,11 +1,4 @@
import type {
ListEventsRequest,
ListPage,
ListPageRequest,
SessionEvent,
SessionPersistDriver,
SessionRecord,
} from "sandbox-agent";
import type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionPersistDriver, SessionRecord } from "sandbox-agent";
const DEFAULT_DB_NAME = "sandbox-agent-session-store";
const DEFAULT_DB_VERSION = 2;
@ -40,9 +33,7 @@ export class IndexedDbSessionPersistDriver implements SessionPersistDriver {
async getSession(id: string): Promise<SessionRecord | null> {
const db = await this.dbPromise;
const row = await requestToPromise<IDBValidKey | SessionRow | undefined>(
db.transaction(SESSIONS_STORE, "readonly").objectStore(SESSIONS_STORE).get(id),
);
const row = await requestToPromise<IDBValidKey | SessionRow | undefined>(db.transaction(SESSIONS_STORE, "readonly").objectStore(SESSIONS_STORE).get(id));
if (!row || typeof row !== "object") {
return null;
}
@ -80,9 +71,7 @@ export class IndexedDbSessionPersistDriver implements SessionPersistDriver {
async listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>> {
const db = await this.dbPromise;
const rows = (await getAllRows<EventRow>(db, EVENTS_STORE))
.filter((row) => row.sessionId === request.sessionId)
.sort(compareEventRowsByOrder);
const rows = (await getAllRows<EventRow>(db, EVENTS_STORE)).filter((row) => row.sessionId === request.sessionId).sort(compareEventRowsByOrder);
const offset = parseCursor(request.cursor);
const limit = normalizeLimit(request.limit);
@ -264,12 +253,7 @@ function requestToPromise<T>(request: IDBRequest<T>): Promise<T> {
});
}
function transactionPromise<T>(
db: IDBDatabase,
stores: string[],
mode: IDBTransactionMode,
run: (tx: IDBTransaction) => T | Promise<T>,
): Promise<T> {
function transactionPromise<T>(db: IDBDatabase, stores: string[], mode: IDBTransactionMode, run: (tx: IDBTransaction) => T | Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
const tx = db.transaction(stores, mode);
let settled = false;

View file

@ -16,10 +16,7 @@ function findBinary(): string | null {
return process.env.SANDBOX_AGENT_BIN;
}
const cargoPaths = [
resolve(__dirname, "../../../target/debug/sandbox-agent"),
resolve(__dirname, "../../../target/release/sandbox-agent"),
];
const cargoPaths = [resolve(__dirname, "../../../target/debug/sandbox-agent"), resolve(__dirname, "../../../target/release/sandbox-agent")];
for (const p of cargoPaths) {
if (existsSync(p)) {
@ -36,9 +33,7 @@ function uniqueDbName(prefix: string): string {
const BINARY_PATH = findBinary();
if (!BINARY_PATH) {
throw new Error(
"sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.",
);
throw new Error("sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.");
}
if (!process.env.SANDBOX_AGENT_BIN) {
process.env.SANDBOX_AGENT_BIN = BINARY_PATH;
@ -123,11 +118,7 @@ describe("IndexedDB persistence end-to-end", () => {
const params = payload.params as Record<string, unknown> | undefined;
const prompt = Array.isArray(params?.prompt) ? params?.prompt : [];
const firstBlock = prompt[0] as Record<string, unknown> | 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.includes("Previous session history is replayed below");
});
expect(replayInjected).toBeTruthy();

View file

@ -4,6 +4,6 @@ export default defineConfig({
test: {
include: ["tests/**/*.test.ts"],
testTimeout: 60000,
environment: "node"
environment: "node",
},
});

View file

@ -1,12 +1,5 @@
import { Pool, type PoolConfig } from "pg";
import type {
ListEventsRequest,
ListPage,
ListPageRequest,
SessionEvent,
SessionPersistDriver,
SessionRecord,
} from "sandbox-agent";
import type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionPersistDriver, SessionRecord } from "sandbox-agent";
const DEFAULT_LIST_LIMIT = 100;
@ -122,10 +115,9 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
[request.sessionId, limit, offset],
);
const countResult = await this.pool.query<{ count: string }>(
`SELECT COUNT(*) AS count FROM ${this.table("events")} WHERE session_id = $1`,
[request.sessionId],
);
const countResult = await this.pool.query<{ count: string }>(`SELECT COUNT(*) AS count FROM ${this.table("events")} WHERE session_id = $1`, [
request.sessionId,
]);
const total = parseInteger(countResult.rows[0]?.count ?? "0");
const nextOffset = offset + rowsResult.rows.length;
@ -149,15 +141,7 @@ export class PostgresSessionPersistDriver implements SessionPersistDriver {
connection_id = EXCLUDED.connection_id,
sender = EXCLUDED.sender,
payload_json = EXCLUDED.payload_json`,
[
event.id,
event.eventIndex,
event.sessionId,
event.createdAt,
event.connectionId,
event.sender,
event.payload,
],
[event.id, event.eventIndex, event.sessionId, event.createdAt, event.connectionId, event.sender, event.payload],
);
}

View file

@ -18,10 +18,7 @@ function findBinary(): string | null {
return process.env.SANDBOX_AGENT_BIN;
}
const cargoPaths = [
resolve(__dirname, "../../../target/debug/sandbox-agent"),
resolve(__dirname, "../../../target/release/sandbox-agent"),
];
const cargoPaths = [resolve(__dirname, "../../../target/debug/sandbox-agent"), resolve(__dirname, "../../../target/release/sandbox-agent")];
for (const p of cargoPaths) {
if (existsSync(p)) {
@ -34,9 +31,7 @@ function findBinary(): string | null {
const BINARY_PATH = findBinary();
if (!BINARY_PATH) {
throw new Error(
"sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.",
);
throw new Error("sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.");
}
if (!process.env.SANDBOX_AGENT_BIN) {
process.env.SANDBOX_AGENT_BIN = BINARY_PATH;
@ -149,11 +144,7 @@ describe("Postgres persistence driver", () => {
const params = payload.params as Record<string, unknown> | undefined;
const prompt = Array.isArray(params?.prompt) ? params?.prompt : [];
const firstBlock = prompt[0] as Record<string, unknown> | 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.includes("Previous session history is replayed below");
});
expect(replayInjected).toBeTruthy();

View file

@ -1,11 +1,4 @@
import type {
ListEventsRequest,
ListPage,
ListPageRequest,
SessionEvent,
SessionPersistDriver,
SessionRecord,
} from "sandbox-agent";
import type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionPersistDriver, SessionRecord } from "sandbox-agent";
/** Structural type compatible with rivetkit's ActorContext without importing it. */
export interface ActorContextLike {
@ -44,10 +37,7 @@ export class RivetSessionPersistDriver implements SessionPersistDriver {
constructor(ctx: ActorContextLike, options: RivetSessionPersistDriverOptions = {}) {
this.ctx = ctx;
this.maxSessions = normalizeCap(options.maxSessions, DEFAULT_MAX_SESSIONS);
this.maxEventsPerSession = normalizeCap(
options.maxEventsPerSession,
DEFAULT_MAX_EVENTS_PER_SESSION,
);
this.maxEventsPerSession = normalizeCap(options.maxEventsPerSession, DEFAULT_MAX_EVENTS_PER_SESSION);
this.stateKey = options.stateKey ?? DEFAULT_STATE_KEY;
// Auto-initialize if absent; preserve existing data on actor wake.
@ -137,9 +127,7 @@ export class RivetSessionPersistDriver implements SessionPersistDriver {
function cloneSessionRecord(session: SessionRecord): SessionRecord {
return {
...session,
sessionInit: session.sessionInit
? (JSON.parse(JSON.stringify(session.sessionInit)) as SessionRecord["sessionInit"])
: undefined,
sessionInit: session.sessionInit ? (JSON.parse(JSON.stringify(session.sessionInit)) as SessionRecord["sessionInit"]) : undefined,
};
}

View file

@ -1,12 +1,5 @@
import Database from "better-sqlite3";
import type {
ListEventsRequest,
ListPage,
ListPageRequest,
SessionEvent,
SessionPersistDriver,
SessionRecord,
} from "sandbox-agent";
import type { ListEventsRequest, ListPage, ListPageRequest, SessionEvent, SessionPersistDriver, SessionRecord } from "sandbox-agent";
const DEFAULT_LIST_LIMIT = 100;
@ -98,9 +91,7 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
)
.all(request.sessionId, limit, offset) as EventRow[];
const countRow = this.db
.prepare(`SELECT COUNT(*) as count FROM events WHERE session_id = ?`)
.get(request.sessionId) as { count: number };
const countRow = this.db.prepare(`SELECT COUNT(*) as count FROM events WHERE session_id = ?`).get(request.sessionId) as { count: number };
const nextOffset = offset + rows.length;
@ -124,15 +115,7 @@ export class SQLiteSessionPersistDriver implements SessionPersistDriver {
sender = excluded.sender,
payload_json = excluded.payload_json`,
)
.run(
event.id,
event.eventIndex,
event.sessionId,
event.createdAt,
event.connectionId,
event.sender,
JSON.stringify(event.payload),
);
.run(event.id, event.eventIndex, event.sessionId, event.createdAt, event.connectionId, event.sender, JSON.stringify(event.payload));
}
close(): void {
@ -266,9 +249,7 @@ function decodeSessionRow(row: SessionRow): SessionRecord {
lastConnectionId: row.last_connection_id,
createdAt: row.created_at,
destroyedAt: row.destroyed_at ?? undefined,
sessionInit: row.session_init_json
? (JSON.parse(row.session_init_json) as SessionRecord["sessionInit"])
: undefined,
sessionInit: row.session_init_json ? (JSON.parse(row.session_init_json) as SessionRecord["sessionInit"]) : undefined,
};
}

View file

@ -15,10 +15,7 @@ function findBinary(): string | null {
return process.env.SANDBOX_AGENT_BIN;
}
const cargoPaths = [
resolve(__dirname, "../../../target/debug/sandbox-agent"),
resolve(__dirname, "../../../target/release/sandbox-agent"),
];
const cargoPaths = [resolve(__dirname, "../../../target/debug/sandbox-agent"), resolve(__dirname, "../../../target/release/sandbox-agent")];
for (const p of cargoPaths) {
if (existsSync(p)) {
@ -31,9 +28,7 @@ function findBinary(): string | null {
const BINARY_PATH = findBinary();
if (!BINARY_PATH) {
throw new Error(
"sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.",
);
throw new Error("sandbox-agent binary not found. Build it (cargo build -p sandbox-agent) or set SANDBOX_AGENT_BIN.");
}
if (!process.env.SANDBOX_AGENT_BIN) {
process.env.SANDBOX_AGENT_BIN = BINARY_PATH;
@ -125,11 +120,7 @@ describe("SQLite persistence driver", () => {
const params = payload.params as Record<string, unknown> | undefined;
const prompt = Array.isArray(params?.prompt) ? params?.prompt : [];
const firstBlock = prompt[0] as Record<string, unknown> | 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.includes("Previous session history is replayed below");
});
expect(replayInjected).toBeTruthy();

View file

@ -90,16 +90,7 @@ const getStatusColor = (state: ConnectionState): string => {
}
};
export const ProcessTerminal = ({
client,
processId,
className,
style,
terminalStyle,
height = 360,
onExit,
onError,
}: ProcessTerminalProps) => {
export const ProcessTerminal = ({ client, processId, className, style, terminalStyle, height = 360, onExit, onError }: ProcessTerminalProps) => {
const hostRef = useRef<HTMLDivElement | null>(null);
const [connectionState, setConnectionState] = useState<ConnectionState>("connecting");
const [statusMessage, setStatusMessage] = useState("Connecting to PTY...");
@ -198,9 +189,7 @@ export const ProcessTerminal = ({
setConnectionState("closed");
setExitCode(frame.exitCode ?? null);
setStatusMessage(
frame.exitCode == null ? "Process exited." : `Process exited with code ${frame.exitCode}.`
);
setStatusMessage(frame.exitCode == null ? "Process exited." : `Process exited with code ${frame.exitCode}.`);
onExit?.(frame);
});

View file

@ -5,9 +5,9 @@ const target = resolve(process.cwd(), "src/generated/openapi.ts");
let source = readFileSync(target, "utf8");
const replacements = [
["components[\"schemas\"][\"McpCommand\"]", "string"],
["components[\"schemas\"][\"McpOAuthConfigOrDisabled\"]", "Record<string, unknown> | null"],
["components[\"schemas\"][\"McpRemoteTransport\"]", "string"],
['components["schemas"]["McpCommand"]', "string"],
['components["schemas"]["McpOAuthConfigOrDisabled"]', "Record<string, unknown> | null"],
['components["schemas"]["McpRemoteTransport"]', "string"],
];
for (const [from, to] of replacements) {

View file

@ -3,7 +3,6 @@
* Do not make direct changes to the file.
*/
export interface paths {
"/v1/acp": {
get: operations["get_v1_acp_servers"];
@ -235,7 +234,23 @@ export interface components {
agents: components["schemas"]["AgentInfo"][];
};
/** @enum {string} */
ErrorType: "invalid_request" | "conflict" | "unsupported_agent" | "agent_not_installed" | "install_failed" | "agent_process_exited" | "token_invalid" | "permission_denied" | "not_acceptable" | "unsupported_media_type" | "not_found" | "session_not_found" | "session_already_exists" | "mode_not_supported" | "stream_error" | "timeout";
ErrorType:
| "invalid_request"
| "conflict"
| "unsupported_agent"
| "agent_not_installed"
| "install_failed"
| "agent_process_exited"
| "token_invalid"
| "permission_denied"
| "not_acceptable"
| "unsupported_media_type"
| "not_found"
| "session_not_found"
| "session_already_exists"
| "mode_not_supported"
| "stream_error"
| "timeout";
FsActionResponse: {
path: string;
};
@ -294,35 +309,37 @@ export interface components {
directory: string;
mcpName: string;
};
McpServerConfig: ({
args?: string[];
command: string;
cwd?: string | null;
enabled?: boolean | null;
env?: {
[key: string]: string;
} | null;
/** Format: int64 */
timeoutMs?: number | null;
/** @enum {string} */
type: "local";
}) | ({
bearerTokenEnvVar?: string | null;
enabled?: boolean | null;
envHeaders?: {
[key: string]: string;
} | null;
headers?: {
[key: string]: string;
} | null;
oauth?: Record<string, unknown> | null | null;
/** Format: int64 */
timeoutMs?: number | null;
transport?: string | null;
/** @enum {string} */
type: "remote";
url: string;
});
McpServerConfig:
| {
args?: string[];
command: string;
cwd?: string | null;
enabled?: boolean | null;
env?: {
[key: string]: string;
} | null;
/** Format: int64 */
timeoutMs?: number | null;
/** @enum {string} */
type: "local";
}
| {
bearerTokenEnvVar?: string | null;
enabled?: boolean | null;
envHeaders?: {
[key: string]: string;
} | null;
headers?: {
[key: string]: string;
} | null;
oauth?: Record<string, unknown> | null | null;
/** Format: int64 */
timeoutMs?: number | null;
transport?: string | null;
/** @enum {string} */
type: "remote";
url: string;
};
ProblemDetails: {
detail?: string | null;
instance?: string | null;
@ -476,7 +493,6 @@ export type $defs = Record<string, never>;
export type external = Record<string, never>;
export interface operations {
get_v1_acp_servers: {
responses: {
/** @description Active ACP server instances */

View file

@ -37,9 +37,7 @@ export type {
export type { InspectorUrlOptions } from "./inspector.ts";
export {
InMemorySessionPersistDriver,
} from "./types.ts";
export { InMemorySessionPersistDriver } from "./types.ts";
export type {
AcpEnvelope,

View file

@ -1,9 +1,6 @@
import type { ChildProcess } from "node:child_process";
import type { AddressInfo } from "node:net";
import {
assertExecutable,
formatNonExecutableBinaryMessage,
} from "@sandbox-agent/cli-shared";
import { assertExecutable, formatNonExecutableBinaryMessage } from "@sandbox-agent/cli-shared";
export type SandboxAgentSpawnLogMode = "inherit" | "pipe" | "silent";
@ -40,17 +37,12 @@ export function isNodeRuntime(): boolean {
return typeof process !== "undefined" && !!process.versions?.node;
}
export async function spawnSandboxAgent(
options: SandboxAgentSpawnOptions,
fetcher?: typeof fetch,
): Promise<SandboxAgentSpawnHandle> {
export async function spawnSandboxAgent(options: SandboxAgentSpawnOptions, fetcher?: typeof fetch): Promise<SandboxAgentSpawnHandle> {
if (!isNodeRuntime()) {
throw new Error("Autospawn requires a Node.js runtime.");
}
const {
spawn,
} = await import("node:child_process");
const { spawn } = await import("node:child_process");
const crypto = await import("node:crypto");
const fs = await import("node:fs");
const path = await import("node:path");
@ -82,17 +74,11 @@ export async function spawnSandboxAgent(
bunInstallBlocks: [
{
label: "Project install",
commands: [
`bun pm trust ${TRUST_PACKAGES}`,
"bun add sandbox-agent",
],
commands: [`bun pm trust ${TRUST_PACKAGES}`, "bun add sandbox-agent"],
},
{
label: "Global install",
commands: [
`bun pm -g trust ${TRUST_PACKAGES}`,
"bun add -g sandbox-agent",
],
commands: [`bun pm -g trust ${TRUST_PACKAGES}`, "bun add -g sandbox-agent"],
},
],
}),
@ -189,13 +175,7 @@ async function getFreePort(net: typeof import("node:net"), host: string): Promis
});
}
async function waitForHealth(
baseUrl: string,
fetcher: typeof fetch | undefined,
timeoutMs: number,
child: ChildProcess,
token: string,
): Promise<void> {
async function waitForHealth(baseUrl: string, fetcher: typeof fetch | undefined, timeoutMs: number, child: ChildProcess, token: string): Promise<void> {
if (!fetcher) {
throw new Error("Fetch API is not available; provide a fetch implementation.");
}

View file

@ -1,9 +1,4 @@
import type {
AnyMessage,
NewSessionRequest,
SessionConfigOption,
SessionModeState,
} from "acp-http-client";
import type { AnyMessage, NewSessionRequest, SessionConfigOption, SessionModeState } from "acp-http-client";
import type { components, operations } from "./generated/openapi.ts";
export type ProblemDetails = components["schemas"]["ProblemDetails"];
@ -84,10 +79,7 @@ export interface ProcessTerminalErrorFrame {
message: string;
}
export type ProcessTerminalServerFrame =
| ProcessTerminalReadyFrame
| ProcessTerminalExitFrame
| ProcessTerminalErrorFrame;
export type ProcessTerminalServerFrame = ProcessTerminalReadyFrame | ProcessTerminalExitFrame | ProcessTerminalErrorFrame;
export type TerminalReadyStatus = ProcessTerminalReadyFrame;
export type TerminalExitStatus = ProcessTerminalExitFrame;
@ -163,10 +155,7 @@ export class InMemorySessionPersistDriver implements SessionPersistDriver {
constructor(options: InMemorySessionPersistDriverOptions = {}) {
this.maxSessions = normalizeCap(options.maxSessions, DEFAULT_MAX_SESSIONS);
this.maxEventsPerSession = normalizeCap(
options.maxEventsPerSession,
DEFAULT_MAX_EVENTS_PER_SESSION,
);
this.maxEventsPerSession = normalizeCap(options.maxEventsPerSession, DEFAULT_MAX_EVENTS_PER_SESSION);
}
async getSession(id: string): Promise<SessionRecord | null> {
@ -245,15 +234,9 @@ export class InMemorySessionPersistDriver implements SessionPersistDriver {
function cloneSessionRecord(session: SessionRecord): SessionRecord {
return {
...session,
sessionInit: session.sessionInit
? (JSON.parse(JSON.stringify(session.sessionInit)) as SessionRecord["sessionInit"])
: undefined,
configOptions: session.configOptions
? (JSON.parse(JSON.stringify(session.configOptions)) as SessionRecord["configOptions"])
: undefined,
modes: session.modes
? (JSON.parse(JSON.stringify(session.modes)) as SessionRecord["modes"])
: session.modes,
sessionInit: session.sessionInit ? (JSON.parse(JSON.stringify(session.sessionInit)) as SessionRecord["sessionInit"]) : undefined,
configOptions: session.configOptions ? (JSON.parse(JSON.stringify(session.configOptions)) as SessionRecord["configOptions"]) : undefined,
modes: session.modes ? (JSON.parse(JSON.stringify(session.modes)) as SessionRecord["modes"]) : session.modes,
};
}
@ -277,11 +260,7 @@ type JsonRequestBody<T> = T extends {
? B
: never;
type QueryParams<T> = T extends { parameters: { query: infer Q } }
? Q
: T extends { parameters: { query?: infer Q } }
? Q
: never;
type QueryParams<T> = T extends { parameters: { query: infer Q } } ? Q : T extends { parameters: { query?: infer Q } } ? Q : never;
function normalizeCap(value: number | undefined, fallback: number): number {
if (!Number.isFinite(value) || (value ?? 0) < 1) {

View file

@ -25,7 +25,7 @@ export function prepareMockAgentDataHome(dataHome: string): Record<string, strin
runtimeEnv.XDG_DATA_HOME = dataHome;
}
const nodeScript = String.raw`#!/usr/bin/env node
const nodeScript = String.raw`#!/usr/bin/env node
const { createInterface } = require("node:readline");
let nextSession = 0;
@ -225,13 +225,9 @@ rl.on("line", (line) => {
const processDir = join(installDir, "agent_processes");
mkdirSync(processDir, { recursive: true });
const runner = process.platform === "win32"
? join(processDir, "mock-acp.cmd")
: join(processDir, "mock-acp");
const runner = process.platform === "win32" ? join(processDir, "mock-acp.cmd") : join(processDir, "mock-acp");
const scriptFile = process.platform === "win32"
? join(processDir, "mock-acp.js")
: runner;
const scriptFile = process.platform === "win32" ? join(processDir, "mock-acp.js") : runner;
writeFileSync(scriptFile, nodeScript);

View file

@ -538,9 +538,17 @@ describe("Integration: TypeScript SDK flat session API", () => {
const session = await sdk.createSession({ agent: "mock" });
await session.setMode("plan");
const modes = await session.getModes();
expect(modes?.currentModeId).toBe("plan");
expect((await session.getConfigOptions()).find((o) => o.category === "mode")?.currentValue).toBe("plan");
const modes = await waitForAsync(async () => {
const current = await session.getModes();
return current?.currentModeId === "plan" ? current : null;
});
expect(modes.currentModeId).toBe("plan");
const modeOption = await waitForAsync(async () => {
const option = (await session.getConfigOptions()).find((o) => o.category === "mode");
return option?.currentValue === "plan" ? option : null;
});
expect(modeOption.currentValue).toBe("plan");
await sdk.dispose();
});