feat: acp http adapter

This commit is contained in:
Nathan Flurry 2026-02-10 16:05:56 -08:00
parent 2ba630c180
commit b4c8564cb2
217 changed files with 18785 additions and 17400 deletions

View file

@ -25,11 +25,11 @@
],
"scripts": {
"generate:openapi": "SANDBOX_AGENT_SKIP_INSPECTOR=1 cargo run -p sandbox-agent-openapi-gen -- --out ../../docs/openapi.json",
"generate:types": "openapi-typescript ../../docs/openapi.json -o src/generated/openapi.ts",
"generate:types": "openapi-typescript ../../docs/openapi.json -o src/generated/openapi.ts && node ./scripts/patch-openapi-types.mjs",
"generate": "pnpm run generate:openapi && pnpm run generate:types",
"build": "pnpm --filter acp-http-client build && if [ -z \"$SKIP_OPENAPI_GEN\" ]; then pnpm run generate:openapi; fi && pnpm run generate:types && tsup",
"typecheck": "pnpm --filter acp-http-client build && tsc --noEmit",
"test": "pnpm --filter acp-http-client build && vitest run",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {

View file

@ -0,0 +1,17 @@
import { readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
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"],
];
for (const [from, to] of replacements) {
source = source.split(from).join(to);
}
writeFileSync(target, source);

File diff suppressed because it is too large Load diff

View file

@ -5,36 +5,57 @@
export interface paths {
"/v2/fs/file": {
get: operations["get_v2_fs_file"];
put: operations["put_v2_fs_file"];
"/v1/acp": {
get: operations["get_v1_acp_servers"];
};
"/v2/fs/upload-batch": {
post: operations["post_v2_fs_upload_batch"];
"/v1/acp/{server_id}": {
get: operations["get_v1_acp"];
post: operations["post_v1_acp"];
delete: operations["delete_v1_acp"];
};
"/v2/health": {
/**
* v2 Health
* @description Returns server health for the v2 ACP surface.
*/
get: operations["get_v2_health"];
"/v1/agents": {
get: operations["get_v1_agents"];
};
"/v2/rpc": {
/**
* ACP SSE
* @description Streams ACP JSON-RPC envelopes for an ACP client over SSE.
*/
get: operations["get_v2_acp"];
/**
* ACP POST
* @description Sends ACP JSON-RPC envelopes to an ACP client and returns request responses.
*/
post: operations["post_v2_acp"];
/**
* ACP Close
* @description Closes an ACP client and releases agent process resources.
*/
delete: operations["delete_v2_acp"];
"/v1/agents/{agent}": {
get: operations["get_v1_agent"];
};
"/v1/agents/{agent}/install": {
post: operations["post_v1_agent_install"];
};
"/v1/config/mcp": {
get: operations["get_v1_config_mcp"];
put: operations["put_v1_config_mcp"];
delete: operations["delete_v1_config_mcp"];
};
"/v1/config/skills": {
get: operations["get_v1_config_skills"];
put: operations["put_v1_config_skills"];
delete: operations["delete_v1_config_skills"];
};
"/v1/fs/entries": {
get: operations["get_v1_fs_entries"];
};
"/v1/fs/entry": {
delete: operations["delete_v1_fs_entry"];
};
"/v1/fs/file": {
get: operations["get_v1_fs_file"];
put: operations["put_v1_fs_file"];
};
"/v1/fs/mkdir": {
post: operations["post_v1_fs_mkdir"];
};
"/v1/fs/move": {
post: operations["post_v1_fs_move"];
};
"/v1/fs/stat": {
get: operations["get_v1_fs_stat"];
};
"/v1/fs/upload-batch": {
post: operations["post_v1_fs_upload_batch"];
};
"/v1/health": {
get: operations["get_v1_health"];
};
}
@ -50,6 +71,18 @@ export interface components {
params?: unknown;
result?: unknown;
};
AcpPostQuery: {
agent?: string | null;
};
AcpServerInfo: {
agent: string;
/** Format: int64 */
createdAtMs: number;
serverId: string;
};
AcpServerListResponse: {
servers: components["schemas"]["AcpServerInfo"][];
};
AgentCapabilities: {
commandExecution: boolean;
errorEvents: boolean;
@ -72,12 +105,11 @@ export interface components {
};
AgentInfo: {
capabilities: components["schemas"]["AgentCapabilities"];
configError?: string | null;
configOptions?: unknown[] | null;
credentialsAvailable: boolean;
defaultModel?: string | null;
id: string;
installed: boolean;
models?: components["schemas"]["AgentModelInfo"][] | null;
modes?: components["schemas"]["AgentModeInfo"][] | null;
path?: string | null;
serverStatus?: components["schemas"]["ServerStatusInfo"] | null;
version?: string | null;
@ -100,28 +132,17 @@ export interface components {
AgentListResponse: {
agents: components["schemas"]["AgentInfo"][];
};
AgentModeInfo: {
description: string;
id: string;
name: string;
};
AgentModelInfo: {
id: string;
name?: string | null;
};
/** @enum {string} */
ErrorType: "invalid_request" | "unsupported_agent" | "agent_not_installed" | "install_failed" | "agent_process_exited" | "token_invalid" | "permission_denied" | "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" | "session_not_found" | "session_already_exists" | "mode_not_supported" | "stream_error" | "timeout";
FsActionResponse: {
path: string;
};
FsDeleteQuery: {
path: string;
recursive?: boolean | null;
sessionId?: string | null;
};
FsEntriesQuery: {
path?: string | null;
sessionId?: string | null;
};
FsEntry: {
entryType: components["schemas"]["FsEntryType"];
@ -144,10 +165,6 @@ export interface components {
};
FsPathQuery: {
path: string;
sessionId?: string | null;
};
FsSessionQuery: {
sessionId?: string | null;
};
FsStat: {
entryType: components["schemas"]["FsEntryType"];
@ -158,7 +175,6 @@ export interface components {
};
FsUploadBatchQuery: {
path?: string | null;
sessionId?: string | null;
};
FsUploadBatchResponse: {
paths: string[];
@ -172,6 +188,39 @@ export interface components {
HealthResponse: {
status: string;
};
McpConfigQuery: {
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;
});
ProblemDetails: {
detail?: string | null;
instance?: string | null;
@ -182,50 +231,25 @@ export interface components {
[key: string]: unknown;
};
/** @enum {string} */
ServerStatus: "running" | "stopped" | "error";
ServerStatus: "running" | "stopped";
ServerStatusInfo: {
baseUrl?: string | null;
lastError?: string | null;
/** Format: int64 */
restartCount: number;
status: components["schemas"]["ServerStatus"];
/** Format: int64 */
uptimeMs?: number | null;
};
SessionInfo: {
agent: string;
agentMode: string;
/** Format: int64 */
createdAt: number;
directory?: string | null;
ended: boolean;
/** Format: int64 */
eventCount: number;
model?: string | null;
nativeSessionId?: string | null;
permissionMode: string;
sessionId: string;
terminationInfo?: components["schemas"]["TerminationInfo"] | null;
title?: string | null;
/** Format: int64 */
updatedAt: number;
SkillSource: {
ref?: string | null;
skills?: string[] | null;
source: string;
subpath?: string | null;
type: string;
};
SessionListResponse: {
sessions: components["schemas"]["SessionInfo"][];
SkillsConfig: {
sources: components["schemas"]["SkillSource"][];
};
StderrOutput: {
head?: string | null;
tail?: string | null;
totalLines?: number | null;
truncated: boolean;
};
TerminationInfo: {
/** Format: int32 */
exitCode?: number | null;
message?: string | null;
reason: string;
stderr?: components["schemas"]["StderrOutput"] | null;
terminatedBy: string;
SkillsConfigQuery: {
directory: string;
skillName: string;
};
};
responses: never;
@ -241,89 +265,23 @@ export type external = Record<string, never>;
export interface operations {
get_v2_fs_file: {
parameters: {
query: {
/** @description File path */
path: string;
/** @description Session id for relative path base */
session_id?: string | null;
};
};
get_v1_acp_servers: {
responses: {
/** @description File content */
200: {
content: never;
};
};
};
put_v2_fs_file: {
parameters: {
query: {
/** @description File path */
path: string;
/** @description Session id for relative path base */
session_id?: string | null;
};
};
/** @description Raw file bytes */
requestBody: {
content: {
"text/plain": string;
};
};
responses: {
/** @description Write result */
/** @description Active ACP server instances */
200: {
content: {
"application/json": components["schemas"]["FsWriteResponse"];
"application/json": components["schemas"]["AcpServerListResponse"];
};
};
};
};
post_v2_fs_upload_batch: {
get_v1_acp: {
parameters: {
query?: {
/** @description Destination path */
path?: string | null;
/** @description Session id for relative path base */
session_id?: string | null;
path: {
/** @description Client-defined ACP server id */
server_id: string;
};
};
/** @description tar archive body */
requestBody: {
content: {
"text/plain": string;
};
};
responses: {
/** @description Upload/extract result */
200: {
content: {
"application/json": components["schemas"]["FsUploadBatchResponse"];
};
};
};
};
/**
* v2 Health
* @description Returns server health for the v2 ACP surface.
*/
get_v2_health: {
responses: {
/** @description Service health response */
200: {
content: {
"application/json": components["schemas"]["HealthResponse"];
};
};
};
};
/**
* ACP SSE
* @description Streams ACP JSON-RPC envelopes for an ACP client over SSE.
*/
get_v2_acp: {
responses: {
/** @description SSE stream of ACP envelopes */
200: {
@ -335,19 +293,31 @@ export interface operations {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Unknown ACP client */
/** @description Unknown ACP server */
404: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Client does not accept SSE responses */
406: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
/**
* ACP POST
* @description Sends ACP JSON-RPC envelopes to an ACP client and returns request responses.
*/
post_v2_acp: {
post_v1_acp: {
parameters: {
query?: {
/** @description Agent id required for first POST */
agent?: string | null;
};
path: {
/** @description Client-defined ACP server id */
server_id: string;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["AcpEnvelope"];
@ -370,12 +340,30 @@ export interface operations {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Unknown ACP client */
/** @description Unknown ACP server */
404: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Client does not accept JSON responses */
406: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description ACP server bound to different agent */
409: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Unsupported media type */
415: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description ACP agent process response timeout */
504: {
content: {
@ -384,23 +372,128 @@ export interface operations {
};
};
};
/**
* ACP Close
* @description Closes an ACP client and releases agent process resources.
*/
delete_v2_acp: {
delete_v1_acp: {
parameters: {
path: {
/** @description Client-defined ACP server id */
server_id: string;
};
};
responses: {
/** @description ACP client closed */
/** @description ACP server closed */
204: {
content: never;
};
};
};
get_v1_agents: {
parameters: {
query?: {
/** @description When true, include version/path/configOptions (slower) */
config?: boolean | null;
/** @description When true, bypass version cache */
no_cache?: boolean | null;
};
};
responses: {
/** @description List of v1 agents */
200: {
content: {
"application/json": components["schemas"]["AgentListResponse"];
};
};
/** @description Authentication required */
401: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
get_v1_agent: {
parameters: {
query?: {
/** @description When true, include version/path/configOptions (slower) */
config?: boolean | null;
/** @description When true, bypass version cache */
no_cache?: boolean | null;
};
path: {
/** @description Agent id */
agent: string;
};
};
responses: {
/** @description Agent info */
200: {
content: {
"application/json": components["schemas"]["AgentInfo"];
};
};
/** @description Unknown agent */
400: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Authentication required */
401: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
post_v1_agent_install: {
parameters: {
path: {
/** @description Agent id */
agent: string;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["AgentInstallRequest"];
};
};
responses: {
/** @description Agent install result */
200: {
content: {
"application/json": components["schemas"]["AgentInstallResponse"];
};
};
/** @description Invalid request */
400: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
/** @description Unknown ACP client */
/** @description Install failed */
500: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
get_v1_config_mcp: {
parameters: {
query: {
/** @description Target directory */
directory: string;
/** @description MCP entry name */
mcpName: string;
};
};
responses: {
/** @description MCP entry */
200: {
content: {
"application/json": components["schemas"]["McpServerConfig"];
};
};
/** @description Entry not found */
404: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
@ -408,4 +501,251 @@ export interface operations {
};
};
};
put_v1_config_mcp: {
parameters: {
query: {
/** @description Target directory */
directory: string;
/** @description MCP entry name */
mcpName: string;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["McpServerConfig"];
};
};
responses: {
/** @description Stored */
204: {
content: never;
};
};
};
delete_v1_config_mcp: {
parameters: {
query: {
/** @description Target directory */
directory: string;
/** @description MCP entry name */
mcpName: string;
};
};
responses: {
/** @description Deleted */
204: {
content: never;
};
};
};
get_v1_config_skills: {
parameters: {
query: {
/** @description Target directory */
directory: string;
/** @description Skill entry name */
skillName: string;
};
};
responses: {
/** @description Skills entry */
200: {
content: {
"application/json": components["schemas"]["SkillsConfig"];
};
};
/** @description Entry not found */
404: {
content: {
"application/json": components["schemas"]["ProblemDetails"];
};
};
};
};
put_v1_config_skills: {
parameters: {
query: {
/** @description Target directory */
directory: string;
/** @description Skill entry name */
skillName: string;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["SkillsConfig"];
};
};
responses: {
/** @description Stored */
204: {
content: never;
};
};
};
delete_v1_config_skills: {
parameters: {
query: {
/** @description Target directory */
directory: string;
/** @description Skill entry name */
skillName: string;
};
};
responses: {
/** @description Deleted */
204: {
content: never;
};
};
};
get_v1_fs_entries: {
parameters: {
query?: {
/** @description Directory path */
path?: string | null;
};
};
responses: {
/** @description Directory entries */
200: {
content: {
"application/json": components["schemas"]["FsEntry"][];
};
};
};
};
delete_v1_fs_entry: {
parameters: {
query: {
/** @description File or directory path */
path: string;
/** @description Delete directory recursively */
recursive?: boolean | null;
};
};
responses: {
/** @description Delete result */
200: {
content: {
"application/json": components["schemas"]["FsActionResponse"];
};
};
};
};
get_v1_fs_file: {
parameters: {
query: {
/** @description File path */
path: string;
};
};
responses: {
/** @description File content */
200: {
content: never;
};
};
};
put_v1_fs_file: {
parameters: {
query: {
/** @description File path */
path: string;
};
};
/** @description Raw file bytes */
requestBody: {
content: {
"text/plain": string;
};
};
responses: {
/** @description Write result */
200: {
content: {
"application/json": components["schemas"]["FsWriteResponse"];
};
};
};
};
post_v1_fs_mkdir: {
parameters: {
query: {
/** @description Directory path */
path: string;
};
};
responses: {
/** @description Directory created */
200: {
content: {
"application/json": components["schemas"]["FsActionResponse"];
};
};
};
};
post_v1_fs_move: {
requestBody: {
content: {
"application/json": components["schemas"]["FsMoveRequest"];
};
};
responses: {
/** @description Move result */
200: {
content: {
"application/json": components["schemas"]["FsMoveResponse"];
};
};
};
};
get_v1_fs_stat: {
parameters: {
query: {
/** @description Path to stat */
path: string;
};
};
responses: {
/** @description Path metadata */
200: {
content: {
"application/json": components["schemas"]["FsStat"];
};
};
};
};
post_v1_fs_upload_batch: {
parameters: {
query?: {
/** @description Destination path */
path?: string | null;
};
};
/** @description tar archive body */
requestBody: {
content: {
"text/plain": string;
};
};
responses: {
/** @description Upload/extract result */
200: {
content: {
"application/json": components["schemas"]["FsUploadBatchResponse"];
};
};
};
};
get_v1_health: {
responses: {
/** @description Service health response */
200: {
content: {
"application/json": components["schemas"]["HealthResponse"];
};
};
};
};
}

View file

@ -1,45 +1,61 @@
export {
AlreadyConnectedError,
NotConnectedError,
LiveAcpConnection,
SandboxAgent,
SandboxAgentClient,
SandboxAgentError,
Session,
} from "./client.ts";
export { AcpRpcError } from "acp-http-client";
export { buildInspectorUrl } from "./inspector.ts";
export type {
AgentEvent,
AgentUnparsedNotification,
ListModelsResponse,
PermissionRequest,
PermissionResponse,
SandboxAgentClientConnectOptions,
SandboxAgentClientOptions,
SandboxAgentConnectOptions,
SandboxAgentEventObserver,
SandboxAgentStartOptions,
SandboxMetadata,
SessionCreateRequest,
SessionModelInfo,
SessionUpdateNotification,
SessionResumeOrCreateRequest,
SessionSendOptions,
SessionEventListener,
} from "./client.ts";
export type {
InspectorUrlOptions,
} from "./inspector.ts";
export type { InspectorUrlOptions } from "./inspector.ts";
export {
InMemorySessionPersistDriver,
} from "./types.ts";
export type {
AgentCapabilities,
AcpEnvelope,
AcpServerInfo,
AcpServerListResponse,
AgentInfo,
AgentInstallArtifact,
AgentInstallRequest,
AgentInstallResponse,
AgentListResponse,
FsActionResponse,
FsDeleteQuery,
FsEntriesQuery,
FsEntry,
FsMoveRequest,
FsMoveResponse,
FsPathQuery,
FsStat,
FsUploadBatchQuery,
FsUploadBatchResponse,
FsWriteResponse,
HealthResponse,
InMemorySessionPersistDriverOptions,
ListEventsRequest,
ListPage,
ListPageRequest,
McpConfigQuery,
McpServerConfig,
ProblemDetails,
SessionInfo,
SessionListResponse,
SessionTerminateResponse,
SessionEvent,
SessionPersistDriver,
SessionRecord,
SkillsConfig,
SkillsConfigQuery,
} from "./types.ts";
export type {

View file

@ -207,7 +207,7 @@ async function waitForHealth(
throw new Error("sandbox-agent exited before becoming healthy.");
}
try {
const response = await fetcher(`${baseUrl}/v2/health`, {
const response = await fetcher(`${baseUrl}/v1/health`, {
headers: { Authorization: `Bearer ${token}` },
});
if (response.ok) {

View file

@ -1,282 +1,237 @@
export interface ProblemDetails {
type: string;
title: string;
status: number;
detail?: string;
instance?: string;
[key: string]: unknown;
}
import type { AnyMessage, NewSessionRequest } from "acp-http-client";
import type { components, operations } from "./generated/openapi.ts";
export type HealthStatus = "healthy" | "degraded" | "unhealthy" | "ok";
export type ProblemDetails = components["schemas"]["ProblemDetails"];
export interface AgentHealthInfo {
export type HealthResponse = JsonResponse<operations["get_v1_health"], 200>;
export type AgentListResponse = JsonResponse<operations["get_v1_agents"], 200>;
export type AgentInfo = components["schemas"]["AgentInfo"];
export type AgentInstallRequest = JsonRequestBody<operations["post_v1_agent_install"]>;
export type AgentInstallResponse = JsonResponse<operations["post_v1_agent_install"], 200>;
export type AcpEnvelope = components["schemas"]["AcpEnvelope"];
export type AcpServerInfo = components["schemas"]["AcpServerInfo"];
export type AcpServerListResponse = JsonResponse<operations["get_v1_acp_servers"], 200>;
export type FsEntriesQuery = QueryParams<operations["get_v1_fs_entries"]>;
export type FsEntry = components["schemas"]["FsEntry"];
export type FsPathQuery = QueryParams<operations["get_v1_fs_file"]>;
export type FsDeleteQuery = QueryParams<operations["delete_v1_fs_entry"]>;
export type FsUploadBatchQuery = QueryParams<operations["post_v1_fs_upload_batch"]>;
export type FsWriteResponse = JsonResponse<operations["put_v1_fs_file"], 200>;
export type FsActionResponse = JsonResponse<operations["delete_v1_fs_entry"], 200>;
export type FsMoveRequest = JsonRequestBody<operations["post_v1_fs_move"]>;
export type FsMoveResponse = JsonResponse<operations["post_v1_fs_move"], 200>;
export type FsStat = JsonResponse<operations["get_v1_fs_stat"], 200>;
export type FsUploadBatchResponse = JsonResponse<operations["post_v1_fs_upload_batch"], 200>;
export type McpConfigQuery = QueryParams<operations["get_v1_config_mcp"]>;
export type McpServerConfig = components["schemas"]["McpServerConfig"];
export type SkillsConfigQuery = QueryParams<operations["get_v1_config_skills"]>;
export type SkillsConfig = components["schemas"]["SkillsConfig"];
export interface SessionRecord {
id: string;
agent: string;
installed: boolean;
running: boolean;
[key: string]: unknown;
agentSessionId: string;
lastConnectionId: string;
createdAt: number;
destroyedAt?: number;
sessionInit?: Omit<NewSessionRequest, "_meta">;
}
export interface HealthResponse {
status: HealthStatus | string;
version: string;
uptime_ms: number;
agents: AgentHealthInfo[];
// Backward-compatible field from earlier v2 payloads.
api_version?: string;
[key: string]: unknown;
}
export type SessionEventSender = "client" | "agent";
export type ServerStatus = "running" | "stopped" | "error";
export interface ServerStatusInfo {
status: ServerStatus | string;
base_url?: string | null;
baseUrl?: string | null;
uptime_ms?: number | null;
uptimeMs?: number | null;
restart_count?: number;
restartCount?: number;
last_error?: string | null;
lastError?: string | null;
[key: string]: unknown;
}
export interface AgentModelInfo {
id?: string;
model_id?: string;
modelId?: string;
name?: string | null;
description?: string | null;
default_variant?: string | null;
defaultVariant?: string | null;
variants?: string[] | null;
[key: string]: unknown;
}
export interface AgentModeInfo {
export interface SessionEvent {
// Stable unique event id. For ordering, sort by (sessionId, eventIndex).
id: string;
name: string;
description: string;
[key: string]: unknown;
eventIndex: number;
sessionId: string;
createdAt: number;
connectionId: string;
sender: SessionEventSender;
payload: AnyMessage;
}
export interface AgentCapabilities {
plan_mode?: boolean;
permissions?: boolean;
questions?: boolean;
tool_calls?: boolean;
tool_results?: boolean;
text_messages?: boolean;
images?: boolean;
file_attachments?: boolean;
session_lifecycle?: boolean;
error_events?: boolean;
reasoning?: boolean;
status?: boolean;
command_execution?: boolean;
file_changes?: boolean;
mcp_tools?: boolean;
streaming_deltas?: boolean;
item_started?: boolean;
shared_process?: boolean;
unstable_methods?: boolean;
[key: string]: unknown;
export interface ListPageRequest {
cursor?: string;
limit?: number;
}
export interface AgentInfo {
id: string;
installed?: boolean;
credentials_available?: boolean;
native_required?: boolean;
native_installed?: boolean;
native_version?: string | null;
agent_process_installed?: boolean;
agent_process_source?: string | null;
agent_process_version?: string | null;
version?: string | null;
path?: string | null;
server_status?: ServerStatusInfo | null;
models?: AgentModelInfo[] | null;
default_model?: string | null;
modes?: AgentModeInfo[] | null;
capabilities: AgentCapabilities;
[key: string]: unknown;
export interface ListPage<T> {
items: T[];
nextCursor?: string;
}
export interface AgentListResponse {
agents: AgentInfo[];
export interface ListEventsRequest extends ListPageRequest {
sessionId: string;
}
export interface AgentInstallRequest {
reinstall?: boolean;
agentVersion?: string;
agentProcessVersion?: string;
export interface SessionPersistDriver {
getSession(id: string): Promise<SessionRecord | null>;
listSessions(request?: ListPageRequest): Promise<ListPage<SessionRecord>>;
updateSession(session: SessionRecord): Promise<void>;
listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>>;
insertEvent(event: SessionEvent): Promise<void>;
}
export interface AgentInstallArtifact {
kind: string;
path: string;
source: string;
version?: string | null;
export interface InMemorySessionPersistDriverOptions {
maxSessions?: number;
maxEventsPerSession?: number;
}
export interface AgentInstallResponse {
already_installed: boolean;
artifacts: AgentInstallArtifact[];
const DEFAULT_MAX_SESSIONS = 1024;
const DEFAULT_MAX_EVENTS_PER_SESSION = 500;
const DEFAULT_LIST_LIMIT = 100;
export class InMemorySessionPersistDriver implements SessionPersistDriver {
private readonly maxSessions: number;
private readonly maxEventsPerSession: number;
private readonly sessions = new Map<string, SessionRecord>();
private readonly eventsBySession = new Map<string, SessionEvent[]>();
constructor(options: InMemorySessionPersistDriverOptions = {}) {
this.maxSessions = normalizeCap(options.maxSessions, DEFAULT_MAX_SESSIONS);
this.maxEventsPerSession = normalizeCap(
options.maxEventsPerSession,
DEFAULT_MAX_EVENTS_PER_SESSION,
);
}
async getSession(id: string): Promise<SessionRecord | null> {
const session = this.sessions.get(id);
return session ? cloneSessionRecord(session) : null;
}
async listSessions(request: ListPageRequest = {}): Promise<ListPage<SessionRecord>> {
const sorted = [...this.sessions.values()].sort((a, b) => {
if (a.createdAt !== b.createdAt) {
return a.createdAt - b.createdAt;
}
return a.id.localeCompare(b.id);
});
const page = paginate(sorted, request);
return {
items: page.items.map(cloneSessionRecord),
nextCursor: page.nextCursor,
};
}
async updateSession(session: SessionRecord): Promise<void> {
this.sessions.set(session.id, { ...session });
if (!this.eventsBySession.has(session.id)) {
this.eventsBySession.set(session.id, []);
}
if (this.sessions.size <= this.maxSessions) {
return;
}
const overflow = this.sessions.size - this.maxSessions;
const removable = [...this.sessions.values()]
.sort((a, b) => {
if (a.createdAt !== b.createdAt) {
return a.createdAt - b.createdAt;
}
return a.id.localeCompare(b.id);
})
.slice(0, overflow)
.map((sessionToRemove) => sessionToRemove.id);
for (const sessionId of removable) {
this.sessions.delete(sessionId);
this.eventsBySession.delete(sessionId);
}
}
async listEvents(request: ListEventsRequest): Promise<ListPage<SessionEvent>> {
const all = [...(this.eventsBySession.get(request.sessionId) ?? [])].sort((a, b) => {
if (a.eventIndex !== b.eventIndex) {
return a.eventIndex - b.eventIndex;
}
return a.id.localeCompare(b.id);
});
const page = paginate(all, request);
return {
items: page.items.map(cloneSessionEvent),
nextCursor: page.nextCursor,
};
}
async insertEvent(event: SessionEvent): Promise<void> {
const events = this.eventsBySession.get(event.sessionId) ?? [];
events.push(cloneSessionEvent(event));
if (events.length > this.maxEventsPerSession) {
events.splice(0, events.length - this.maxEventsPerSession);
}
this.eventsBySession.set(event.sessionId, events);
}
}
export type SessionEndReason = "completed" | "error" | "terminated";
export type TerminatedBy = "agent" | "daemon";
export interface StderrOutput {
head?: string | null;
tail?: string | null;
truncated: boolean;
total_lines?: number | null;
function cloneSessionRecord(session: SessionRecord): SessionRecord {
return {
...session,
sessionInit: session.sessionInit
? (JSON.parse(JSON.stringify(session.sessionInit)) as SessionRecord["sessionInit"])
: undefined,
};
}
export interface SessionTerminationInfo {
reason: SessionEndReason | string;
terminated_by: TerminatedBy | string;
message?: string | null;
exit_code?: number | null;
stderr?: StderrOutput | null;
[key: string]: unknown;
function cloneSessionEvent(event: SessionEvent): SessionEvent {
return {
...event,
payload: JSON.parse(JSON.stringify(event.payload)) as AnyMessage,
};
}
export interface SessionInfo {
session_id: string;
sessionId?: string;
agent?: string;
cwd?: string;
title?: string | null;
ended?: boolean;
created_at?: string | number | null;
createdAt?: string | number | null;
updated_at?: string | number | null;
updatedAt?: string | number | null;
model?: string | null;
metadata?: Record<string, unknown> | null;
agent_mode?: string;
agentMode?: string;
permission_mode?: string;
permissionMode?: string;
native_session_id?: string | null;
nativeSessionId?: string | null;
event_count?: number;
eventCount?: number;
directory?: string | null;
variant?: string | null;
mcp?: Record<string, unknown> | null;
skills?: Record<string, unknown> | null;
termination_info?: SessionTerminationInfo | null;
terminationInfo?: SessionTerminationInfo | null;
[key: string]: unknown;
type ResponsesOf<T> = T extends { responses: infer R } ? R : never;
type JsonResponse<T, StatusCode extends keyof ResponsesOf<T>> = ResponsesOf<T>[StatusCode] extends {
content: { "application/json": infer B };
}
? B
: never;
type JsonRequestBody<T> = T extends {
requestBody: { content: { "application/json": infer B } };
}
? B
: 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) {
return fallback;
}
return Math.floor(value as number);
}
export interface SessionListResponse {
sessions: SessionInfo[];
function paginate<T>(items: T[], request: ListPageRequest): ListPage<T> {
const offset = parseCursor(request.cursor);
const limit = normalizeCap(request.limit, DEFAULT_LIST_LIMIT);
const slice = items.slice(offset, offset + limit);
const nextOffset = offset + slice.length;
return {
items: slice,
nextCursor: nextOffset < items.length ? String(nextOffset) : undefined,
};
}
export interface SessionTerminateResponse {
terminated?: boolean;
reason?: SessionEndReason | string;
terminated_by?: TerminatedBy | string;
terminatedBy?: TerminatedBy | string;
[key: string]: unknown;
}
export interface SessionEndedParams {
session_id?: string;
sessionId?: string;
data?: SessionTerminationInfo;
reason?: SessionEndReason | string;
terminated_by?: TerminatedBy | string;
terminatedBy?: TerminatedBy | string;
message?: string | null;
exit_code?: number | null;
stderr?: StderrOutput | null;
[key: string]: unknown;
}
export interface SessionEndedNotification {
jsonrpc: "2.0";
method: "_sandboxagent/session/ended";
params: SessionEndedParams;
[key: string]: unknown;
}
export interface FsPathQuery {
path: string;
session_id?: string | null;
sessionId?: string | null;
}
export interface FsEntriesQuery {
path?: string | null;
session_id?: string | null;
sessionId?: string | null;
}
export interface FsSessionQuery {
session_id?: string | null;
sessionId?: string | null;
}
export interface FsDeleteQuery {
path: string;
recursive?: boolean | null;
session_id?: string | null;
sessionId?: string | null;
}
export interface FsUploadBatchQuery {
path?: string | null;
session_id?: string | null;
sessionId?: string | null;
}
export type FsEntryType = "file" | "directory";
export interface FsEntry {
name: string;
path: string;
size: number;
entry_type?: FsEntryType;
entryType?: FsEntryType;
modified?: string | null;
}
export interface FsStat {
path: string;
size: number;
entry_type?: FsEntryType;
entryType?: FsEntryType;
modified?: string | null;
}
export interface FsWriteResponse {
path: string;
bytes_written?: number;
bytesWritten?: number;
}
export interface FsMoveRequest {
from: string;
to: string;
overwrite?: boolean | null;
}
export interface FsMoveResponse {
from: string;
to: string;
}
export interface FsActionResponse {
path: string;
}
export interface FsUploadBatchResponse {
paths: string[];
truncated: boolean;
function parseCursor(cursor: string | undefined): number {
if (!cursor) {
return 0;
}
const parsed = Number.parseInt(cursor, 10);
if (!Number.isFinite(parsed) || parsed < 0) {
return 0;
}
return parsed;
}

View file

@ -0,0 +1,140 @@
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
export function prepareMockAgentDataHome(dataHome: string): void {
const installDir = join(dataHome, "sandbox-agent", "bin");
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 scriptFile = process.platform === "win32"
? join(processDir, "mock-acp.js")
: runner;
const nodeScript = String.raw`#!/usr/bin/env node
const { createInterface } = require("node:readline");
let nextSession = 0;
function emit(value) {
process.stdout.write(JSON.stringify(value) + "\n");
}
function firstText(prompt) {
if (!Array.isArray(prompt)) {
return "";
}
for (const block of prompt) {
if (block && block.type === "text" && typeof block.text === "string") {
return block.text;
}
}
return "";
}
const rl = createInterface({
input: process.stdin,
crlfDelay: Infinity,
});
rl.on("line", (line) => {
let msg;
try {
msg = JSON.parse(line);
} catch {
return;
}
const hasMethod = typeof msg?.method === "string";
const hasId = Object.prototype.hasOwnProperty.call(msg, "id");
const method = hasMethod ? msg.method : undefined;
if (method === "session/prompt") {
const sessionId = typeof msg?.params?.sessionId === "string" ? msg.params.sessionId : "";
const text = firstText(msg?.params?.prompt);
emit({
jsonrpc: "2.0",
method: "session/update",
params: {
sessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "mock: " + text,
},
},
},
});
}
if (!hasMethod || !hasId) {
return;
}
if (method === "initialize") {
emit({
jsonrpc: "2.0",
id: msg.id,
result: {
protocolVersion: 1,
capabilities: {},
serverInfo: {
name: "mock-acp-agent",
version: "0.0.1",
},
},
});
return;
}
if (method === "session/new") {
nextSession += 1;
emit({
jsonrpc: "2.0",
id: msg.id,
result: {
sessionId: "mock-session-" + nextSession,
},
});
return;
}
if (method === "session/prompt") {
emit({
jsonrpc: "2.0",
id: msg.id,
result: {
stopReason: "end_turn",
},
});
return;
}
emit({
jsonrpc: "2.0",
id: msg.id,
result: {
ok: true,
echoedMethod: method,
},
});
});
`;
writeFileSync(scriptFile, nodeScript);
if (process.platform === "win32") {
writeFileSync(runner, `@echo off\r\nnode "${scriptFile}" %*\r\n`);
}
chmodSync(scriptFile, 0o755);
if (process.platform === "win32") {
chmodSync(runner, 0o755);
}
}

View file

@ -1,18 +1,19 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { existsSync } from "node:fs";
import { mkdtempSync, rmSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { tmpdir } from "node:os";
import {
AlreadyConnectedError,
NotConnectedError,
InMemorySessionPersistDriver,
SandboxAgent,
SandboxAgentClient,
type AgentEvent,
type SessionEvent,
} from "../src/index.ts";
import { spawnSandboxAgent, isNodeRuntime, type SandboxAgentSpawnHandle } from "../src/spawn.ts";
import { prepareMockAgentDataHome } from "./helpers/mock-agent.ts";
const __dirname = dirname(fileURLToPath(import.meta.url));
const AGENT_UNPARSED_METHOD = "_sandboxagent/agent/unparsed";
function findBinary(): string | null {
if (process.env.SANDBOX_AGENT_BIN) {
@ -49,8 +50,8 @@ function sleep(ms: number): Promise<void> {
async function waitFor<T>(
fn: () => T | undefined | null,
timeoutMs = 5000,
stepMs = 25,
timeoutMs = 6000,
stepMs = 30,
): Promise<T> {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
@ -63,16 +64,23 @@ async function waitFor<T>(
throw new Error("timed out waiting for condition");
}
describe("Integration: TypeScript SDK against real server/runtime", () => {
describe("Integration: TypeScript SDK flat session API", () => {
let handle: SandboxAgentSpawnHandle;
let baseUrl: string;
let token: string;
let dataHome: string;
beforeAll(async () => {
dataHome = mkdtempSync(join(tmpdir(), "sdk-integration-"));
prepareMockAgentDataHome(dataHome);
handle = await spawnSandboxAgent({
enabled: true,
log: "silent",
timeoutMs: 30000,
env: {
XDG_DATA_HOME: dataHome,
},
});
baseUrl = handle.baseUrl;
token = handle.token;
@ -80,246 +88,197 @@ describe("Integration: TypeScript SDK against real server/runtime", () => {
afterAll(async () => {
await handle.dispose();
rmSync(dataHome, { recursive: true, force: true });
});
it("detects Node.js runtime", () => {
expect(isNodeRuntime()).toBe(true);
});
it("keeps health on HTTP and requires ACP connection for ACP-backed helpers", async () => {
const client = await SandboxAgent.connect({
it("creates a session, sends prompt, and persists events", async () => {
const sdk = await SandboxAgent.connect({
baseUrl,
token,
agent: "mock",
autoConnect: false,
});
const health = await client.getHealth();
expect(health.status).toBe("ok");
const session = await sdk.createSession({ agent: "mock" });
await expect(client.listAgents()).rejects.toBeInstanceOf(NotConnectedError);
await client.connect();
const agents = await client.listAgents();
expect(Array.isArray(agents.agents)).toBe(true);
expect(agents.agents.length).toBeGreaterThan(0);
await client.disconnect();
});
it("auto-connects on constructor and runs initialize/new/prompt flow", async () => {
const events: AgentEvent[] = [];
const client = new SandboxAgentClient({
baseUrl,
token,
agent: "mock",
onEvent: (event) => {
events.push(event);
},
const observed: SessionEvent[] = [];
const off = session.onEvent((event) => {
observed.push(event);
});
const session = await client.newSession({
cwd: process.cwd(),
mcpServers: [],
metadata: {
agent: "mock",
},
});
expect(session.sessionId).toBeTruthy();
const prompt = await client.prompt({
sessionId: session.sessionId,
prompt: [{ type: "text", text: "hello integration" }],
});
const prompt = await session.prompt([{ type: "text", text: "hello flat sdk" }]);
expect(prompt.stopReason).toBe("end_turn");
await waitFor(() => {
const text = events
.filter((event): event is Extract<AgentEvent, { type: "sessionUpdate" }> => {
return event.type === "sessionUpdate";
})
.map((event) => event.notification)
.filter((entry) => entry.update.sessionUpdate === "agent_message_chunk")
.map((entry) => entry.update.content)
.filter((content) => content.type === "text")
.map((content) => content.text)
.join("");
return text.includes("mock: hello integration") ? text : undefined;
const inbound = observed.find((event) => event.sender === "agent");
return inbound;
});
await client.disconnect();
});
const listed = await sdk.listSessions({ limit: 20 });
expect(listed.items.some((entry) => entry.id === session.id)).toBe(true);
it("enforces manual connect and disconnect lifecycle when autoConnect is disabled", async () => {
const client = new SandboxAgentClient({
baseUrl,
token,
agent: "mock",
autoConnect: false,
});
const fetched = await sdk.getSession(session.id);
expect(fetched?.agent).toBe("mock");
await expect(
client.newSession({
cwd: process.cwd(),
mcpServers: [],
metadata: {
agent: "mock",
},
}),
).rejects.toBeInstanceOf(NotConnectedError);
const events = await sdk.getEvents({ sessionId: session.id, limit: 100 });
expect(events.items.length).toBeGreaterThan(0);
expect(events.items.some((event) => event.sender === "client")).toBe(true);
expect(events.items.some((event) => event.sender === "agent")).toBe(true);
expect(events.items.every((event) => typeof event.id === "string")).toBe(true);
expect(events.items.every((event) => Number.isInteger(event.eventIndex))).toBe(true);
await client.connect();
const session = await client.newSession({
cwd: process.cwd(),
mcpServers: [],
metadata: {
agent: "mock",
},
});
expect(session.sessionId).toBeTruthy();
await client.disconnect();
await expect(
client.prompt({
sessionId: session.sessionId,
prompt: [{ type: "text", text: "after disconnect" }],
}),
).rejects.toBeInstanceOf(NotConnectedError);
});
it("rejects duplicate connect calls for a single client instance", async () => {
const client = new SandboxAgentClient({
baseUrl,
token,
agent: "mock",
autoConnect: false,
});
await client.connect();
await expect(client.connect()).rejects.toBeInstanceOf(AlreadyConnectedError);
await client.disconnect();
});
it("injects metadata on newSession and extracts metadata from session/list", async () => {
const client = new SandboxAgentClient({
baseUrl,
token,
agent: "mock",
autoConnect: false,
});
await client.connect();
const session = await client.newSession({
cwd: process.cwd(),
mcpServers: [],
metadata: {
agent: "mock",
variant: "high",
},
});
await client.setMetadata(session.sessionId, {
title: "sdk title",
permissionMode: "ask",
model: "mock",
});
const listed = await client.unstableListSessions({});
const current = listed.sessions.find((entry) => entry.sessionId === session.sessionId) as
| (Record<string, unknown> & { metadata?: Record<string, unknown> })
| undefined;
expect(current).toBeTruthy();
expect(current?.title).toBe("sdk title");
const metadata =
(current?.metadata as Record<string, unknown> | undefined) ??
((current?._meta as Record<string, unknown> | undefined)?.["sandboxagent.dev"] as
| Record<string, unknown>
| undefined);
expect(metadata?.variant).toBe("high");
expect(metadata?.permissionMode).toBe("ask");
expect(metadata?.model).toBe("mock");
await client.disconnect();
});
it("converts _sandboxagent/session/ended into typed agent events", async () => {
const events: AgentEvent[] = [];
const client = new SandboxAgentClient({
baseUrl,
token,
agent: "mock",
autoConnect: false,
onEvent: (event) => {
events.push(event);
},
});
await client.connect();
const session = await client.newSession({
cwd: process.cwd(),
mcpServers: [],
metadata: {
agent: "mock",
},
});
await client.terminateSession(session.sessionId);
const ended = await waitFor(() => {
return events.find((event) => event.type === "sessionEnded");
});
expect(ended.type).toBe("sessionEnded");
if (ended.type === "sessionEnded") {
const endedSessionId =
ended.notification.params.sessionId ?? ended.notification.params.session_id;
expect(endedSessionId).toBe(session.sessionId);
for (let i = 1; i < events.items.length; i += 1) {
expect(events.items[i]!.eventIndex).toBeGreaterThanOrEqual(events.items[i - 1]!.eventIndex);
}
await client.disconnect();
off();
await sdk.dispose();
});
it("converts _sandboxagent/agent/unparsed notifications through the event adapter", async () => {
const events: AgentEvent[] = [];
const client = new SandboxAgentClient({
it("restores a session on stale connection by recreating and replaying history on first prompt", async () => {
const persist = new InMemorySessionPersistDriver({
maxEventsPerSession: 200,
});
const first = await SandboxAgent.connect({
baseUrl,
token,
autoConnect: false,
onEvent: (event) => {
events.push(event);
},
persist,
replayMaxEvents: 50,
replayMaxChars: 20_000,
});
(client as any).handleEnvelope(
const created = await first.createSession({ agent: "mock" });
await created.prompt([{ type: "text", text: "first run" }]);
const oldConnectionId = created.lastConnectionId;
await first.dispose();
const second = await SandboxAgent.connect({
baseUrl,
token,
persist,
replayMaxEvents: 50,
replayMaxChars: 20_000,
});
const restored = await second.resumeSession(created.id);
expect(restored.lastConnectionId).not.toBe(oldConnectionId);
await restored.prompt([{ type: "text", text: "second run" }]);
const events = await second.getEvents({ sessionId: restored.id, limit: 500 });
const replayInjected = events.items.find((event) => {
if (event.sender !== "client") {
return false;
}
const payload = event.payload as Record<string, unknown>;
const method = payload.method;
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")
);
});
expect(replayInjected).toBeTruthy();
await second.dispose();
});
it("enforces in-memory event cap to avoid leaks", async () => {
const persist = new InMemorySessionPersistDriver({
maxEventsPerSession: 8,
});
const sdk = await SandboxAgent.connect({
baseUrl,
token,
persist,
});
const session = await sdk.createSession({ agent: "mock" });
for (let i = 0; i < 20; i += 1) {
await session.prompt([{ type: "text", text: `event-cap-${i}` }]);
}
const events = await sdk.getEvents({ sessionId: session.id, limit: 200 });
expect(events.items.length).toBeLessThanOrEqual(8);
await sdk.dispose();
});
it("supports MCP and skills config HTTP helpers", async () => {
const sdk = await SandboxAgent.connect({
baseUrl,
token,
});
const directory = mkdtempSync(join(tmpdir(), "sdk-config-"));
const mcpConfig = {
type: "local" as const,
command: "node",
args: ["server.js"],
env: { LOG_LEVEL: "debug" },
};
await sdk.setMcpConfig(
{
jsonrpc: "2.0",
method: AGENT_UNPARSED_METHOD,
params: {
raw: "unexpected payload",
},
directory,
mcpName: "local-test",
},
"inbound",
mcpConfig,
);
const unparsed = events.find((event) => event.type === "agentUnparsed");
expect(unparsed?.type).toBe("agentUnparsed");
});
const loadedMcp = await sdk.getMcpConfig({
directory,
mcpName: "local-test",
});
expect(loadedMcp.type).toBe("local");
it("rejects invalid token on protected /v2 endpoints", async () => {
const client = new SandboxAgentClient({
baseUrl,
token: "invalid-token",
autoConnect: false,
await sdk.deleteMcpConfig({
directory,
mcpName: "local-test",
});
await expect(client.getHealth()).rejects.toThrow();
const skillsConfig = {
sources: [
{
type: "github",
source: "rivet-dev/skills",
skills: ["sandbox-agent"],
},
],
};
await sdk.setSkillsConfig(
{
directory,
skillName: "default",
},
skillsConfig,
);
const loadedSkills = await sdk.getSkillsConfig({
directory,
skillName: "default",
});
expect(Array.isArray(loadedSkills.sources)).toBe(true);
await sdk.deleteSkillsConfig({
directory,
skillName: "default",
});
await sdk.dispose();
rmSync(directory, { recursive: true, force: true });
});
});