@douglas-agent/sandbank-agent 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @douglas-agent/sandbank-agent
2
+
3
+ > Lightweight client for AI agents running inside [Sandbank](../../README.md) sandboxes.
4
+
5
+ Connects to the relay server via WebSocket, enabling messaging, shared context, and task completion signaling.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @douglas-agent/sandbank-agent
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Runs inside a sandbox. Connection parameters are read from `SANDBANK_*` environment variables injected by `createSession()`:
16
+
17
+ ```typescript
18
+ import { connect } from '@douglas-agent/sandbank-agent'
19
+
20
+ const session = await connect()
21
+
22
+ // Listen for messages
23
+ session.on('message', async (msg) => {
24
+ if (msg.type === 'task') {
25
+ const result = await doWork(msg.payload)
26
+ await session.send(msg.from, 'done', result)
27
+ }
28
+ })
29
+
30
+ // Shared context
31
+ const spec = await session.context.get('spec')
32
+ await session.context.set('output', { files: ['index.ts'] })
33
+
34
+ // Signal completion
35
+ await session.complete({ status: 'success', summary: 'Built 5 endpoints' })
36
+ session.close()
37
+ ```
38
+
39
+ ## CLI
40
+
41
+ Also available as a CLI tool for shell scripts inside sandboxes:
42
+
43
+ ```bash
44
+ sandbank-agent send <to> <type> [payload]
45
+ sandbank-agent recv [--wait 5000]
46
+ sandbank-agent context get <key>
47
+ sandbank-agent context set <key> <value>
48
+ sandbank-agent complete <status> <summary>
49
+ ```
50
+
51
+ ## Environment Variables
52
+
53
+ | Variable | Description |
54
+ |----------|-------------|
55
+ | `SANDBANK_WS_URL` | Relay WebSocket URL |
56
+ | `SANDBANK_SESSION_ID` | Session identifier |
57
+ | `SANDBANK_SANDBOX_NAME` | This agent's sandbox name |
58
+ | `SANDBANK_AUTH_TOKEN` | Authentication token |
59
+
60
+ ## License
61
+
62
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ import { sendMessage, recvMessages, contextGet, contextSet, contextDelete, contextKeys, complete, } from './http-client.js';
3
+ async function main() {
4
+ const args = process.argv.slice(2);
5
+ const command = args[0];
6
+ if (!command) {
7
+ printUsage();
8
+ process.exit(1);
9
+ }
10
+ switch (command) {
11
+ case 'send':
12
+ await handleSend(args.slice(1));
13
+ break;
14
+ case 'recv':
15
+ await handleRecv(args.slice(1));
16
+ break;
17
+ case 'context':
18
+ await handleContext(args.slice(1));
19
+ break;
20
+ case 'complete':
21
+ await handleComplete(args.slice(1));
22
+ break;
23
+ case 'help':
24
+ case '--help':
25
+ case '-h':
26
+ printUsage();
27
+ break;
28
+ default:
29
+ console.error(`Unknown command: ${command}`);
30
+ printUsage();
31
+ process.exit(1);
32
+ }
33
+ }
34
+ async function handleSend(args) {
35
+ // sandbank-agent send <to> <type> [payload] [--steer]
36
+ const steerIdx = args.indexOf('--steer');
37
+ const priority = steerIdx >= 0 ? 'steer' : 'normal';
38
+ const cleanArgs = args.filter((a) => a !== '--steer');
39
+ const to = cleanArgs[0];
40
+ const type = cleanArgs[1];
41
+ const payloadStr = cleanArgs[2];
42
+ if (!to || !type) {
43
+ console.error('Usage: sandbank-agent send <to> <type> [payload] [--steer]');
44
+ process.exit(1);
45
+ }
46
+ let payload = payloadStr ?? null;
47
+ if (typeof payloadStr === 'string') {
48
+ try {
49
+ payload = JSON.parse(payloadStr);
50
+ }
51
+ catch {
52
+ // keep as string
53
+ }
54
+ }
55
+ await sendMessage(to, type, payload, priority);
56
+ console.log('OK');
57
+ }
58
+ async function handleRecv(args) {
59
+ // sandbank-agent recv [--wait <seconds>] [--limit <n>]
60
+ let wait = 0;
61
+ let limit = 100;
62
+ for (let i = 0; i < args.length; i++) {
63
+ if (args[i] === '--wait' && args[i + 1]) {
64
+ wait = parseInt(args[i + 1], 10) * 1000; // seconds → ms
65
+ i++;
66
+ }
67
+ else if (args[i] === '--limit' && args[i + 1]) {
68
+ limit = parseInt(args[i + 1], 10);
69
+ i++;
70
+ }
71
+ }
72
+ const messages = await recvMessages(limit, wait);
73
+ console.log(JSON.stringify(messages, null, 2));
74
+ }
75
+ async function handleContext(args) {
76
+ const sub = args[0];
77
+ switch (sub) {
78
+ case 'get': {
79
+ const key = args[1];
80
+ if (!key) {
81
+ console.error('Usage: sandbank-agent context get <key>');
82
+ process.exit(1);
83
+ }
84
+ const value = await contextGet(key);
85
+ console.log(JSON.stringify(value, null, 2));
86
+ break;
87
+ }
88
+ case 'set': {
89
+ const key = args[1];
90
+ const valueStr = args[2];
91
+ if (!key || valueStr === undefined) {
92
+ console.error('Usage: sandbank-agent context set <key> <value>');
93
+ process.exit(1);
94
+ }
95
+ let value = valueStr;
96
+ try {
97
+ value = JSON.parse(valueStr);
98
+ }
99
+ catch {
100
+ // keep as string
101
+ }
102
+ await contextSet(key, value);
103
+ console.log('OK');
104
+ break;
105
+ }
106
+ case 'delete': {
107
+ const key = args[1];
108
+ if (!key) {
109
+ console.error('Usage: sandbank-agent context delete <key>');
110
+ process.exit(1);
111
+ }
112
+ await contextDelete(key);
113
+ console.log('OK');
114
+ break;
115
+ }
116
+ case 'keys': {
117
+ const keys = await contextKeys();
118
+ console.log(JSON.stringify(keys, null, 2));
119
+ break;
120
+ }
121
+ default:
122
+ console.error('Usage: sandbank-agent context <get|set|delete|keys> [args]');
123
+ process.exit(1);
124
+ }
125
+ }
126
+ async function handleComplete(args) {
127
+ // sandbank-agent complete --status <status> --summary <text>
128
+ let status = 'success';
129
+ let summary = '';
130
+ for (let i = 0; i < args.length; i++) {
131
+ if (args[i] === '--status' && args[i + 1]) {
132
+ status = args[i + 1];
133
+ i++;
134
+ }
135
+ else if (args[i] === '--summary' && args[i + 1]) {
136
+ summary = args[i + 1];
137
+ i++;
138
+ }
139
+ }
140
+ await complete(status, summary);
141
+ console.log('OK');
142
+ }
143
+ function printUsage() {
144
+ console.log(`sandbank-agent — Sandbank Agent CLI
145
+
146
+ Commands:
147
+ send <to> <type> [payload] [--steer] Send a message
148
+ recv [--wait <sec>] [--limit <n>] Receive messages
149
+ context get <key> Get context value
150
+ context set <key> <value> Set context value
151
+ context delete <key> Delete context key
152
+ context keys List all context keys
153
+ complete --status <s> --summary <text> Mark sandbox as complete
154
+
155
+ Environment variables:
156
+ SANDBANK_RELAY_URL Relay HTTP URL
157
+ SANDBANK_SESSION_ID Session ID
158
+ SANDBANK_SANDBOX_NAME This sandbox's name
159
+ SANDBANK_AUTH_TOKEN Auth token (required)`);
160
+ }
161
+ main().catch((err) => {
162
+ console.error(err.message);
163
+ process.exit(1);
164
+ });
@@ -0,0 +1,3 @@
1
+ import type { AgentSession, ConnectOptions } from './types.js';
2
+ export declare function connect(options?: ConnectOptions): Promise<AgentSession>;
3
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../src/connect.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAgB,cAAc,EAAE,MAAM,YAAY,CAAA;AAK5E,wBAAsB,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CA2HjF"}
@@ -0,0 +1,120 @@
1
+ import { createWebSocketTransport } from './transport.js';
2
+ import { createRequest, RpcPendingMap } from './rpc.js';
3
+ import { createWsContextClient } from './context-client.js';
4
+ export async function connect(options = {}) {
5
+ const wsUrl = options.wsUrl ?? process.env['SANDBANK_WS_URL'];
6
+ const sessionId = options.sessionId ?? process.env['SANDBANK_SESSION_ID'];
7
+ const sandboxName = options.sandboxName ?? process.env['SANDBANK_SANDBOX_NAME'];
8
+ const token = options.token ?? process.env['SANDBANK_AUTH_TOKEN'];
9
+ if (!wsUrl)
10
+ throw new Error('Missing wsUrl (set SANDBANK_WS_URL)');
11
+ if (!sessionId)
12
+ throw new Error('Missing sessionId (set SANDBANK_SESSION_ID)');
13
+ if (!sandboxName)
14
+ throw new Error('Missing sandboxName (set SANDBANK_SANDBOX_NAME)');
15
+ const transport = await createWebSocketTransport(wsUrl);
16
+ const pending = new RpcPendingMap();
17
+ const messageListeners = [];
18
+ // 路由所有 WebSocket 消息
19
+ transport.onMessage((data) => {
20
+ try {
21
+ const msg = JSON.parse(data);
22
+ // RPC 响应
23
+ if ('id' in msg && msg.id != null) {
24
+ pending.resolve(msg);
25
+ return;
26
+ }
27
+ // 通知:消息推送
28
+ if ('method' in msg && msg.method === 'message' && msg.params) {
29
+ const agentMsg = msg.params;
30
+ for (const fn of messageListeners) {
31
+ fn(agentMsg);
32
+ }
33
+ }
34
+ }
35
+ catch {
36
+ // ignore
37
+ }
38
+ });
39
+ // 认证
40
+ const authReq = createRequest('session.auth', {
41
+ sessionId,
42
+ sandboxName,
43
+ token: token ?? undefined,
44
+ role: 'agent',
45
+ });
46
+ const authPromise = pending.add(authReq.id);
47
+ transport.send(JSON.stringify(authReq));
48
+ let authResult;
49
+ try {
50
+ authResult = await authPromise;
51
+ }
52
+ catch (err) {
53
+ transport.close();
54
+ throw err;
55
+ }
56
+ if (!authResult.ok) {
57
+ transport.close();
58
+ throw new Error('Authentication failed');
59
+ }
60
+ const context = createWsContextClient(transport, pending);
61
+ const session = {
62
+ async send(to, type, payload, opts) {
63
+ const req = createRequest('message.send', {
64
+ to,
65
+ type,
66
+ payload: payload ?? null,
67
+ priority: opts?.priority ?? 'normal',
68
+ });
69
+ const promise = pending.add(req.id);
70
+ transport.send(JSON.stringify(req));
71
+ await promise;
72
+ },
73
+ async broadcast(type, payload, opts) {
74
+ const req = createRequest('message.broadcast', {
75
+ type,
76
+ payload: payload ?? null,
77
+ priority: opts?.priority ?? 'normal',
78
+ });
79
+ const promise = pending.add(req.id);
80
+ transport.send(JSON.stringify(req));
81
+ await promise;
82
+ },
83
+ async recv(opts) {
84
+ const req = createRequest('message.recv', {
85
+ limit: opts?.limit ?? 100,
86
+ wait: opts?.wait ?? 0,
87
+ });
88
+ const promise = pending.add(req.id);
89
+ transport.send(JSON.stringify(req));
90
+ const result = await promise;
91
+ return result.messages;
92
+ },
93
+ on(event, fn) {
94
+ if (event === 'message') {
95
+ messageListeners.push(fn);
96
+ return () => {
97
+ const idx = messageListeners.indexOf(fn);
98
+ if (idx >= 0)
99
+ messageListeners.splice(idx, 1);
100
+ };
101
+ }
102
+ return () => { };
103
+ },
104
+ context,
105
+ async complete(result) {
106
+ const req = createRequest('sandbox.complete', {
107
+ status: result.status,
108
+ summary: result.summary,
109
+ });
110
+ const promise = pending.add(req.id);
111
+ transport.send(JSON.stringify(req));
112
+ await promise;
113
+ },
114
+ close() {
115
+ pending.rejectAll('Connection closed');
116
+ transport.close();
117
+ },
118
+ };
119
+ return session;
120
+ }
@@ -0,0 +1,5 @@
1
+ import type { ContextStore, Transport } from '@douglas-agent/sandbank-core';
2
+ import { RpcPendingMap } from './rpc.js';
3
+ /** WebSocket 模式下的 ContextStore 代理 */
4
+ export declare function createWsContextClient(transport: Transport, pending: RpcPendingMap): ContextStore;
5
+ //# sourceMappingURL=context-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-client.d.ts","sourceRoot":"","sources":["../src/context-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAwC,MAAM,8BAA8B,CAAA;AACjH,OAAO,EAAiB,aAAa,EAAE,MAAM,UAAU,CAAA;AAEvD,qCAAqC;AACrC,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,GAAG,YAAY,CA+DhG"}
@@ -0,0 +1,63 @@
1
+ import { createRequest, RpcPendingMap } from './rpc.js';
2
+ /** WebSocket 模式下的 ContextStore 代理 */
3
+ export function createWsContextClient(transport, pending) {
4
+ const watchers = new Map();
5
+ const allWatchers = new Set();
6
+ // 监听 context.changed 通知
7
+ transport.onMessage((data) => {
8
+ try {
9
+ const msg = JSON.parse(data);
10
+ if ('method' in msg && msg.method === 'context.changed' && msg.params) {
11
+ const { key, value } = msg.params;
12
+ const fns = watchers.get(key);
13
+ if (fns) {
14
+ for (const fn of fns)
15
+ fn(value);
16
+ }
17
+ for (const fn of allWatchers)
18
+ fn(key, value);
19
+ }
20
+ }
21
+ catch {
22
+ // ignore non-JSON or irrelevant messages
23
+ }
24
+ });
25
+ return {
26
+ async get(key) {
27
+ const req = createRequest('context.get', { key });
28
+ const promise = pending.add(req.id);
29
+ transport.send(JSON.stringify(req));
30
+ const result = await promise;
31
+ return result.value ?? undefined;
32
+ },
33
+ async set(key, value) {
34
+ const req = createRequest('context.set', { key, value });
35
+ const promise = pending.add(req.id);
36
+ transport.send(JSON.stringify(req));
37
+ await promise;
38
+ },
39
+ async delete(key) {
40
+ const req = createRequest('context.delete', { key });
41
+ const promise = pending.add(req.id);
42
+ transport.send(JSON.stringify(req));
43
+ await promise;
44
+ },
45
+ async keys() {
46
+ const req = createRequest('context.keys');
47
+ const promise = pending.add(req.id);
48
+ transport.send(JSON.stringify(req));
49
+ const result = await promise;
50
+ return result.keys;
51
+ },
52
+ watch(key, fn) {
53
+ if (!watchers.has(key))
54
+ watchers.set(key, new Set());
55
+ watchers.get(key).add(fn);
56
+ return () => { watchers.get(key)?.delete(fn); };
57
+ },
58
+ watchAll(fn) {
59
+ allWatchers.add(fn);
60
+ return () => { allWatchers.delete(fn); };
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,14 @@
1
+ export interface HttpClientConfig {
2
+ relayUrl: string;
3
+ sessionId: string;
4
+ sandboxName: string;
5
+ token: string;
6
+ }
7
+ export declare function sendMessage(to: string, type: string, payload: unknown, priority?: 'normal' | 'steer'): Promise<void>;
8
+ export declare function recvMessages(limit?: number, waitMs?: number): Promise<unknown[]>;
9
+ export declare function contextGet(key: string): Promise<unknown>;
10
+ export declare function contextSet(key: string, value: unknown): Promise<void>;
11
+ export declare function contextDelete(key: string): Promise<void>;
12
+ export declare function contextKeys(): Promise<string[]>;
13
+ export declare function complete(status: string, summary: string): Promise<void>;
14
+ //# sourceMappingURL=http-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;CACd;AAsCD,wBAAsB,WAAW,CAC/B,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,QAAQ,GAAE,QAAQ,GAAG,OAAkB,GACtC,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,YAAY,CAChC,KAAK,SAAM,EACX,MAAM,SAAI,GACT,OAAO,CAAC,OAAO,EAAE,CAAC,CAGpB;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG9D;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3E;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAGrD;AAED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7E"}
@@ -0,0 +1,59 @@
1
+ import { createRequest } from './rpc.js';
2
+ function getConfig() {
3
+ const relayUrl = process.env['SANDBANK_RELAY_URL'];
4
+ const sessionId = process.env['SANDBANK_SESSION_ID'];
5
+ const sandboxName = process.env['SANDBANK_SANDBOX_NAME'];
6
+ const token = process.env['SANDBANK_AUTH_TOKEN'];
7
+ if (!relayUrl)
8
+ throw new Error('Missing SANDBANK_RELAY_URL');
9
+ if (!sessionId)
10
+ throw new Error('Missing SANDBANK_SESSION_ID');
11
+ if (!sandboxName)
12
+ throw new Error('Missing SANDBANK_SANDBOX_NAME');
13
+ if (!token)
14
+ throw new Error('Missing SANDBANK_AUTH_TOKEN');
15
+ return { relayUrl, sessionId, sandboxName, token };
16
+ }
17
+ async function rpcCall(method, params) {
18
+ const config = getConfig();
19
+ const request = createRequest(method, params);
20
+ const response = await fetch(`${config.relayUrl}/rpc`, {
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ 'X-Session-Id': config.sessionId,
25
+ 'X-Sandbox-Name': config.sandboxName,
26
+ 'X-Auth-Token': config.token,
27
+ },
28
+ body: JSON.stringify(request),
29
+ });
30
+ const result = await response.json();
31
+ if (result.error) {
32
+ throw new Error(`RPC error ${result.error.code}: ${result.error.message}`);
33
+ }
34
+ return result.result;
35
+ }
36
+ export async function sendMessage(to, type, payload, priority = 'normal') {
37
+ await rpcCall('message.send', { to, type, payload, priority });
38
+ }
39
+ export async function recvMessages(limit = 100, waitMs = 0) {
40
+ const result = await rpcCall('message.recv', { limit, wait: waitMs });
41
+ return result.messages;
42
+ }
43
+ export async function contextGet(key) {
44
+ const result = await rpcCall('context.get', { key });
45
+ return result.value ?? undefined;
46
+ }
47
+ export async function contextSet(key, value) {
48
+ await rpcCall('context.set', { key, value });
49
+ }
50
+ export async function contextDelete(key) {
51
+ await rpcCall('context.delete', { key });
52
+ }
53
+ export async function contextKeys() {
54
+ const result = await rpcCall('context.keys');
55
+ return result.keys;
56
+ }
57
+ export async function complete(status, summary) {
58
+ await rpcCall('sandbox.complete', { status, summary });
59
+ }
@@ -0,0 +1,3 @@
1
+ export { connect } from './connect.js';
2
+ export type { AgentSession, AgentMessage, ConnectOptions } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { connect } from './connect.js';
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { JsonRpcRequest, JsonRpcResponse } from '@douglas-agent/sandbank-core';
2
+ export declare function createRequest(method: string, params?: Record<string, unknown>): JsonRpcRequest;
3
+ /** 管理 RPC 请求的 pending promise */
4
+ export declare class RpcPendingMap {
5
+ private pending;
6
+ add(id: number | string, timeoutMs?: number): Promise<unknown>;
7
+ resolve(response: JsonRpcResponse): boolean;
8
+ rejectAll(reason: string): void;
9
+ }
10
+ //# sourceMappingURL=rpc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAEnF,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc,CAO9F;AAED,iCAAiC;AACjC,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAGX;IAEJ,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAa9D,OAAO,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO;IAc3C,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;CAMhC"}
package/dist/rpc.js ADDED
@@ -0,0 +1,45 @@
1
+ export function createRequest(method, params) {
2
+ return {
3
+ jsonrpc: '2.0',
4
+ id: crypto.randomUUID(),
5
+ method,
6
+ ...(params ? { params } : {}),
7
+ };
8
+ }
9
+ /** 管理 RPC 请求的 pending promise */
10
+ export class RpcPendingMap {
11
+ pending = new Map();
12
+ add(id, timeoutMs = 30_000) {
13
+ return new Promise((resolve, reject) => {
14
+ const timer = setTimeout(() => {
15
+ this.pending.delete(id);
16
+ reject(new Error(`RPC timeout (${timeoutMs}ms)`));
17
+ }, timeoutMs);
18
+ this.pending.set(id, {
19
+ resolve: (result) => { clearTimeout(timer); resolve(result); },
20
+ reject: (err) => { clearTimeout(timer); reject(err); },
21
+ });
22
+ });
23
+ }
24
+ resolve(response) {
25
+ if (response.id == null)
26
+ return false;
27
+ const entry = this.pending.get(response.id);
28
+ if (!entry)
29
+ return false;
30
+ this.pending.delete(response.id);
31
+ if (response.error) {
32
+ entry.reject(new Error(`RPC error ${response.error.code}: ${response.error.message}`));
33
+ }
34
+ else {
35
+ entry.resolve(response.result);
36
+ }
37
+ return true;
38
+ }
39
+ rejectAll(reason) {
40
+ for (const [, entry] of this.pending) {
41
+ entry.reject(new Error(reason));
42
+ }
43
+ this.pending.clear();
44
+ }
45
+ }
@@ -0,0 +1,4 @@
1
+ import type { Transport } from '@douglas-agent/sandbank-core';
2
+ /** 使用 Node 内置 WebSocket 的 Transport 实现 */
3
+ export declare function createWebSocketTransport(url: string): Promise<Transport>;
4
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAE7D,0CAA0C;AAC1C,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAgDxE"}
@@ -0,0 +1,45 @@
1
+ /** 使用 Node 内置 WebSocket 的 Transport 实现 */
2
+ export function createWebSocketTransport(url) {
3
+ return new Promise((resolve, reject) => {
4
+ const ws = new WebSocket(url);
5
+ const listeners = [];
6
+ let state = 'connecting';
7
+ ws.addEventListener('open', () => {
8
+ state = 'open';
9
+ resolve(transport);
10
+ });
11
+ ws.addEventListener('error', (evt) => {
12
+ if (state === 'connecting') {
13
+ reject(new Error(`WebSocket connection failed: ${url}`));
14
+ }
15
+ state = 'closed';
16
+ });
17
+ ws.addEventListener('message', (evt) => {
18
+ const data = typeof evt.data === 'string' ? evt.data : String(evt.data);
19
+ for (const fn of listeners) {
20
+ fn(data);
21
+ }
22
+ });
23
+ ws.addEventListener('close', () => {
24
+ state = 'closed';
25
+ });
26
+ const transport = {
27
+ send(data) {
28
+ if (state !== 'open') {
29
+ throw new Error('WebSocket is not open');
30
+ }
31
+ ws.send(data);
32
+ },
33
+ onMessage(fn) {
34
+ listeners.push(fn);
35
+ },
36
+ close() {
37
+ listeners.length = 0;
38
+ ws.close();
39
+ },
40
+ get readyState() {
41
+ return state;
42
+ },
43
+ };
44
+ });
45
+ }
@@ -0,0 +1,46 @@
1
+ import type { MessagePriority, ContextStore } from '@douglas-agent/sandbank-core';
2
+ export interface AgentMessage {
3
+ from: string;
4
+ to: string | null;
5
+ type: string;
6
+ payload: unknown;
7
+ priority: MessagePriority;
8
+ timestamp: string;
9
+ }
10
+ export interface AgentSession {
11
+ /** 发送消息给指定沙箱 */
12
+ send(to: string, type: string, payload?: unknown, options?: {
13
+ priority?: MessagePriority;
14
+ }): Promise<void>;
15
+ /** 广播消息给所有沙箱 */
16
+ broadcast(type: string, payload?: unknown, options?: {
17
+ priority?: MessagePriority;
18
+ }): Promise<void>;
19
+ /** 拉取待处理消息 */
20
+ recv(options?: {
21
+ limit?: number;
22
+ wait?: number;
23
+ }): Promise<AgentMessage[]>;
24
+ /** 监听实时消息(WebSocket 模式) */
25
+ on(event: 'message', fn: (msg: AgentMessage) => void): () => void;
26
+ /** 共享上下文 */
27
+ readonly context: ContextStore;
28
+ /** 标记当前沙箱完成 */
29
+ complete(result: {
30
+ status: string;
31
+ summary: string;
32
+ }): Promise<void>;
33
+ /** 关闭连接 */
34
+ close(): void;
35
+ }
36
+ export interface ConnectOptions {
37
+ /** Relay WebSocket URL(默认读 SANDBANK_WS_URL) */
38
+ wsUrl?: string;
39
+ /** Session ID(默认读 SANDBANK_SESSION_ID) */
40
+ sessionId?: string;
41
+ /** 沙箱名(默认读 SANDBANK_SANDBOX_NAME) */
42
+ sandboxName?: string;
43
+ /** 认证 token(默认读 SANDBANK_AUTH_TOKEN) */
44
+ token?: string;
45
+ }
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAEjF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,gBAAgB;IAChB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,eAAe,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE1G,gBAAgB;IAChB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,eAAe,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEnG,cAAc;IACd,IAAI,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAA;IAE1E,2BAA2B;IAC3B,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IAEjE,YAAY;IACZ,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAE9B,eAAe;IACf,QAAQ,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEpE,WAAW;IACX,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@douglas-agent/sandbank-agent",
3
+ "version": "0.2.0",
4
+ "description": "Lightweight client for AI agents running inside Sandbank sandboxes",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "homepage": "https://sandbank.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/Xeonice/sandbank-douglas-agent.git",
11
+ "directory": "packages/agent"
12
+ },
13
+ "keywords": [
14
+ "sandbox",
15
+ "ai-agent",
16
+ "client",
17
+ "cli"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "bin": {
26
+ "sandbank-agent": "./dist/cli.js"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "typecheck": "tsc --noEmit",
34
+ "clean": "rm -rf dist"
35
+ },
36
+ "peerDependencies": {
37
+ "@douglas-agent/sandbank-core": "^0.3.6"
38
+ },
39
+ "devDependencies": {
40
+ "@douglas-agent/sandbank-core": "^0.3.6",
41
+ "@douglas-agent/sandbank-relay": "^0.2.1",
42
+ "@types/node": "^22.15.0",
43
+ "typescript": "^5.7.3",
44
+ "ws": "^8.18.0",
45
+ "@types/ws": "^8.18.0"
46
+ }
47
+ }