mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-19 01:04:32 +00:00
feat: acp http adapter
This commit is contained in:
parent
2ba630c180
commit
b4c8564cb2
217 changed files with 18785 additions and 17400 deletions
|
|
@ -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": {
|
||||
|
|
|
|||
17
sdks/typescript/scripts/patch-openapi-types.mjs
Normal file
17
sdks/typescript/scripts/patch-openapi-types.mjs
Normal 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
|
|
@ -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"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
140
sdks/typescript/tests/helpers/mock-agent.ts
Normal file
140
sdks/typescript/tests/helpers/mock-agent.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue