sandbox-agent 0.1.11 → 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/dist/index.js CHANGED
@@ -1,5 +1,129 @@
1
+ // src/client.ts
2
+ import {
3
+ AcpHttpClient,
4
+ PROTOCOL_VERSION
5
+ } from "acp-http-client";
6
+
7
+ // src/types.ts
8
+ var DEFAULT_MAX_SESSIONS = 1024;
9
+ var DEFAULT_MAX_EVENTS_PER_SESSION = 500;
10
+ var DEFAULT_LIST_LIMIT = 100;
11
+ var InMemorySessionPersistDriver = class {
12
+ maxSessions;
13
+ maxEventsPerSession;
14
+ sessions = /* @__PURE__ */ new Map();
15
+ eventsBySession = /* @__PURE__ */ new Map();
16
+ constructor(options = {}) {
17
+ this.maxSessions = normalizeCap(options.maxSessions, DEFAULT_MAX_SESSIONS);
18
+ this.maxEventsPerSession = normalizeCap(
19
+ options.maxEventsPerSession,
20
+ DEFAULT_MAX_EVENTS_PER_SESSION
21
+ );
22
+ }
23
+ async getSession(id) {
24
+ const session = this.sessions.get(id);
25
+ return session ? cloneSessionRecord(session) : null;
26
+ }
27
+ async listSessions(request = {}) {
28
+ const sorted = [...this.sessions.values()].sort((a, b) => {
29
+ if (a.createdAt !== b.createdAt) {
30
+ return a.createdAt - b.createdAt;
31
+ }
32
+ return a.id.localeCompare(b.id);
33
+ });
34
+ const page = paginate(sorted, request);
35
+ return {
36
+ items: page.items.map(cloneSessionRecord),
37
+ nextCursor: page.nextCursor
38
+ };
39
+ }
40
+ async updateSession(session) {
41
+ this.sessions.set(session.id, { ...session });
42
+ if (!this.eventsBySession.has(session.id)) {
43
+ this.eventsBySession.set(session.id, []);
44
+ }
45
+ if (this.sessions.size <= this.maxSessions) {
46
+ return;
47
+ }
48
+ const overflow = this.sessions.size - this.maxSessions;
49
+ const removable = [...this.sessions.values()].sort((a, b) => {
50
+ if (a.createdAt !== b.createdAt) {
51
+ return a.createdAt - b.createdAt;
52
+ }
53
+ return a.id.localeCompare(b.id);
54
+ }).slice(0, overflow).map((sessionToRemove) => sessionToRemove.id);
55
+ for (const sessionId of removable) {
56
+ this.sessions.delete(sessionId);
57
+ this.eventsBySession.delete(sessionId);
58
+ }
59
+ }
60
+ async listEvents(request) {
61
+ const all = [...this.eventsBySession.get(request.sessionId) ?? []].sort((a, b) => {
62
+ if (a.eventIndex !== b.eventIndex) {
63
+ return a.eventIndex - b.eventIndex;
64
+ }
65
+ return a.id.localeCompare(b.id);
66
+ });
67
+ const page = paginate(all, request);
68
+ return {
69
+ items: page.items.map(cloneSessionEvent),
70
+ nextCursor: page.nextCursor
71
+ };
72
+ }
73
+ async insertEvent(event) {
74
+ const events = this.eventsBySession.get(event.sessionId) ?? [];
75
+ events.push(cloneSessionEvent(event));
76
+ if (events.length > this.maxEventsPerSession) {
77
+ events.splice(0, events.length - this.maxEventsPerSession);
78
+ }
79
+ this.eventsBySession.set(event.sessionId, events);
80
+ }
81
+ };
82
+ function cloneSessionRecord(session) {
83
+ return {
84
+ ...session,
85
+ sessionInit: session.sessionInit ? JSON.parse(JSON.stringify(session.sessionInit)) : void 0
86
+ };
87
+ }
88
+ function cloneSessionEvent(event) {
89
+ return {
90
+ ...event,
91
+ payload: JSON.parse(JSON.stringify(event.payload))
92
+ };
93
+ }
94
+ function normalizeCap(value, fallback) {
95
+ if (!Number.isFinite(value) || (value ?? 0) < 1) {
96
+ return fallback;
97
+ }
98
+ return Math.floor(value);
99
+ }
100
+ function paginate(items, request) {
101
+ const offset = parseCursor(request.cursor);
102
+ const limit = normalizeCap(request.limit, DEFAULT_LIST_LIMIT);
103
+ const slice = items.slice(offset, offset + limit);
104
+ const nextOffset = offset + slice.length;
105
+ return {
106
+ items: slice,
107
+ nextCursor: nextOffset < items.length ? String(nextOffset) : void 0
108
+ };
109
+ }
110
+ function parseCursor(cursor) {
111
+ if (!cursor) {
112
+ return 0;
113
+ }
114
+ const parsed = Number.parseInt(cursor, 10);
115
+ if (!Number.isFinite(parsed) || parsed < 0) {
116
+ return 0;
117
+ }
118
+ return parsed;
119
+ }
120
+
1
121
  // src/client.ts
2
122
  var API_PREFIX = "/v1";
123
+ var FS_PATH = `${API_PREFIX}/fs`;
124
+ var DEFAULT_REPLAY_MAX_EVENTS = 50;
125
+ var DEFAULT_REPLAY_MAX_CHARS = 12e3;
126
+ var EVENT_INDEX_SCAN_EVENTS_LIMIT = 500;
3
127
  var SandboxAgentError = class extends Error {
4
128
  status;
5
129
  problem;
@@ -12,17 +136,246 @@ var SandboxAgentError = class extends Error {
12
136
  this.response = response;
13
137
  }
14
138
  };
139
+ var Session = class {
140
+ record;
141
+ sandbox;
142
+ constructor(sandbox, record) {
143
+ this.sandbox = sandbox;
144
+ this.record = { ...record };
145
+ }
146
+ get id() {
147
+ return this.record.id;
148
+ }
149
+ get agent() {
150
+ return this.record.agent;
151
+ }
152
+ get agentSessionId() {
153
+ return this.record.agentSessionId;
154
+ }
155
+ get lastConnectionId() {
156
+ return this.record.lastConnectionId;
157
+ }
158
+ get createdAt() {
159
+ return this.record.createdAt;
160
+ }
161
+ get destroyedAt() {
162
+ return this.record.destroyedAt;
163
+ }
164
+ async refresh() {
165
+ const latest = await this.sandbox.getSession(this.id);
166
+ if (!latest) {
167
+ throw new Error(`session '${this.id}' no longer exists`);
168
+ }
169
+ this.apply(latest.toRecord());
170
+ return this;
171
+ }
172
+ async send(method, params = {}, options = {}) {
173
+ const updated = await this.sandbox.sendSessionMethod(this.id, method, params, options);
174
+ this.apply(updated.session.toRecord());
175
+ return updated.response;
176
+ }
177
+ async prompt(prompt) {
178
+ const response = await this.send("session/prompt", { prompt });
179
+ return response;
180
+ }
181
+ onEvent(listener) {
182
+ return this.sandbox.onSessionEvent(this.id, listener);
183
+ }
184
+ toRecord() {
185
+ return { ...this.record };
186
+ }
187
+ apply(record) {
188
+ this.record = { ...record };
189
+ }
190
+ };
191
+ var LiveAcpConnection = class _LiveAcpConnection {
192
+ connectionId;
193
+ agent;
194
+ acp;
195
+ sessionByLocalId = /* @__PURE__ */ new Map();
196
+ localByAgentSessionId = /* @__PURE__ */ new Map();
197
+ pendingNewSessionLocals = [];
198
+ pendingRequestSessionById = /* @__PURE__ */ new Map();
199
+ pendingReplayByLocalSessionId = /* @__PURE__ */ new Map();
200
+ onObservedEnvelope;
201
+ constructor(agent, connectionId, acp, onObservedEnvelope) {
202
+ this.agent = agent;
203
+ this.connectionId = connectionId;
204
+ this.acp = acp;
205
+ this.onObservedEnvelope = onObservedEnvelope;
206
+ }
207
+ static async create(options) {
208
+ const connectionId = randomId();
209
+ let live = null;
210
+ const acp = new AcpHttpClient({
211
+ baseUrl: options.baseUrl,
212
+ token: options.token,
213
+ fetch: options.fetcher,
214
+ headers: options.headers,
215
+ transport: {
216
+ path: `${API_PREFIX}/acp/${encodeURIComponent(options.serverId)}`,
217
+ bootstrapQuery: { agent: options.agent }
218
+ },
219
+ client: {
220
+ sessionUpdate: async (_notification) => {
221
+ }
222
+ },
223
+ onEnvelope: (envelope, direction) => {
224
+ if (!live) {
225
+ return;
226
+ }
227
+ live.handleEnvelope(envelope, direction);
228
+ }
229
+ });
230
+ live = new _LiveAcpConnection(options.agent, connectionId, acp, options.onObservedEnvelope);
231
+ const initResult = await acp.initialize({
232
+ protocolVersion: PROTOCOL_VERSION,
233
+ clientInfo: {
234
+ name: "sandbox-agent-sdk",
235
+ version: "v1"
236
+ }
237
+ });
238
+ if (initResult.authMethods && initResult.authMethods.length > 0) {
239
+ await autoAuthenticate(acp, initResult.authMethods);
240
+ }
241
+ return live;
242
+ }
243
+ async close() {
244
+ await this.acp.disconnect();
245
+ }
246
+ hasBoundSession(localSessionId, agentSessionId) {
247
+ const bound = this.sessionByLocalId.get(localSessionId);
248
+ if (!bound) {
249
+ return false;
250
+ }
251
+ if (agentSessionId && bound !== agentSessionId) {
252
+ return false;
253
+ }
254
+ return true;
255
+ }
256
+ bindSession(localSessionId, agentSessionId) {
257
+ this.sessionByLocalId.set(localSessionId, agentSessionId);
258
+ this.localByAgentSessionId.set(agentSessionId, localSessionId);
259
+ }
260
+ queueReplay(localSessionId, replayText) {
261
+ if (!replayText) {
262
+ this.pendingReplayByLocalSessionId.delete(localSessionId);
263
+ return;
264
+ }
265
+ this.pendingReplayByLocalSessionId.set(localSessionId, replayText);
266
+ }
267
+ async createRemoteSession(localSessionId, sessionInit) {
268
+ this.pendingNewSessionLocals.push(localSessionId);
269
+ try {
270
+ const response = await this.acp.newSession(sessionInit);
271
+ this.bindSession(localSessionId, response.sessionId);
272
+ return response;
273
+ } catch (error) {
274
+ const index = this.pendingNewSessionLocals.indexOf(localSessionId);
275
+ if (index !== -1) {
276
+ this.pendingNewSessionLocals.splice(index, 1);
277
+ }
278
+ throw error;
279
+ }
280
+ }
281
+ async sendSessionMethod(localSessionId, method, params, options) {
282
+ const agentSessionId = this.sessionByLocalId.get(localSessionId);
283
+ if (!agentSessionId) {
284
+ throw new Error(`session '${localSessionId}' is not bound to live ACP connection '${this.connectionId}'`);
285
+ }
286
+ const mappedParams = mapSessionParams(params, agentSessionId);
287
+ if (method === "session/prompt") {
288
+ const replayText = this.pendingReplayByLocalSessionId.get(localSessionId);
289
+ if (replayText) {
290
+ this.pendingReplayByLocalSessionId.delete(localSessionId);
291
+ injectReplayPrompt(mappedParams, replayText);
292
+ }
293
+ if (options.notification) {
294
+ await this.acp.extNotification(method, mappedParams);
295
+ return void 0;
296
+ }
297
+ return this.acp.prompt(mappedParams);
298
+ }
299
+ if (method === "session/cancel") {
300
+ await this.acp.cancel(mappedParams);
301
+ return void 0;
302
+ }
303
+ if (method === "session/set_mode") {
304
+ return this.acp.setSessionMode(mappedParams);
305
+ }
306
+ if (method === "session/set_config_option") {
307
+ return this.acp.setSessionConfigOption(mappedParams);
308
+ }
309
+ if (options.notification) {
310
+ await this.acp.extNotification(method, mappedParams);
311
+ return void 0;
312
+ }
313
+ return this.acp.extMethod(method, mappedParams);
314
+ }
315
+ handleEnvelope(envelope, direction) {
316
+ const localSessionId = this.resolveSessionId(envelope, direction);
317
+ this.onObservedEnvelope(this, envelope, direction, localSessionId);
318
+ }
319
+ resolveSessionId(envelope, direction) {
320
+ const id = envelopeId(envelope);
321
+ const method = envelopeMethod(envelope);
322
+ if (direction === "outbound") {
323
+ if (id && method === "session/new") {
324
+ const localSessionId = this.pendingNewSessionLocals.shift() ?? null;
325
+ if (localSessionId) {
326
+ this.pendingRequestSessionById.set(id, localSessionId);
327
+ }
328
+ return localSessionId;
329
+ }
330
+ const localFromParams = this.localFromEnvelopeParams(envelope);
331
+ if (id && localFromParams) {
332
+ this.pendingRequestSessionById.set(id, localFromParams);
333
+ }
334
+ return localFromParams;
335
+ }
336
+ if (id) {
337
+ const pending = this.pendingRequestSessionById.get(id) ?? null;
338
+ if (pending) {
339
+ this.pendingRequestSessionById.delete(id);
340
+ const sessionIdFromResult = envelopeSessionIdFromResult(envelope);
341
+ if (sessionIdFromResult) {
342
+ this.bindSession(pending, sessionIdFromResult);
343
+ }
344
+ return pending;
345
+ }
346
+ }
347
+ return this.localFromEnvelopeParams(envelope);
348
+ }
349
+ localFromEnvelopeParams(envelope) {
350
+ const agentSessionId = envelopeSessionIdFromParams(envelope);
351
+ if (!agentSessionId) {
352
+ return null;
353
+ }
354
+ return this.localByAgentSessionId.get(agentSessionId) ?? null;
355
+ }
356
+ };
15
357
  var SandboxAgent = class _SandboxAgent {
16
358
  baseUrl;
17
359
  token;
18
360
  fetcher;
19
361
  defaultHeaders;
362
+ persist;
363
+ replayMaxEvents;
364
+ replayMaxChars;
20
365
  spawnHandle;
366
+ liveConnections = /* @__PURE__ */ new Map();
367
+ sessionHandles = /* @__PURE__ */ new Map();
368
+ eventListeners = /* @__PURE__ */ new Map();
369
+ nextSessionEventIndexBySession = /* @__PURE__ */ new Map();
370
+ seedSessionEventIndexBySession = /* @__PURE__ */ new Map();
21
371
  constructor(options) {
22
372
  this.baseUrl = options.baseUrl.replace(/\/$/, "");
23
373
  this.token = options.token;
24
374
  this.fetcher = options.fetch ?? globalThis.fetch.bind(globalThis);
25
375
  this.defaultHeaders = options.headers;
376
+ this.persist = options.persist ?? new InMemorySessionPersistDriver();
377
+ this.replayMaxEvents = normalizePositiveInt(options.replayMaxEvents, DEFAULT_REPLAY_MAX_EVENTS);
378
+ this.replayMaxChars = normalizePositiveInt(options.replayMaxChars, DEFAULT_REPLAY_MAX_CHARS);
26
379
  if (!this.fetcher) {
27
380
  throw new Error("Fetch API is not available; provide a fetch implementation.");
28
381
  }
@@ -41,97 +394,167 @@ var SandboxAgent = class _SandboxAgent {
41
394
  baseUrl: handle.baseUrl,
42
395
  token: handle.token,
43
396
  fetch: options.fetch,
44
- headers: options.headers
397
+ headers: options.headers,
398
+ persist: options.persist,
399
+ replayMaxEvents: options.replayMaxEvents,
400
+ replayMaxChars: options.replayMaxChars
45
401
  });
46
402
  client.spawnHandle = handle;
47
403
  return client;
48
404
  }
49
- async listAgents() {
50
- return this.requestJson("GET", `${API_PREFIX}/agents`);
51
- }
52
- async getHealth() {
53
- return this.requestJson("GET", `${API_PREFIX}/health`);
54
- }
55
- async installAgent(agent, request = {}) {
56
- await this.requestJson("POST", `${API_PREFIX}/agents/${encodeURIComponent(agent)}/install`, {
57
- body: request
58
- });
405
+ async dispose() {
406
+ const connections = [...this.liveConnections.values()];
407
+ this.liveConnections.clear();
408
+ await Promise.all(
409
+ connections.map(async (connection) => {
410
+ await connection.close();
411
+ })
412
+ );
413
+ if (this.spawnHandle) {
414
+ await this.spawnHandle.dispose();
415
+ this.spawnHandle = void 0;
416
+ }
59
417
  }
60
- async getAgentModes(agent) {
61
- return this.requestJson("GET", `${API_PREFIX}/agents/${encodeURIComponent(agent)}/modes`);
418
+ async listSessions(request = {}) {
419
+ const page = await this.persist.listSessions(request);
420
+ return {
421
+ items: page.items.map((record) => this.upsertSessionHandle(record)),
422
+ nextCursor: page.nextCursor
423
+ };
424
+ }
425
+ async getSession(id) {
426
+ const record = await this.persist.getSession(id);
427
+ if (!record) {
428
+ return null;
429
+ }
430
+ return this.upsertSessionHandle(record);
62
431
  }
63
- async getAgentModels(agent) {
64
- return this.requestJson("GET", `${API_PREFIX}/agents/${encodeURIComponent(agent)}/models`);
432
+ async getEvents(request) {
433
+ return this.persist.listEvents(request);
65
434
  }
66
- async createSession(sessionId, request) {
67
- return this.requestJson("POST", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}`, {
68
- body: request
69
- });
435
+ async createSession(request) {
436
+ if (!request.agent.trim()) {
437
+ throw new Error("createSession requires a non-empty agent");
438
+ }
439
+ const localSessionId = request.id?.trim() || randomId();
440
+ const live = await this.getLiveConnection(request.agent.trim());
441
+ const sessionInit = normalizeSessionInit(request.sessionInit);
442
+ const response = await live.createRemoteSession(localSessionId, sessionInit);
443
+ const record = {
444
+ id: localSessionId,
445
+ agent: request.agent.trim(),
446
+ agentSessionId: response.sessionId,
447
+ lastConnectionId: live.connectionId,
448
+ createdAt: nowMs(),
449
+ sessionInit
450
+ };
451
+ await this.persist.updateSession(record);
452
+ this.nextSessionEventIndexBySession.set(record.id, 1);
453
+ live.bindSession(record.id, record.agentSessionId);
454
+ return this.upsertSessionHandle(record);
455
+ }
456
+ async resumeSession(id) {
457
+ const existing = await this.persist.getSession(id);
458
+ if (!existing) {
459
+ throw new Error(`session '${id}' not found`);
460
+ }
461
+ const live = await this.getLiveConnection(existing.agent);
462
+ if (existing.lastConnectionId === live.connectionId && live.hasBoundSession(id, existing.agentSessionId)) {
463
+ return this.upsertSessionHandle(existing);
464
+ }
465
+ const replaySource = await this.collectReplayEvents(existing.id, this.replayMaxEvents);
466
+ const replayText = buildReplayText(replaySource, this.replayMaxChars);
467
+ const recreated = await live.createRemoteSession(existing.id, normalizeSessionInit(existing.sessionInit));
468
+ const updated = {
469
+ ...existing,
470
+ agentSessionId: recreated.sessionId,
471
+ lastConnectionId: live.connectionId,
472
+ destroyedAt: void 0
473
+ };
474
+ await this.persist.updateSession(updated);
475
+ live.bindSession(updated.id, updated.agentSessionId);
476
+ live.queueReplay(updated.id, replayText);
477
+ return this.upsertSessionHandle(updated);
478
+ }
479
+ async resumeOrCreateSession(request) {
480
+ const existing = await this.persist.getSession(request.id);
481
+ if (existing) {
482
+ return this.resumeSession(existing.id);
483
+ }
484
+ return this.createSession(request);
70
485
  }
71
- async listSessions() {
72
- return this.requestJson("GET", `${API_PREFIX}/sessions`);
486
+ async destroySession(id) {
487
+ const existing = await this.persist.getSession(id);
488
+ if (!existing) {
489
+ throw new Error(`session '${id}' not found`);
490
+ }
491
+ const updated = {
492
+ ...existing,
493
+ destroyedAt: nowMs()
494
+ };
495
+ await this.persist.updateSession(updated);
496
+ return this.upsertSessionHandle(updated);
497
+ }
498
+ async sendSessionMethod(sessionId, method, params, options = {}) {
499
+ const record = await this.persist.getSession(sessionId);
500
+ if (!record) {
501
+ throw new Error(`session '${sessionId}' not found`);
502
+ }
503
+ const live = await this.getLiveConnection(record.agent);
504
+ if (!live.hasBoundSession(record.id, record.agentSessionId)) {
505
+ const restored = await this.resumeSession(record.id);
506
+ return this.sendSessionMethod(restored.id, method, params, options);
507
+ }
508
+ const response = await live.sendSessionMethod(record.id, method, params, options);
509
+ const refreshed = await this.requireSessionRecord(record.id);
510
+ return {
511
+ session: this.upsertSessionHandle(refreshed),
512
+ response
513
+ };
514
+ }
515
+ onSessionEvent(sessionId, listener) {
516
+ const listeners = this.eventListeners.get(sessionId) ?? /* @__PURE__ */ new Set();
517
+ listeners.add(listener);
518
+ this.eventListeners.set(sessionId, listeners);
519
+ return () => {
520
+ const set = this.eventListeners.get(sessionId);
521
+ if (!set) {
522
+ return;
523
+ }
524
+ set.delete(listener);
525
+ if (set.size === 0) {
526
+ this.eventListeners.delete(sessionId);
527
+ }
528
+ };
73
529
  }
74
- async postMessage(sessionId, request) {
75
- await this.requestJson("POST", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/messages`, {
76
- body: request
77
- });
530
+ async getHealth() {
531
+ return this.requestJson("GET", `${API_PREFIX}/health`);
78
532
  }
79
- async getEvents(sessionId, query) {
80
- return this.requestJson("GET", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/events`, {
81
- query
533
+ async listAgents(options) {
534
+ return this.requestJson("GET", `${API_PREFIX}/agents`, {
535
+ query: options?.config ? { config: "true" } : void 0
82
536
  });
83
537
  }
84
- async getEventsSse(sessionId, query, signal) {
85
- return this.requestRaw("GET", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/events/sse`, {
86
- query,
87
- accept: "text/event-stream",
88
- signal
538
+ async getAgent(agent, options) {
539
+ return this.requestJson("GET", `${API_PREFIX}/agents/${encodeURIComponent(agent)}`, {
540
+ query: options?.config ? { config: "true" } : void 0
89
541
  });
90
542
  }
91
- async postMessageStream(sessionId, request, query, signal) {
92
- return this.requestRaw("POST", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/messages/stream`, {
93
- query,
94
- body: request,
95
- accept: "text/event-stream",
96
- signal
543
+ async installAgent(agent, request = {}) {
544
+ return this.requestJson("POST", `${API_PREFIX}/agents/${encodeURIComponent(agent)}/install`, {
545
+ body: request
97
546
  });
98
547
  }
99
- async *streamEvents(sessionId, query, signal) {
100
- const response = await this.getEventsSse(sessionId, query, signal);
101
- yield* this.parseSseStream(response);
548
+ async listAcpServers() {
549
+ return this.requestJson("GET", `${API_PREFIX}/acp`);
102
550
  }
103
- async *streamTurn(sessionId, request, query, signal) {
104
- const response = await this.postMessageStream(sessionId, request, query, signal);
105
- yield* this.parseSseStream(response);
106
- }
107
- async replyQuestion(sessionId, questionId, request) {
108
- await this.requestJson(
109
- "POST",
110
- `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/questions/${encodeURIComponent(questionId)}/reply`,
111
- { body: request }
112
- );
113
- }
114
- async rejectQuestion(sessionId, questionId) {
115
- await this.requestJson(
116
- "POST",
117
- `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/questions/${encodeURIComponent(questionId)}/reject`
118
- );
119
- }
120
- async replyPermission(sessionId, permissionId, request) {
121
- await this.requestJson(
122
- "POST",
123
- `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/permissions/${encodeURIComponent(permissionId)}/reply`,
124
- { body: request }
125
- );
126
- }
127
- async terminateSession(sessionId) {
128
- await this.requestJson("POST", `${API_PREFIX}/sessions/${encodeURIComponent(sessionId)}/terminate`);
129
- }
130
- async listFsEntries(query) {
131
- return this.requestJson("GET", `${API_PREFIX}/fs/entries`, { query });
551
+ async listFsEntries(query = {}) {
552
+ return this.requestJson("GET", `${FS_PATH}/entries`, {
553
+ query
554
+ });
132
555
  }
133
556
  async readFsFile(query) {
134
- const response = await this.requestRaw("GET", `${API_PREFIX}/fs/file`, {
557
+ const response = await this.requestRaw("GET", `${FS_PATH}/file`, {
135
558
  query,
136
559
  accept: "application/octet-stream"
137
560
  });
@@ -139,69 +562,198 @@ var SandboxAgent = class _SandboxAgent {
139
562
  return new Uint8Array(buffer);
140
563
  }
141
564
  async writeFsFile(query, body) {
142
- const response = await this.requestRaw("PUT", `${API_PREFIX}/fs/file`, {
565
+ const response = await this.requestRaw("PUT", `${FS_PATH}/file`, {
143
566
  query,
144
567
  rawBody: body,
145
568
  contentType: "application/octet-stream",
146
569
  accept: "application/json"
147
570
  });
148
- const text = await response.text();
149
- return text ? JSON.parse(text) : { path: "", bytesWritten: 0 };
571
+ return await response.json();
150
572
  }
151
573
  async deleteFsEntry(query) {
152
- return this.requestJson("DELETE", `${API_PREFIX}/fs/entry`, { query });
574
+ return this.requestJson("DELETE", `${FS_PATH}/entry`, { query });
153
575
  }
154
576
  async mkdirFs(query) {
155
- return this.requestJson("POST", `${API_PREFIX}/fs/mkdir`, { query });
577
+ return this.requestJson("POST", `${FS_PATH}/mkdir`, { query });
156
578
  }
157
- async moveFs(request, query) {
158
- return this.requestJson("POST", `${API_PREFIX}/fs/move`, { query, body: request });
579
+ async moveFs(request) {
580
+ return this.requestJson("POST", `${FS_PATH}/move`, { body: request });
159
581
  }
160
582
  async statFs(query) {
161
- return this.requestJson("GET", `${API_PREFIX}/fs/stat`, { query });
583
+ return this.requestJson("GET", `${FS_PATH}/stat`, { query });
162
584
  }
163
585
  async uploadFsBatch(body, query) {
164
- const response = await this.requestRaw("POST", `${API_PREFIX}/fs/upload-batch`, {
586
+ const response = await this.requestRaw("POST", `${FS_PATH}/upload-batch`, {
165
587
  query,
166
588
  rawBody: body,
167
589
  contentType: "application/x-tar",
168
590
  accept: "application/json"
169
591
  });
170
- const text = await response.text();
171
- return text ? JSON.parse(text) : { paths: [], truncated: false };
592
+ return await response.json();
172
593
  }
173
- async dispose() {
174
- if (this.spawnHandle) {
175
- await this.spawnHandle.dispose();
176
- this.spawnHandle = void 0;
594
+ async getMcpConfig(query) {
595
+ return this.requestJson("GET", `${API_PREFIX}/config/mcp`, { query });
596
+ }
597
+ async setMcpConfig(query, config) {
598
+ await this.requestRaw("PUT", `${API_PREFIX}/config/mcp`, { query, body: config });
599
+ }
600
+ async deleteMcpConfig(query) {
601
+ await this.requestRaw("DELETE", `${API_PREFIX}/config/mcp`, { query });
602
+ }
603
+ async getSkillsConfig(query) {
604
+ return this.requestJson("GET", `${API_PREFIX}/config/skills`, { query });
605
+ }
606
+ async setSkillsConfig(query, config) {
607
+ await this.requestRaw("PUT", `${API_PREFIX}/config/skills`, { query, body: config });
608
+ }
609
+ async deleteSkillsConfig(query) {
610
+ await this.requestRaw("DELETE", `${API_PREFIX}/config/skills`, { query });
611
+ }
612
+ async getLiveConnection(agent) {
613
+ const existing = this.liveConnections.get(agent);
614
+ if (existing) {
615
+ return existing;
616
+ }
617
+ const serverId = `sdk-${agent}-${randomId()}`;
618
+ const created = await LiveAcpConnection.create({
619
+ baseUrl: this.baseUrl,
620
+ token: this.token,
621
+ fetcher: this.fetcher,
622
+ headers: this.defaultHeaders,
623
+ agent,
624
+ serverId,
625
+ onObservedEnvelope: (connection, envelope, direction, localSessionId) => {
626
+ void this.persistObservedEnvelope(connection, envelope, direction, localSessionId);
627
+ }
628
+ });
629
+ this.liveConnections.set(agent, created);
630
+ return created;
631
+ }
632
+ async persistObservedEnvelope(connection, envelope, direction, localSessionId) {
633
+ if (!localSessionId) {
634
+ return;
635
+ }
636
+ const event = {
637
+ id: randomId(),
638
+ eventIndex: await this.allocateSessionEventIndex(localSessionId),
639
+ sessionId: localSessionId,
640
+ createdAt: nowMs(),
641
+ connectionId: connection.connectionId,
642
+ sender: direction === "outbound" ? "client" : "agent",
643
+ payload: cloneEnvelope(envelope)
644
+ };
645
+ await this.persist.insertEvent(event);
646
+ const listeners = this.eventListeners.get(localSessionId);
647
+ if (!listeners || listeners.size === 0) {
648
+ return;
177
649
  }
650
+ for (const listener of listeners) {
651
+ listener(event);
652
+ }
653
+ }
654
+ async allocateSessionEventIndex(sessionId) {
655
+ await this.ensureSessionEventIndexSeeded(sessionId);
656
+ const nextIndex = this.nextSessionEventIndexBySession.get(sessionId) ?? 1;
657
+ this.nextSessionEventIndexBySession.set(sessionId, nextIndex + 1);
658
+ return nextIndex;
659
+ }
660
+ async ensureSessionEventIndexSeeded(sessionId) {
661
+ if (this.nextSessionEventIndexBySession.has(sessionId)) {
662
+ return;
663
+ }
664
+ if (!this.seedSessionEventIndexBySession.has(sessionId)) {
665
+ const pending2 = (async () => {
666
+ const maxPersistedIndex = await this.findMaxPersistedSessionEventIndex(sessionId);
667
+ this.nextSessionEventIndexBySession.set(sessionId, Math.max(1, maxPersistedIndex + 1));
668
+ })().finally(() => {
669
+ this.seedSessionEventIndexBySession.delete(sessionId);
670
+ });
671
+ this.seedSessionEventIndexBySession.set(sessionId, pending2);
672
+ }
673
+ const pending = this.seedSessionEventIndexBySession.get(sessionId);
674
+ if (pending) {
675
+ await pending;
676
+ }
677
+ }
678
+ async findMaxPersistedSessionEventIndex(sessionId) {
679
+ let maxIndex = 0;
680
+ let eventCursor;
681
+ while (true) {
682
+ const eventsPage = await this.persist.listEvents({
683
+ sessionId,
684
+ cursor: eventCursor,
685
+ limit: EVENT_INDEX_SCAN_EVENTS_LIMIT
686
+ });
687
+ for (const event of eventsPage.items) {
688
+ if (Number.isFinite(event.eventIndex) && event.eventIndex > maxIndex) {
689
+ maxIndex = Math.floor(event.eventIndex);
690
+ }
691
+ }
692
+ if (!eventsPage.nextCursor) {
693
+ break;
694
+ }
695
+ eventCursor = eventsPage.nextCursor;
696
+ }
697
+ return maxIndex;
698
+ }
699
+ async collectReplayEvents(sessionId, maxEvents) {
700
+ const all = [];
701
+ let cursor;
702
+ while (true) {
703
+ const page = await this.persist.listEvents({
704
+ sessionId,
705
+ cursor,
706
+ limit: Math.max(100, maxEvents)
707
+ });
708
+ all.push(...page.items);
709
+ if (!page.nextCursor) {
710
+ break;
711
+ }
712
+ cursor = page.nextCursor;
713
+ }
714
+ return all.slice(-maxEvents);
715
+ }
716
+ upsertSessionHandle(record) {
717
+ const existing = this.sessionHandles.get(record.id);
718
+ if (existing) {
719
+ existing.apply(record);
720
+ return existing;
721
+ }
722
+ const created = new Session(this, record);
723
+ this.sessionHandles.set(record.id, created);
724
+ return created;
725
+ }
726
+ async requireSessionRecord(id) {
727
+ const record = await this.persist.getSession(id);
728
+ if (!record) {
729
+ throw new Error(`session '${id}' not found`);
730
+ }
731
+ return record;
178
732
  }
179
733
  async requestJson(method, path, options = {}) {
180
734
  const response = await this.requestRaw(method, path, {
181
735
  query: options.query,
182
736
  body: options.body,
183
737
  headers: options.headers,
184
- accept: options.accept ?? "application/json"
738
+ accept: options.accept ?? "application/json",
739
+ signal: options.signal
185
740
  });
186
741
  if (response.status === 204) {
187
742
  return void 0;
188
743
  }
189
- const text = await response.text();
190
- if (!text) {
191
- return void 0;
192
- }
193
- return JSON.parse(text);
744
+ return await response.json();
194
745
  }
195
746
  async requestRaw(method, path, options = {}) {
196
747
  const url = this.buildUrl(path, options.query);
197
- const headers = new Headers(this.defaultHeaders ?? void 0);
198
- if (this.token) {
199
- headers.set("Authorization", `Bearer ${this.token}`);
200
- }
748
+ const headers = this.buildHeaders(options.headers);
201
749
  if (options.accept) {
202
750
  headers.set("Accept", options.accept);
203
751
  }
204
- const init = { method, headers, signal: options.signal };
752
+ const init = {
753
+ method,
754
+ headers,
755
+ signal: options.signal
756
+ };
205
757
  if (options.rawBody !== void 0 && options.body !== void 0) {
206
758
  throw new Error("requestRaw received both rawBody and body");
207
759
  }
@@ -214,17 +766,24 @@ var SandboxAgent = class _SandboxAgent {
214
766
  headers.set("Content-Type", "application/json");
215
767
  init.body = JSON.stringify(options.body);
216
768
  }
217
- if (options.headers) {
218
- const extra = new Headers(options.headers);
219
- extra.forEach((value, key) => headers.set(key, value));
220
- }
221
769
  const response = await this.fetcher(url, init);
222
770
  if (!response.ok) {
223
- const problem = await this.readProblem(response);
771
+ const problem = await readProblem(response);
224
772
  throw new SandboxAgentError(response.status, problem, response);
225
773
  }
226
774
  return response;
227
775
  }
776
+ buildHeaders(extra) {
777
+ const headers = new Headers(this.defaultHeaders ?? void 0);
778
+ if (this.token) {
779
+ headers.set("Authorization", `Bearer ${this.token}`);
780
+ }
781
+ if (extra) {
782
+ const merged = new Headers(extra);
783
+ merged.forEach((value, key) => headers.set(key, value));
784
+ }
785
+ return headers;
786
+ }
228
787
  buildUrl(path, query) {
229
788
  const url = new URL(`${this.baseUrl}${path}`);
230
789
  if (query) {
@@ -237,55 +796,152 @@ var SandboxAgent = class _SandboxAgent {
237
796
  }
238
797
  return url.toString();
239
798
  }
240
- async readProblem(response) {
241
- try {
242
- const text = await response.clone().text();
243
- if (!text) {
244
- return void 0;
245
- }
246
- return JSON.parse(text);
247
- } catch {
248
- return void 0;
249
- }
799
+ };
800
+ async function autoAuthenticate(acp, methods) {
801
+ const envBased = methods.find(
802
+ (m) => m.id === "codex-api-key" || m.id === "openai-api-key" || m.id === "anthropic-api-key"
803
+ );
804
+ if (!envBased) {
805
+ return;
250
806
  }
251
- async *parseSseStream(response) {
252
- if (!response.body) {
253
- throw new Error("SSE stream is not readable in this environment.");
254
- }
255
- const reader = response.body.getReader();
256
- const decoder = new TextDecoder();
257
- let buffer = "";
258
- while (true) {
259
- const { done, value } = await reader.read();
260
- if (done) {
261
- break;
262
- }
263
- buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
264
- let index = buffer.indexOf("\n\n");
265
- while (index !== -1) {
266
- const chunk = buffer.slice(0, index);
267
- buffer = buffer.slice(index + 2);
268
- const dataLines = chunk.split("\n").filter((line) => line.startsWith("data:"));
269
- if (dataLines.length > 0) {
270
- const payload = dataLines.map((line) => line.slice(5).trim()).join("\n");
271
- if (payload) {
272
- yield JSON.parse(payload);
273
- }
274
- }
275
- index = buffer.indexOf("\n\n");
276
- }
807
+ try {
808
+ await acp.authenticate({ methodId: envBased.id });
809
+ } catch {
810
+ }
811
+ }
812
+ function normalizeSessionInit(value) {
813
+ if (!value) {
814
+ return {
815
+ cwd: defaultCwd(),
816
+ mcpServers: []
817
+ };
818
+ }
819
+ return {
820
+ ...value,
821
+ cwd: value.cwd ?? defaultCwd(),
822
+ mcpServers: value.mcpServers ?? []
823
+ };
824
+ }
825
+ function mapSessionParams(params, agentSessionId) {
826
+ return {
827
+ ...params,
828
+ sessionId: agentSessionId
829
+ };
830
+ }
831
+ function injectReplayPrompt(params, replayText) {
832
+ const prompt = Array.isArray(params.prompt) ? [...params.prompt] : [];
833
+ prompt.unshift({
834
+ type: "text",
835
+ text: replayText
836
+ });
837
+ params.prompt = prompt;
838
+ }
839
+ function buildReplayText(events, maxChars) {
840
+ if (events.length === 0) {
841
+ return null;
842
+ }
843
+ const prefix = "Previous session history is replayed below as JSON-RPC envelopes. Use it as context before responding to the latest user prompt.\n";
844
+ let text = prefix;
845
+ for (const event of events) {
846
+ const line = JSON.stringify({
847
+ createdAt: event.createdAt,
848
+ sender: event.sender,
849
+ payload: event.payload
850
+ });
851
+ if (text.length + line.length + 1 > maxChars) {
852
+ text += "\n[history truncated]";
853
+ break;
277
854
  }
855
+ text += `${line}
856
+ `;
278
857
  }
279
- };
280
- var normalizeSpawnOptions = (spawn, defaultEnabled) => {
281
- if (typeof spawn === "boolean") {
282
- return { enabled: spawn };
858
+ return text;
859
+ }
860
+ function envelopeMethod(message) {
861
+ if (!isRecord(message) || !("method" in message) || typeof message["method"] !== "string") {
862
+ return null;
283
863
  }
284
- if (spawn) {
285
- return { enabled: spawn.enabled ?? defaultEnabled, ...spawn };
864
+ return message["method"];
865
+ }
866
+ function envelopeId(message) {
867
+ if (!isRecord(message) || !("id" in message) || message["id"] === void 0 || message["id"] === null) {
868
+ return null;
286
869
  }
287
- return { enabled: defaultEnabled };
288
- };
870
+ return String(message["id"]);
871
+ }
872
+ function envelopeSessionIdFromParams(message) {
873
+ if (!isRecord(message) || !("params" in message) || !isRecord(message["params"])) {
874
+ return null;
875
+ }
876
+ const params = message["params"];
877
+ if (typeof params.sessionId === "string" && params.sessionId.length > 0) {
878
+ return params.sessionId;
879
+ }
880
+ return null;
881
+ }
882
+ function envelopeSessionIdFromResult(message) {
883
+ if (!isRecord(message) || !("result" in message) || !isRecord(message["result"])) {
884
+ return null;
885
+ }
886
+ const result = message["result"];
887
+ if (typeof result.sessionId === "string" && result.sessionId.length > 0) {
888
+ return result.sessionId;
889
+ }
890
+ return null;
891
+ }
892
+ function cloneEnvelope(envelope) {
893
+ return JSON.parse(JSON.stringify(envelope));
894
+ }
895
+ function isRecord(value) {
896
+ return typeof value === "object" && value !== null;
897
+ }
898
+ function randomId() {
899
+ if (typeof globalThis.crypto?.randomUUID === "function") {
900
+ return globalThis.crypto.randomUUID();
901
+ }
902
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
903
+ }
904
+ function nowMs() {
905
+ return Date.now();
906
+ }
907
+ function defaultCwd() {
908
+ if (typeof process !== "undefined" && typeof process.cwd === "function") {
909
+ return process.cwd();
910
+ }
911
+ return "/";
912
+ }
913
+ function normalizePositiveInt(value, fallback) {
914
+ if (!Number.isFinite(value) || (value ?? 0) < 1) {
915
+ return fallback;
916
+ }
917
+ return Math.floor(value);
918
+ }
919
+ function normalizeSpawnOptions(spawn, defaultEnabled) {
920
+ if (spawn === false) {
921
+ return { enabled: false };
922
+ }
923
+ if (spawn === true || spawn === void 0) {
924
+ return { enabled: defaultEnabled };
925
+ }
926
+ return {
927
+ ...spawn,
928
+ enabled: spawn.enabled ?? defaultEnabled
929
+ };
930
+ }
931
+ async function readProblem(response) {
932
+ try {
933
+ const text = await response.clone().text();
934
+ if (!text) {
935
+ return void 0;
936
+ }
937
+ return JSON.parse(text);
938
+ } catch {
939
+ return void 0;
940
+ }
941
+ }
942
+
943
+ // src/index.ts
944
+ import { AcpRpcError } from "acp-http-client";
289
945
 
290
946
  // src/inspector.ts
291
947
  function buildInspectorUrl(options) {
@@ -301,8 +957,12 @@ function buildInspectorUrl(options) {
301
957
  return `${normalized}/ui/${queryString ? `?${queryString}` : ""}`;
302
958
  }
303
959
  export {
960
+ AcpRpcError,
961
+ InMemorySessionPersistDriver,
962
+ LiveAcpConnection,
304
963
  SandboxAgent,
305
964
  SandboxAgentError,
965
+ Session,
306
966
  buildInspectorUrl
307
967
  };
308
968
  //# sourceMappingURL=index.js.map