@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 +62 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +164 -0
- package/dist/connect.d.ts +3 -0
- package/dist/connect.d.ts.map +1 -0
- package/dist/connect.js +120 -0
- package/dist/context-client.d.ts +5 -0
- package/dist/context-client.d.ts.map +1 -0
- package/dist/context-client.js +63 -0
- package/dist/http-client.d.ts +14 -0
- package/dist/http-client.d.ts.map +1 -0
- package/dist/http-client.js +59 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/rpc.d.ts +10 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +45 -0
- package/dist/transport.d.ts +4 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +45 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +47 -0
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 @@
|
|
|
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 @@
|
|
|
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"}
|
package/dist/connect.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|