mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 05:02:11 +00:00
Merge remote-tracking branch 'origin/main' into foundry-terminal-pane
# Conflicts: # factory/packages/backend/src/driver.ts # factory/packages/backend/src/integrations/sandbox-agent/client.ts # factory/packages/backend/test/helpers/test-driver.ts # factory/packages/frontend/src/components/mock-layout.tsx # pnpm-lock.yaml # sdks/react/src/ProcessTerminal.tsx
This commit is contained in:
commit
b00c0109d0
288 changed files with 7048 additions and 9134 deletions
|
|
@ -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>({
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ export default defineConfig({
|
|||
test: {
|
||||
include: ["tests/**/*.test.ts"],
|
||||
testTimeout: 60000,
|
||||
environment: "node"
|
||||
environment: "node",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -33,10 +33,7 @@ const DEFAULT_CLASS_NAMES: AgentConversationClassNames = {
|
|||
|
||||
const cx = (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(" ");
|
||||
|
||||
const mergeClassNames = (
|
||||
defaults: AgentConversationClassNames,
|
||||
overrides?: Partial<AgentConversationClassNames>,
|
||||
): AgentConversationClassNames => ({
|
||||
const mergeClassNames = (defaults: AgentConversationClassNames, overrides?: Partial<AgentConversationClassNames>): AgentConversationClassNames => ({
|
||||
root: cx(defaults.root, overrides?.root),
|
||||
transcript: cx(defaults.transcript, overrides?.transcript),
|
||||
emptyState: cx(defaults.emptyState, overrides?.emptyState),
|
||||
|
|
@ -56,8 +53,7 @@ export const AgentConversation = ({
|
|||
composerProps,
|
||||
}: AgentConversationProps) => {
|
||||
const resolvedClassNames = mergeClassNames(DEFAULT_CLASS_NAMES, classNameOverrides);
|
||||
const hasTranscriptContent =
|
||||
entries.length > 0 || Boolean(transcriptProps?.sessionError) || Boolean(transcriptProps?.eventError);
|
||||
const hasTranscriptContent = entries.length > 0 || Boolean(transcriptProps?.sessionError) || Boolean(transcriptProps?.eventError);
|
||||
|
||||
return (
|
||||
<div className={cx(resolvedClassNames.root, className)} data-slot="root">
|
||||
|
|
@ -74,11 +70,7 @@ export const AgentConversation = ({
|
|||
</div>
|
||||
) : null}
|
||||
{composerProps ? (
|
||||
<ChatComposer
|
||||
className={cx(resolvedClassNames.composer, composerClassName)}
|
||||
classNames={composerClassNames}
|
||||
{...composerProps}
|
||||
/>
|
||||
<ChatComposer className={cx(resolvedClassNames.composer, composerClassName)} classNames={composerClassNames} {...composerProps} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -177,10 +177,7 @@ const DEFAULT_DIVIDER_TITLES = new Set(["Session Started", "Turn Started", "Turn
|
|||
|
||||
const cx = (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(" ");
|
||||
|
||||
const mergeClassNames = (
|
||||
defaults: AgentTranscriptClassNames,
|
||||
overrides?: Partial<AgentTranscriptClassNames>,
|
||||
): AgentTranscriptClassNames => ({
|
||||
const mergeClassNames = (defaults: AgentTranscriptClassNames, overrides?: Partial<AgentTranscriptClassNames>): AgentTranscriptClassNames => ({
|
||||
root: cx(defaults.root, overrides?.root),
|
||||
divider: cx(defaults.divider, overrides?.divider),
|
||||
dividerLine: cx(defaults.dividerLine, overrides?.dividerLine),
|
||||
|
|
@ -240,10 +237,7 @@ const getMessageVariant = (entry: TranscriptEntry) => {
|
|||
|
||||
const getToolItemLabel = (entry: TranscriptEntry) => {
|
||||
if (entry.kind === "tool") {
|
||||
const statusLabel =
|
||||
entry.toolStatus && entry.toolStatus !== "completed"
|
||||
? ` (${entry.toolStatus.replaceAll("_", " ")})`
|
||||
: "";
|
||||
const statusLabel = entry.toolStatus && entry.toolStatus !== "completed" ? ` (${entry.toolStatus.replaceAll("_", " ")})` : "";
|
||||
return `${entry.toolName ?? "tool"}${statusLabel}`;
|
||||
}
|
||||
|
||||
|
|
@ -287,18 +281,12 @@ const defaultRenderPendingIndicator = () => "...";
|
|||
const defaultRenderChevron = (expanded: boolean) => (expanded ? "▾" : "▸");
|
||||
const defaultRenderEventLinkContent = () => "Open";
|
||||
const defaultRenderPermissionIcon = () => "Permission";
|
||||
const defaultRenderPermissionOptionContent = ({
|
||||
label,
|
||||
}: PermissionOptionRenderContext) => label;
|
||||
const defaultIsDividerEntry = (entry: TranscriptEntry) =>
|
||||
entry.kind === "meta" && DEFAULT_DIVIDER_TITLES.has(entry.meta?.title ?? "");
|
||||
const defaultRenderPermissionOptionContent = ({ label }: PermissionOptionRenderContext) => label;
|
||||
const defaultIsDividerEntry = (entry: TranscriptEntry) => entry.kind === "meta" && DEFAULT_DIVIDER_TITLES.has(entry.meta?.title ?? "");
|
||||
|
||||
const defaultCanOpenEvent = (entry: TranscriptEntry) => Boolean(entry.eventId);
|
||||
|
||||
const buildGroupedEntries = (
|
||||
entries: TranscriptEntry[],
|
||||
isDividerEntry: (entry: TranscriptEntry) => boolean,
|
||||
): GroupedEntries[] => {
|
||||
const buildGroupedEntries = (entries: TranscriptEntry[], isDividerEntry: (entry: TranscriptEntry) => boolean): GroupedEntries[] => {
|
||||
const groupedEntries: GroupedEntries[] = [];
|
||||
let currentToolGroup: TranscriptEntry[] = [];
|
||||
|
||||
|
|
@ -524,11 +512,7 @@ const ToolGroup = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(classNames.toolGroupContainer, hasFailed && "failed")}
|
||||
data-slot="tool-group"
|
||||
data-failed={hasFailed ? "true" : undefined}
|
||||
>
|
||||
<div className={cx(classNames.toolGroupContainer, hasFailed && "failed")} data-slot="tool-group" data-failed={hasFailed ? "true" : undefined}>
|
||||
<button
|
||||
type="button"
|
||||
className={cx(classNames.toolGroupHeader, expanded && "expanded")}
|
||||
|
|
@ -591,11 +575,7 @@ const PermissionPrompt = ({
|
|||
const canReply = Boolean(onPermissionReply) && !resolved;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(classNames.permissionPrompt, resolved && "resolved")}
|
||||
data-slot="permission-prompt"
|
||||
data-resolved={resolved ? "true" : undefined}
|
||||
>
|
||||
<div className={cx(classNames.permissionPrompt, resolved && "resolved")} data-slot="permission-prompt" data-resolved={resolved ? "true" : undefined}>
|
||||
<div className={classNames.permissionHeader} data-slot="permission-header">
|
||||
<span className={classNames.permissionIcon} data-slot="permission-icon">
|
||||
{renderPermissionIcon(entry)}
|
||||
|
|
@ -675,14 +655,8 @@ export const AgentTranscript = ({
|
|||
renderPermissionIcon = defaultRenderPermissionIcon,
|
||||
renderPermissionOptionContent = defaultRenderPermissionOptionContent,
|
||||
}: AgentTranscriptProps) => {
|
||||
const resolvedClassNames = useMemo(
|
||||
() => mergeClassNames(DEFAULT_CLASS_NAMES, classNameOverrides),
|
||||
[classNameOverrides],
|
||||
);
|
||||
const groupedEntries = useMemo(
|
||||
() => buildGroupedEntries(entries, isDividerEntry),
|
||||
[entries, isDividerEntry],
|
||||
);
|
||||
const resolvedClassNames = useMemo(() => mergeClassNames(DEFAULT_CLASS_NAMES, classNameOverrides), [classNameOverrides]);
|
||||
const groupedEntries = useMemo(() => buildGroupedEntries(entries, isDividerEntry), [entries, isDividerEntry]);
|
||||
|
||||
return (
|
||||
<div className={cx(resolvedClassNames.root, className)} data-slot="root">
|
||||
|
|
@ -771,13 +745,13 @@ export const AgentTranscript = ({
|
|||
</div>
|
||||
) : null}
|
||||
{isThinking
|
||||
? renderThinkingState?.({ agentId }) ?? (
|
||||
? (renderThinkingState?.({ agentId }) ?? (
|
||||
<div className={resolvedClassNames.thinkingRow} data-slot="thinking-row">
|
||||
<span className={resolvedClassNames.thinkingIndicator} data-slot="thinking-indicator">
|
||||
Thinking...
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
))
|
||||
: null}
|
||||
<div ref={endRef} className={resolvedClassNames.endAnchor} data-slot="end-anchor" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,11 +24,9 @@ export interface ChatComposerProps {
|
|||
classNames?: Partial<ChatComposerClassNames>;
|
||||
inputRef?: Ref<HTMLTextAreaElement>;
|
||||
rows?: number;
|
||||
textareaProps?: Omit<
|
||||
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
"className" | "disabled" | "onChange" | "onKeyDown" | "placeholder" | "rows" | "value"
|
||||
>;
|
||||
textareaProps?: Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "className" | "disabled" | "onChange" | "onKeyDown" | "placeholder" | "rows" | "value">;
|
||||
renderSubmitContent?: () => ReactNode;
|
||||
renderFooter?: () => ReactNode;
|
||||
}
|
||||
|
||||
const DEFAULT_CLASS_NAMES: ChatComposerClassNames = {
|
||||
|
|
@ -41,10 +39,7 @@ const DEFAULT_CLASS_NAMES: ChatComposerClassNames = {
|
|||
|
||||
const cx = (...values: Array<string | false | null | undefined>) => values.filter(Boolean).join(" ");
|
||||
|
||||
const mergeClassNames = (
|
||||
defaults: ChatComposerClassNames,
|
||||
overrides?: Partial<ChatComposerClassNames>,
|
||||
): ChatComposerClassNames => ({
|
||||
const mergeClassNames = (defaults: ChatComposerClassNames, overrides?: Partial<ChatComposerClassNames>): ChatComposerClassNames => ({
|
||||
root: cx(defaults.root, overrides?.root),
|
||||
form: cx(defaults.form, overrides?.form),
|
||||
input: cx(defaults.input, overrides?.input),
|
||||
|
|
@ -68,6 +63,7 @@ export const ChatComposer = ({
|
|||
rows = 1,
|
||||
textareaProps,
|
||||
renderSubmitContent,
|
||||
renderFooter,
|
||||
}: ChatComposerProps) => {
|
||||
const resolvedClassNames = mergeClassNames(DEFAULT_CLASS_NAMES, classNameOverrides);
|
||||
const isSubmitDisabled = disabled || submitDisabled || (!allowEmptySubmit && message.trim().length === 0);
|
||||
|
|
@ -98,6 +94,7 @@ export const ChatComposer = ({
|
|||
rows={rows}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{renderFooter?.()}
|
||||
<button
|
||||
type="submit"
|
||||
className={resolvedClassNames.submit}
|
||||
|
|
|
|||
|
|
@ -207,9 +207,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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -83,8 +83,7 @@ const DEFAULT_REPLAY_MAX_EVENTS = 50;
|
|||
const DEFAULT_REPLAY_MAX_CHARS = 12_000;
|
||||
const EVENT_INDEX_SCAN_EVENTS_LIMIT = 500;
|
||||
const SESSION_CANCEL_METHOD = "session/cancel";
|
||||
const MANUAL_CANCEL_ERROR =
|
||||
"Manual session/cancel calls are not allowed. Use destroySession(sessionId) instead.";
|
||||
const MANUAL_CANCEL_ERROR = "Manual session/cancel calls are not allowed. Use destroySession(sessionId) instead.";
|
||||
const HEALTH_WAIT_MIN_DELAY_MS = 500;
|
||||
const HEALTH_WAIT_MAX_DELAY_MS = 15_000;
|
||||
const HEALTH_WAIT_LOG_AFTER_MS = 5_000;
|
||||
|
|
@ -209,9 +208,7 @@ export class UnsupportedSessionCategoryError extends Error {
|
|||
readonly availableCategories: string[];
|
||||
|
||||
constructor(sessionId: string, category: string, availableCategories: string[]) {
|
||||
super(
|
||||
`Session '${sessionId}' does not support category '${category}'. Available categories: ${availableCategories.join(", ") || "(none)"}`,
|
||||
);
|
||||
super(`Session '${sessionId}' does not support category '${category}'. Available categories: ${availableCategories.join(", ") || "(none)"}`);
|
||||
this.name = "UnsupportedSessionCategoryError";
|
||||
this.sessionId = sessionId;
|
||||
this.category = category;
|
||||
|
|
@ -226,13 +223,7 @@ export class UnsupportedSessionValueError extends Error {
|
|||
readonly requestedValue: string;
|
||||
readonly allowedValues: string[];
|
||||
|
||||
constructor(
|
||||
sessionId: string,
|
||||
category: string,
|
||||
configId: string,
|
||||
requestedValue: string,
|
||||
allowedValues: string[],
|
||||
) {
|
||||
constructor(sessionId: string, category: string, configId: string, requestedValue: string, allowedValues: string[]) {
|
||||
super(
|
||||
`Session '${sessionId}' does not support value '${requestedValue}' for category '${category}' (configId='${configId}'). Allowed values: ${allowedValues.join(", ") || "(none)"}`,
|
||||
);
|
||||
|
|
@ -251,9 +242,7 @@ export class UnsupportedSessionConfigOptionError extends Error {
|
|||
readonly availableConfigIds: string[];
|
||||
|
||||
constructor(sessionId: string, configId: string, availableConfigIds: string[]) {
|
||||
super(
|
||||
`Session '${sessionId}' does not expose config option '${configId}'. Available configIds: ${availableConfigIds.join(", ") || "(none)"}`,
|
||||
);
|
||||
super(`Session '${sessionId}' does not expose config option '${configId}'. Available configIds: ${availableConfigIds.join(", ") || "(none)"}`);
|
||||
this.name = "UnsupportedSessionConfigOptionError";
|
||||
this.sessionId = sessionId;
|
||||
this.configId = configId;
|
||||
|
|
@ -267,9 +256,7 @@ export class UnsupportedPermissionReplyError extends Error {
|
|||
readonly availableReplies: PermissionReply[];
|
||||
|
||||
constructor(permissionId: string, requestedReply: PermissionReply, availableReplies: PermissionReply[]) {
|
||||
super(
|
||||
`Permission '${permissionId}' does not support reply '${requestedReply}'. Available replies: ${availableReplies.join(", ") || "(none)"}`,
|
||||
);
|
||||
super(`Permission '${permissionId}' does not support reply '${requestedReply}'. Available replies: ${availableReplies.join(", ") || "(none)"}`);
|
||||
this.name = "UnsupportedPermissionReplyError";
|
||||
this.permissionId = permissionId;
|
||||
this.requestedReply = requestedReply;
|
||||
|
|
@ -417,12 +404,7 @@ export class LiveAcpConnection {
|
|||
agent: string,
|
||||
connectionId: string,
|
||||
acp: AcpHttpClient,
|
||||
onObservedEnvelope: (
|
||||
connection: LiveAcpConnection,
|
||||
envelope: AnyMessage,
|
||||
direction: AcpEnvelopeDirection,
|
||||
localSessionId: string | null,
|
||||
) => void,
|
||||
onObservedEnvelope: (connection: LiveAcpConnection, envelope: AnyMessage, direction: AcpEnvelopeDirection, localSessionId: string | null) => void,
|
||||
onPermissionRequest: (
|
||||
connection: LiveAcpConnection,
|
||||
localSessionId: string,
|
||||
|
|
@ -444,12 +426,7 @@ export class LiveAcpConnection {
|
|||
headers?: HeadersInit;
|
||||
agent: string;
|
||||
serverId: string;
|
||||
onObservedEnvelope: (
|
||||
connection: LiveAcpConnection,
|
||||
envelope: AnyMessage,
|
||||
direction: AcpEnvelopeDirection,
|
||||
localSessionId: string | null,
|
||||
) => void;
|
||||
onObservedEnvelope: (connection: LiveAcpConnection, envelope: AnyMessage, direction: AcpEnvelopeDirection, localSessionId: string | null) => void;
|
||||
onPermissionRequest: (
|
||||
connection: LiveAcpConnection,
|
||||
localSessionId: string,
|
||||
|
|
@ -492,13 +469,7 @@ export class LiveAcpConnection {
|
|||
},
|
||||
});
|
||||
|
||||
live = new LiveAcpConnection(
|
||||
options.agent,
|
||||
connectionId,
|
||||
acp,
|
||||
options.onObservedEnvelope,
|
||||
options.onPermissionRequest,
|
||||
);
|
||||
live = new LiveAcpConnection(options.agent, connectionId, acp, options.onObservedEnvelope, options.onPermissionRequest);
|
||||
|
||||
const initResult = await acp.initialize({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
|
|
@ -541,10 +512,7 @@ export class LiveAcpConnection {
|
|||
this.pendingReplayByLocalSessionId.set(localSessionId, replayText);
|
||||
}
|
||||
|
||||
async createRemoteSession(
|
||||
localSessionId: string,
|
||||
sessionInit: Omit<NewSessionRequest, "_meta">,
|
||||
): Promise<NewSessionResponse> {
|
||||
async createRemoteSession(localSessionId: string, sessionInit: Omit<NewSessionRequest, "_meta">): Promise<NewSessionResponse> {
|
||||
const createStartedAt = Date.now();
|
||||
this.pendingNewSessionLocals.push(localSessionId);
|
||||
|
||||
|
|
@ -566,12 +534,7 @@ export class LiveAcpConnection {
|
|||
}
|
||||
}
|
||||
|
||||
async sendSessionMethod(
|
||||
localSessionId: string,
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
options: SessionSendOptions,
|
||||
): Promise<unknown> {
|
||||
async sendSessionMethod(localSessionId: string, method: string, params: Record<string, unknown>, options: SessionSendOptions): Promise<unknown> {
|
||||
const agentSessionId = this.sessionByLocalId.get(localSessionId);
|
||||
if (!agentSessionId) {
|
||||
throw new Error(`session '${localSessionId}' is not bound to live ACP connection '${this.connectionId}'`);
|
||||
|
|
@ -632,21 +595,14 @@ export class LiveAcpConnection {
|
|||
this.lastAdapterExitAt = Date.now();
|
||||
}
|
||||
|
||||
private async handlePermissionRequest(
|
||||
request: RequestPermissionRequest,
|
||||
): Promise<RequestPermissionResponse> {
|
||||
private async handlePermissionRequest(request: RequestPermissionRequest): Promise<RequestPermissionResponse> {
|
||||
const agentSessionId = request.sessionId;
|
||||
const localSessionId = this.localByAgentSessionId.get(agentSessionId);
|
||||
if (!localSessionId) {
|
||||
return cancelledPermissionResponse();
|
||||
}
|
||||
|
||||
return this.onPermissionRequest(
|
||||
this,
|
||||
localSessionId,
|
||||
agentSessionId,
|
||||
clonePermissionRequest(request),
|
||||
);
|
||||
return this.onPermissionRequest(this, localSessionId, agentSessionId, clonePermissionRequest(request));
|
||||
}
|
||||
|
||||
private resolveSessionId(envelope: AnyMessage, direction: AcpEnvelopeDirection): string | null {
|
||||
|
|
@ -1108,10 +1064,7 @@ export class SandboxAgent {
|
|||
return this.upsertSessionHandle(updated);
|
||||
}
|
||||
|
||||
async setSessionMode(
|
||||
sessionId: string,
|
||||
modeId: string,
|
||||
): Promise<{ session: Session; response: SetSessionModeResponse | void }> {
|
||||
async setSessionMode(sessionId: string, modeId: string): Promise<{ session: Session; response: SetSessionModeResponse | void }> {
|
||||
const mode = modeId.trim();
|
||||
if (!mode) {
|
||||
throw new Error("setSessionMode requires a non-empty modeId");
|
||||
|
|
@ -1124,13 +1077,10 @@ export class SandboxAgent {
|
|||
}
|
||||
|
||||
try {
|
||||
return (await this.sendSessionMethodInternal(
|
||||
sessionId,
|
||||
"session/set_mode",
|
||||
{ modeId: mode },
|
||||
{},
|
||||
false,
|
||||
)) as { session: Session; response: SetSessionModeResponse | void };
|
||||
return (await this.sendSessionMethodInternal(sessionId, "session/set_mode", { modeId: mode }, {}, false)) as {
|
||||
session: Session;
|
||||
response: SetSessionModeResponse | void;
|
||||
};
|
||||
} catch (error) {
|
||||
if (!(error instanceof AcpRpcError) || error.code !== -32601) {
|
||||
throw error;
|
||||
|
|
@ -1139,11 +1089,7 @@ export class SandboxAgent {
|
|||
}
|
||||
}
|
||||
|
||||
async setSessionConfigOption(
|
||||
sessionId: string,
|
||||
configId: string,
|
||||
value: string,
|
||||
): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||
async setSessionConfigOption(sessionId: string, configId: string, value: string): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||
const resolvedConfigId = configId.trim();
|
||||
if (!resolvedConfigId) {
|
||||
throw new Error("setSessionConfigOption requires a non-empty configId");
|
||||
|
|
@ -1165,13 +1111,7 @@ export class SandboxAgent {
|
|||
|
||||
const allowedValues = extractConfigValues(option);
|
||||
if (allowedValues.length > 0 && !allowedValues.includes(resolvedValue)) {
|
||||
throw new UnsupportedSessionValueError(
|
||||
sessionId,
|
||||
option.category ?? "uncategorized",
|
||||
option.id,
|
||||
resolvedValue,
|
||||
allowedValues,
|
||||
);
|
||||
throw new UnsupportedSessionValueError(sessionId, option.category ?? "uncategorized", option.id, resolvedValue, allowedValues);
|
||||
}
|
||||
|
||||
return (await this.sendSessionMethodInternal(
|
||||
|
|
@ -1186,17 +1126,11 @@ export class SandboxAgent {
|
|||
)) as { session: Session; response: SetSessionConfigOptionResponse };
|
||||
}
|
||||
|
||||
async setSessionModel(
|
||||
sessionId: string,
|
||||
model: string,
|
||||
): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||
async setSessionModel(sessionId: string, model: string): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||
return this.setSessionCategoryValue(sessionId, "model", model);
|
||||
}
|
||||
|
||||
async setSessionThoughtLevel(
|
||||
sessionId: string,
|
||||
thoughtLevel: string,
|
||||
): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||
async setSessionThoughtLevel(sessionId: string, thoughtLevel: string): Promise<{ session: Session; response: SetSessionConfigOptionResponse }> {
|
||||
return this.setSessionCategoryValue(sessionId, "thought_level", thoughtLevel);
|
||||
}
|
||||
|
||||
|
|
@ -1249,13 +1183,7 @@ export class SandboxAgent {
|
|||
|
||||
const allowedValues = extractConfigValues(option);
|
||||
if (allowedValues.length > 0 && !allowedValues.includes(resolvedValue)) {
|
||||
throw new UnsupportedSessionValueError(
|
||||
sessionId,
|
||||
category,
|
||||
option.id,
|
||||
resolvedValue,
|
||||
allowedValues,
|
||||
);
|
||||
throw new UnsupportedSessionValueError(sessionId, category, option.id, resolvedValue, allowedValues);
|
||||
}
|
||||
|
||||
return this.setSessionConfigOption(sessionId, option.id, resolvedValue);
|
||||
|
|
@ -1267,16 +1195,26 @@ export class SandboxAgent {
|
|||
}
|
||||
|
||||
const info = await this.getAgent(snapshot.agent, { config: true });
|
||||
const configOptions = normalizeSessionConfigOptions(info.configOptions) ?? [];
|
||||
let configOptions = normalizeSessionConfigOptions(info.configOptions) ?? [];
|
||||
// Re-read the record from persistence so we merge against the latest
|
||||
// state, not a stale snapshot captured before the network await.
|
||||
const record = await this.persist.getSession(sessionId);
|
||||
if (!record) {
|
||||
return { ...snapshot, configOptions };
|
||||
}
|
||||
|
||||
const currentModeId = record.modes?.currentModeId;
|
||||
if (currentModeId) {
|
||||
const modeOption = findConfigOptionByCategory(configOptions, "mode");
|
||||
if (modeOption) {
|
||||
configOptions = applyConfigOptionValue(configOptions, modeOption.id, currentModeId) ?? configOptions;
|
||||
}
|
||||
}
|
||||
|
||||
const updated: SessionRecord = {
|
||||
...record,
|
||||
configOptions,
|
||||
modes: deriveModesFromConfigOptions(configOptions) ?? record.modes,
|
||||
};
|
||||
await this.persist.updateSession(updated);
|
||||
return updated;
|
||||
|
|
@ -1323,12 +1261,7 @@ export class SandboxAgent {
|
|||
};
|
||||
}
|
||||
|
||||
private async persistSessionStateFromMethod(
|
||||
sessionId: string,
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
response: unknown,
|
||||
): Promise<void> {
|
||||
private async persistSessionStateFromMethod(sessionId: string, method: string, params: Record<string, unknown>, response: unknown): Promise<void> {
|
||||
// Re-read the record from persistence so we merge against the latest
|
||||
// state, not a stale snapshot captured before the RPC await.
|
||||
const record = await this.persist.getSession(sessionId);
|
||||
|
|
@ -1624,21 +1557,13 @@ export class SandboxAgent {
|
|||
});
|
||||
}
|
||||
|
||||
async followProcessLogs(
|
||||
id: string,
|
||||
listener: ProcessLogListener,
|
||||
query: ProcessLogFollowQuery = {},
|
||||
): Promise<ProcessLogSubscription> {
|
||||
async followProcessLogs(id: string, listener: ProcessLogListener, query: ProcessLogFollowQuery = {}): Promise<ProcessLogSubscription> {
|
||||
const abortController = new AbortController();
|
||||
const response = await this.requestRaw(
|
||||
"GET",
|
||||
`${API_PREFIX}/processes/${encodeURIComponent(id)}/logs`,
|
||||
{
|
||||
query: { ...query, follow: true },
|
||||
accept: "text/event-stream",
|
||||
signal: abortController.signal,
|
||||
},
|
||||
);
|
||||
const response = await this.requestRaw("GET", `${API_PREFIX}/processes/${encodeURIComponent(id)}/logs`, {
|
||||
query: { ...query, follow: true },
|
||||
accept: "text/event-stream",
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
if (!response.body) {
|
||||
abortController.abort();
|
||||
|
|
@ -1659,23 +1584,13 @@ export class SandboxAgent {
|
|||
});
|
||||
}
|
||||
|
||||
async resizeProcessTerminal(
|
||||
id: string,
|
||||
request: ProcessTerminalResizeRequest,
|
||||
): Promise<ProcessTerminalResizeResponse> {
|
||||
return this.requestJson(
|
||||
"POST",
|
||||
`${API_PREFIX}/processes/${encodeURIComponent(id)}/terminal/resize`,
|
||||
{
|
||||
body: request,
|
||||
},
|
||||
);
|
||||
async resizeProcessTerminal(id: string, request: ProcessTerminalResizeRequest): Promise<ProcessTerminalResizeResponse> {
|
||||
return this.requestJson("POST", `${API_PREFIX}/processes/${encodeURIComponent(id)}/terminal/resize`, {
|
||||
body: request,
|
||||
});
|
||||
}
|
||||
|
||||
buildProcessTerminalWebSocketUrl(
|
||||
id: string,
|
||||
options: ProcessTerminalWebSocketUrlOptions = {},
|
||||
): string {
|
||||
buildProcessTerminalWebSocketUrl(id: string, options: ProcessTerminalWebSocketUrlOptions = {}): string {
|
||||
return toWebSocketUrl(
|
||||
this.buildUrl(`${API_PREFIX}/processes/${encodeURIComponent(id)}/terminal/ws`, {
|
||||
access_token: options.accessToken ?? this.token,
|
||||
|
|
@ -1683,10 +1598,7 @@ export class SandboxAgent {
|
|||
);
|
||||
}
|
||||
|
||||
connectProcessTerminalWebSocket(
|
||||
id: string,
|
||||
options: ProcessTerminalConnectOptions = {},
|
||||
): WebSocket {
|
||||
connectProcessTerminalWebSocket(id: string, options: ProcessTerminalConnectOptions = {}): WebSocket {
|
||||
const WebSocketCtor = options.WebSocket ?? globalThis.WebSocket;
|
||||
if (!WebSocketCtor) {
|
||||
throw new Error("WebSocket API is not available; provide a WebSocket implementation.");
|
||||
|
|
@ -1700,10 +1612,7 @@ export class SandboxAgent {
|
|||
);
|
||||
}
|
||||
|
||||
connectProcessTerminal(
|
||||
id: string,
|
||||
options: ProcessTerminalSessionOptions = {},
|
||||
): ProcessTerminalSession {
|
||||
connectProcessTerminal(id: string, options: ProcessTerminalSessionOptions = {}): ProcessTerminalSession {
|
||||
return new ProcessTerminalSession(this.connectProcessTerminalWebSocket(id, options));
|
||||
}
|
||||
|
||||
|
|
@ -1789,11 +1698,7 @@ export class SandboxAgent {
|
|||
}
|
||||
}
|
||||
|
||||
private async persistSessionStateFromEvent(
|
||||
sessionId: string,
|
||||
envelope: AnyMessage,
|
||||
direction: AcpEnvelopeDirection,
|
||||
): Promise<void> {
|
||||
private async persistSessionStateFromEvent(sessionId: string, envelope: AnyMessage, direction: AcpEnvelopeDirection): Promise<void> {
|
||||
if (direction !== "inbound") {
|
||||
return;
|
||||
}
|
||||
|
|
@ -2081,12 +1986,9 @@ export class SandboxAgent {
|
|||
}
|
||||
|
||||
private async runHealthWait(): Promise<void> {
|
||||
const signal = this.healthWait.enabled
|
||||
? anyAbortSignal([this.healthWait.signal, this.healthWaitAbortController.signal])
|
||||
: undefined;
|
||||
const signal = this.healthWait.enabled ? anyAbortSignal([this.healthWait.signal, this.healthWaitAbortController.signal]) : undefined;
|
||||
const startedAt = Date.now();
|
||||
const deadline =
|
||||
typeof this.healthWait.timeoutMs === "number" ? startedAt + this.healthWait.timeoutMs : undefined;
|
||||
const deadline = typeof this.healthWait.timeoutMs === "number" ? startedAt + this.healthWait.timeoutMs : undefined;
|
||||
|
||||
let delayMs = HEALTH_WAIT_MIN_DELAY_MS;
|
||||
let nextLogAt = startedAt + HEALTH_WAIT_LOG_AFTER_MS;
|
||||
|
|
@ -2111,9 +2013,7 @@ export class SandboxAgent {
|
|||
const now = Date.now();
|
||||
if (now >= nextLogAt) {
|
||||
const details = formatHealthWaitError(lastError);
|
||||
console.warn(
|
||||
`sandbox-agent at ${this.baseUrl} is not healthy after ${now - startedAt}ms; still waiting (${details})`,
|
||||
);
|
||||
console.warn(`sandbox-agent at ${this.baseUrl} is not healthy after ${now - startedAt}ms; still waiting (${details})`);
|
||||
nextLogAt = now + HEALTH_WAIT_LOG_EVERY_MS;
|
||||
}
|
||||
|
||||
|
|
@ -2125,9 +2025,7 @@ export class SandboxAgent {
|
|||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Timed out waiting for sandbox-agent health after ${this.healthWait.timeoutMs}ms (${formatHealthWaitError(lastError)})`,
|
||||
);
|
||||
throw new Error(`Timed out waiting for sandbox-agent health after ${this.healthWait.timeoutMs}ms (${formatHealthWaitError(lastError)})`);
|
||||
}
|
||||
|
||||
private buildHeaders(extra?: HeadersInit): Headers {
|
||||
|
|
@ -2189,9 +2087,7 @@ type RequestOptions = {
|
|||
skipReadyWait?: boolean;
|
||||
};
|
||||
|
||||
type NormalizedHealthWaitOptions =
|
||||
| { enabled: false; timeoutMs?: undefined; signal?: undefined }
|
||||
| { enabled: true; timeoutMs?: number; signal?: AbortSignal };
|
||||
type NormalizedHealthWaitOptions = { enabled: false; timeoutMs?: undefined; signal?: undefined } | { enabled: true; timeoutMs?: number; signal?: AbortSignal };
|
||||
|
||||
function parseProcessTerminalServerFrame(payload: string): ProcessTerminalServerFrame | null {
|
||||
try {
|
||||
|
|
@ -2204,12 +2100,7 @@ function parseProcessTerminalServerFrame(payload: string): ProcessTerminalServer
|
|||
return parsed as ProcessTerminalServerFrame;
|
||||
}
|
||||
|
||||
if (
|
||||
parsed.type === "exit" &&
|
||||
(parsed.exitCode === undefined ||
|
||||
parsed.exitCode === null ||
|
||||
typeof parsed.exitCode === "number")
|
||||
) {
|
||||
if (parsed.type === "exit" && (parsed.exitCode === undefined || parsed.exitCode === null || typeof parsed.exitCode === "number")) {
|
||||
return parsed as ProcessTerminalServerFrame;
|
||||
}
|
||||
|
||||
|
|
@ -2223,9 +2114,7 @@ function parseProcessTerminalServerFrame(payload: string): ProcessTerminalServer
|
|||
return null;
|
||||
}
|
||||
|
||||
function encodeTerminalInput(
|
||||
data: string | ArrayBuffer | ArrayBufferView,
|
||||
): { data: string; encoding?: "base64" } {
|
||||
function encodeTerminalInput(data: string | ArrayBuffer | ArrayBufferView): { data: string; encoding?: "base64" } {
|
||||
if (typeof data === "string") {
|
||||
return { data };
|
||||
}
|
||||
|
|
@ -2286,12 +2175,7 @@ async function autoAuthenticate(acp: AcpHttpClient, methods: AuthMethod[]): Prom
|
|||
// Only attempt env-var-based methods that the server process can satisfy
|
||||
// automatically. Interactive methods (e.g. "claude-login") cannot be
|
||||
// fulfilled programmatically and must be skipped.
|
||||
const envBased = methods.find(
|
||||
(m) =>
|
||||
m.id === "codex-api-key" ||
|
||||
m.id === "openai-api-key" ||
|
||||
m.id === "anthropic-api-key",
|
||||
);
|
||||
const envBased = methods.find((m) => m.id === "codex-api-key" || m.id === "openai-api-key" || m.id === "anthropic-api-key");
|
||||
|
||||
if (!envBased) {
|
||||
return;
|
||||
|
|
@ -2316,9 +2200,7 @@ function toAgentQuery(options: AgentQueryOptions | undefined): Record<string, Qu
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeSessionInit(
|
||||
value: Omit<NewSessionRequest, "_meta"> | undefined,
|
||||
): Omit<NewSessionRequest, "_meta"> {
|
||||
function normalizeSessionInit(value: Omit<NewSessionRequest, "_meta"> | undefined): Omit<NewSessionRequest, "_meta"> {
|
||||
if (!value) {
|
||||
return {
|
||||
cwd: defaultCwd(),
|
||||
|
|
@ -2354,8 +2236,7 @@ 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";
|
||||
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) {
|
||||
|
|
@ -2469,10 +2350,7 @@ function normalizePositiveInt(value: number | undefined, fallback: number): numb
|
|||
return Math.floor(value as number);
|
||||
}
|
||||
|
||||
function normalizeHealthWaitOptions(
|
||||
value: boolean | SandboxAgentHealthWaitOptions | undefined,
|
||||
signal: AbortSignal | undefined,
|
||||
): NormalizedHealthWaitOptions {
|
||||
function normalizeHealthWaitOptions(value: boolean | SandboxAgentHealthWaitOptions | undefined, signal: AbortSignal | undefined): NormalizedHealthWaitOptions {
|
||||
if (value === false) {
|
||||
return { enabled: false };
|
||||
}
|
||||
|
|
@ -2481,10 +2359,7 @@ function normalizeHealthWaitOptions(
|
|||
return { enabled: true, signal };
|
||||
}
|
||||
|
||||
const timeoutMs =
|
||||
typeof value.timeoutMs === "number" && Number.isFinite(value.timeoutMs) && value.timeoutMs > 0
|
||||
? Math.floor(value.timeoutMs)
|
||||
: undefined;
|
||||
const timeoutMs = typeof value.timeoutMs === "number" && Number.isFinite(value.timeoutMs) && value.timeoutMs > 0 ? Math.floor(value.timeoutMs) : undefined;
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
|
|
@ -2538,17 +2413,11 @@ function extractConfigOptionsFromSetResponse(response: unknown): SessionConfigOp
|
|||
return normalizeSessionConfigOptions(response.configOptions);
|
||||
}
|
||||
|
||||
function findConfigOptionByCategory(
|
||||
options: SessionConfigOption[],
|
||||
category: string,
|
||||
): SessionConfigOption | undefined {
|
||||
function findConfigOptionByCategory(options: SessionConfigOption[], category: string): SessionConfigOption | undefined {
|
||||
return options.find((option) => option.category === category);
|
||||
}
|
||||
|
||||
function findConfigOptionById(
|
||||
options: SessionConfigOption[],
|
||||
configId: string,
|
||||
): SessionConfigOption | undefined {
|
||||
function findConfigOptionById(options: SessionConfigOption[], configId: string): SessionConfigOption | undefined {
|
||||
return options.find((option) => option.id === configId);
|
||||
}
|
||||
|
||||
|
|
@ -2583,14 +2452,10 @@ function extractKnownModeIds(modes: SessionModeState | null | undefined): string
|
|||
if (!modes || !Array.isArray(modes.availableModes)) {
|
||||
return [];
|
||||
}
|
||||
return modes.availableModes
|
||||
.map((mode) => (typeof mode.id === "string" ? mode.id : null))
|
||||
.filter((value): value is string => !!value);
|
||||
return modes.availableModes.map((mode) => (typeof mode.id === "string" ? mode.id : null)).filter((value): value is string => !!value);
|
||||
}
|
||||
|
||||
function deriveModesFromConfigOptions(
|
||||
configOptions: SessionConfigOption[] | undefined,
|
||||
): SessionModeState | null {
|
||||
function deriveModesFromConfigOptions(configOptions: SessionConfigOption[] | undefined): SessionModeState | null {
|
||||
if (!configOptions || configOptions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -2609,18 +2474,12 @@ function deriveModesFromConfigOptions(
|
|||
}));
|
||||
|
||||
return {
|
||||
currentModeId:
|
||||
typeof modeOption.currentValue === "string" && modeOption.currentValue.length > 0
|
||||
? modeOption.currentValue
|
||||
: availableModes[0]?.id ?? "",
|
||||
currentModeId: typeof modeOption.currentValue === "string" && modeOption.currentValue.length > 0 ? modeOption.currentValue : (availableModes[0]?.id ?? ""),
|
||||
availableModes,
|
||||
};
|
||||
}
|
||||
|
||||
function applyCurrentMode(
|
||||
modes: SessionModeState | null | undefined,
|
||||
currentModeId: string,
|
||||
): SessionModeState | null {
|
||||
function applyCurrentMode(modes: SessionModeState | null | undefined, currentModeId: string): SessionModeState | null {
|
||||
if (modes && Array.isArray(modes.availableModes)) {
|
||||
return {
|
||||
...modes,
|
||||
|
|
@ -2633,11 +2492,7 @@ function applyCurrentMode(
|
|||
};
|
||||
}
|
||||
|
||||
function applyConfigOptionValue(
|
||||
configOptions: SessionConfigOption[],
|
||||
configId: string,
|
||||
value: string,
|
||||
): SessionConfigOption[] | null {
|
||||
function applyConfigOptionValue(configOptions: SessionConfigOption[], configId: string, value: string): SessionConfigOption[] | null {
|
||||
const idx = configOptions.findIndex((o) => o.id === configId);
|
||||
if (idx === -1) {
|
||||
return null;
|
||||
|
|
@ -2704,28 +2559,16 @@ function availablePermissionReplies(options: PermissionOption[]): PermissionRepl
|
|||
return [...replies];
|
||||
}
|
||||
|
||||
function permissionReplyToResponse(
|
||||
permissionId: string,
|
||||
request: RequestPermissionRequest,
|
||||
reply: PermissionReply,
|
||||
): RequestPermissionResponse {
|
||||
function permissionReplyToResponse(permissionId: string, request: RequestPermissionRequest, reply: PermissionReply): RequestPermissionResponse {
|
||||
const preferredKinds: PermissionOptionKind[] =
|
||||
reply === "once"
|
||||
? ["allow_once"]
|
||||
: reply === "always"
|
||||
? ["allow_always", "allow_once"]
|
||||
: ["reject_once", "reject_always"];
|
||||
reply === "once" ? ["allow_once"] : reply === "always" ? ["allow_always", "allow_once"] : ["reject_once", "reject_always"];
|
||||
|
||||
const selected = preferredKinds
|
||||
.map((kind) => request.options.find((option) => option.kind === kind))
|
||||
.find((option): option is PermissionOption => Boolean(option));
|
||||
|
||||
if (!selected) {
|
||||
throw new UnsupportedPermissionReplyError(
|
||||
permissionId,
|
||||
reply,
|
||||
availablePermissionReplies(request.options),
|
||||
);
|
||||
throw new UnsupportedPermissionReplyError(permissionId, reply, availablePermissionReplies(request.options));
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -2745,12 +2588,7 @@ function cancelledPermissionResponse(): RequestPermissionResponse {
|
|||
}
|
||||
|
||||
function isSessionConfigOption(value: unknown): value is SessionConfigOption {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.id === "string" &&
|
||||
typeof value.name === "string" &&
|
||||
typeof value.type === "string"
|
||||
);
|
||||
return isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && typeof value.type === "string";
|
||||
}
|
||||
|
||||
function toTitleCase(input: string): string {
|
||||
|
|
@ -2850,11 +2688,7 @@ async function waitForAbortable<T>(promise: Promise<T>, signal: AbortSignal | un
|
|||
});
|
||||
}
|
||||
|
||||
async function consumeProcessLogSse(
|
||||
body: ReadableStream<Uint8Array>,
|
||||
listener: ProcessLogListener,
|
||||
signal: AbortSignal,
|
||||
): Promise<void> {
|
||||
async function consumeProcessLogSse(body: ReadableStream<Uint8Array>, listener: ProcessLogListener, signal: AbortSignal): Promise<void> {
|
||||
const reader = body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@ import { dirname, resolve } from "node:path";
|
|||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { tmpdir } from "node:os";
|
||||
import {
|
||||
InMemorySessionPersistDriver,
|
||||
SandboxAgent,
|
||||
type SessionEvent,
|
||||
} from "../src/index.ts";
|
||||
import { InMemorySessionPersistDriver, SandboxAgent, type SessionEvent } from "../src/index.ts";
|
||||
import { spawnSandboxAgent, isNodeRuntime, type SandboxAgentSpawnHandle } from "../src/spawn.ts";
|
||||
import { prepareMockAgentDataHome } from "./helpers/mock-agent.ts";
|
||||
import WebSocket from "ws";
|
||||
|
|
@ -21,10 +17,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)) {
|
||||
|
|
@ -37,9 +30,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;
|
||||
|
|
@ -49,11 +40,7 @@ function sleep(ms: number): Promise<void> {
|
|||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function waitFor<T>(
|
||||
fn: () => T | undefined | null,
|
||||
timeoutMs = 6000,
|
||||
stepMs = 30,
|
||||
): Promise<T> {
|
||||
async function waitFor<T>(fn: () => T | undefined | null, timeoutMs = 6000, stepMs = 30): Promise<T> {
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
const value = fn();
|
||||
|
|
@ -65,11 +52,7 @@ async function waitFor<T>(
|
|||
throw new Error("timed out waiting for condition");
|
||||
}
|
||||
|
||||
async function waitForAsync<T>(
|
||||
fn: () => Promise<T | undefined | null>,
|
||||
timeoutMs = 6000,
|
||||
stepMs = 30,
|
||||
): Promise<T> {
|
||||
async function waitForAsync<T>(fn: () => Promise<T | undefined | null>, timeoutMs = 6000, stepMs = 30): Promise<T> {
|
||||
const started = Date.now();
|
||||
while (Date.now() - started < timeoutMs) {
|
||||
const value = await fn();
|
||||
|
|
@ -265,10 +248,7 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
});
|
||||
expect(moved.to).toBe(movedPath);
|
||||
|
||||
const uploadResult = await sdk.uploadFsBatch(
|
||||
buildTarArchive([{ name: "batch.txt", content: "batch upload works" }]),
|
||||
{ path: uploadDir },
|
||||
);
|
||||
const uploadResult = await sdk.uploadFsBatch(buildTarArchive([{ name: "batch.txt", content: "batch upload works" }]), { path: uploadDir });
|
||||
expect(uploadResult.paths.some((path) => path.endsWith("batch.txt"))).toBe(true);
|
||||
|
||||
const uploaded = await sdk.readFsFile({ path: join(uploadDir, "batch.txt") });
|
||||
|
|
@ -316,9 +296,7 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
}, 60_000);
|
||||
|
||||
it("requires baseUrl when fetch is not provided", async () => {
|
||||
await expect(SandboxAgent.connect({ token } as any)).rejects.toThrow(
|
||||
"baseUrl is required unless fetch is provided.",
|
||||
);
|
||||
await expect(SandboxAgent.connect({ token } as any)).rejects.toThrow("baseUrl is required unless fetch is provided.");
|
||||
});
|
||||
|
||||
it("waits for health before non-ACP HTTP helpers", async () => {
|
||||
|
|
@ -357,11 +335,7 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
|
||||
const firstAgentsRequest = seenPaths.indexOf("/v1/agents");
|
||||
expect(firstAgentsRequest).toBeGreaterThanOrEqual(0);
|
||||
expect(seenPaths.slice(0, firstAgentsRequest)).toEqual([
|
||||
"/v1/health",
|
||||
"/v1/health",
|
||||
"/v1/health",
|
||||
]);
|
||||
expect(seenPaths.slice(0, firstAgentsRequest)).toEqual(["/v1/health", "/v1/health", "/v1/health"]);
|
||||
|
||||
await sdk.dispose();
|
||||
});
|
||||
|
|
@ -469,11 +443,7 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
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();
|
||||
|
|
@ -512,12 +482,8 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
|
||||
const session = await sdk.createSession({ agent: "mock" });
|
||||
|
||||
await expect(session.rawSend("session/cancel")).rejects.toThrow(
|
||||
"Use destroySession(sessionId) instead.",
|
||||
);
|
||||
await expect(sdk.rawSendSessionMethod(session.id, "session/cancel", {})).rejects.toThrow(
|
||||
"Use destroySession(sessionId) instead.",
|
||||
);
|
||||
await expect(session.rawSend("session/cancel")).rejects.toThrow("Use destroySession(sessionId) instead.");
|
||||
await expect(sdk.rawSendSessionMethod(session.id, "session/cancel", {})).rejects.toThrow("Use destroySession(sessionId) instead.");
|
||||
|
||||
const destroyed = await sdk.destroySession(session.id);
|
||||
expect(destroyed.destroyedAt).toBeDefined();
|
||||
|
|
@ -572,8 +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");
|
||||
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();
|
||||
});
|
||||
|
|
@ -775,13 +750,9 @@ describe("Integration: TypeScript SDK flat session API", () => {
|
|||
|
||||
const initialLogs = await waitForAsync(async () => {
|
||||
const logs = await sdk.getProcessLogs(interactiveProcess.id, { tail: 10 });
|
||||
return logs.entries.some((entry) => decodeProcessLogData(entry.data, entry.encoding).includes("ready"))
|
||||
? logs
|
||||
: undefined;
|
||||
return logs.entries.some((entry) => decodeProcessLogData(entry.data, entry.encoding).includes("ready")) ? logs : undefined;
|
||||
});
|
||||
expect(
|
||||
initialLogs.entries.some((entry) => decodeProcessLogData(entry.data, entry.encoding).includes("ready")),
|
||||
).toBe(true);
|
||||
expect(initialLogs.entries.some((entry) => decodeProcessLogData(entry.data, entry.encoding).includes("ready"))).toBe(true);
|
||||
|
||||
const followedLogs: string[] = [];
|
||||
const subscription = await sdk.followProcessLogs(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue