sandbox-agent 0.2.1 → 0.2.2

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,6 +1,7 @@
1
1
  // src/client.ts
2
2
  import {
3
3
  AcpHttpClient,
4
+ AcpRpcError,
4
5
  PROTOCOL_VERSION
5
6
  } from "acp-http-client";
6
7
 
@@ -82,7 +83,9 @@ var InMemorySessionPersistDriver = class {
82
83
  function cloneSessionRecord(session) {
83
84
  return {
84
85
  ...session,
85
- sessionInit: session.sessionInit ? JSON.parse(JSON.stringify(session.sessionInit)) : void 0
86
+ sessionInit: session.sessionInit ? JSON.parse(JSON.stringify(session.sessionInit)) : void 0,
87
+ configOptions: session.configOptions ? JSON.parse(JSON.stringify(session.configOptions)) : void 0,
88
+ modes: session.modes ? JSON.parse(JSON.stringify(session.modes)) : session.modes
86
89
  };
87
90
  }
88
91
  function cloneSessionEvent(event) {
@@ -125,6 +128,12 @@ var DEFAULT_BASE_URL = "http://sandbox-agent";
125
128
  var DEFAULT_REPLAY_MAX_EVENTS = 50;
126
129
  var DEFAULT_REPLAY_MAX_CHARS = 12e3;
127
130
  var EVENT_INDEX_SCAN_EVENTS_LIMIT = 500;
131
+ var SESSION_CANCEL_METHOD = "session/cancel";
132
+ var MANUAL_CANCEL_ERROR = "Manual session/cancel calls are not allowed. Use destroySession(sessionId) instead.";
133
+ var HEALTH_WAIT_MIN_DELAY_MS = 500;
134
+ var HEALTH_WAIT_MAX_DELAY_MS = 15e3;
135
+ var HEALTH_WAIT_LOG_AFTER_MS = 5e3;
136
+ var HEALTH_WAIT_LOG_EVERY_MS = 1e4;
128
137
  var SandboxAgentError = class extends Error {
129
138
  status;
130
139
  problem;
@@ -137,6 +146,52 @@ var SandboxAgentError = class extends Error {
137
146
  this.response = response;
138
147
  }
139
148
  };
149
+ var UnsupportedSessionCategoryError = class extends Error {
150
+ sessionId;
151
+ category;
152
+ availableCategories;
153
+ constructor(sessionId, category, availableCategories) {
154
+ super(
155
+ `Session '${sessionId}' does not support category '${category}'. Available categories: ${availableCategories.join(", ") || "(none)"}`
156
+ );
157
+ this.name = "UnsupportedSessionCategoryError";
158
+ this.sessionId = sessionId;
159
+ this.category = category;
160
+ this.availableCategories = availableCategories;
161
+ }
162
+ };
163
+ var UnsupportedSessionValueError = class extends Error {
164
+ sessionId;
165
+ category;
166
+ configId;
167
+ requestedValue;
168
+ allowedValues;
169
+ constructor(sessionId, category, configId, requestedValue, allowedValues) {
170
+ super(
171
+ `Session '${sessionId}' does not support value '${requestedValue}' for category '${category}' (configId='${configId}'). Allowed values: ${allowedValues.join(", ") || "(none)"}`
172
+ );
173
+ this.name = "UnsupportedSessionValueError";
174
+ this.sessionId = sessionId;
175
+ this.category = category;
176
+ this.configId = configId;
177
+ this.requestedValue = requestedValue;
178
+ this.allowedValues = allowedValues;
179
+ }
180
+ };
181
+ var UnsupportedSessionConfigOptionError = class extends Error {
182
+ sessionId;
183
+ configId;
184
+ availableConfigIds;
185
+ constructor(sessionId, configId, availableConfigIds) {
186
+ super(
187
+ `Session '${sessionId}' does not expose config option '${configId}'. Available configIds: ${availableConfigIds.join(", ") || "(none)"}`
188
+ );
189
+ this.name = "UnsupportedSessionConfigOptionError";
190
+ this.sessionId = sessionId;
191
+ this.configId = configId;
192
+ this.availableConfigIds = availableConfigIds;
193
+ }
194
+ };
140
195
  var Session = class {
141
196
  record;
142
197
  sandbox;
@@ -179,6 +234,32 @@ var Session = class {
179
234
  const response = await this.send("session/prompt", { prompt });
180
235
  return response;
181
236
  }
237
+ async setMode(modeId) {
238
+ const updated = await this.sandbox.setSessionMode(this.id, modeId);
239
+ this.apply(updated.session.toRecord());
240
+ return updated.response;
241
+ }
242
+ async setConfigOption(configId, value) {
243
+ const updated = await this.sandbox.setSessionConfigOption(this.id, configId, value);
244
+ this.apply(updated.session.toRecord());
245
+ return updated.response;
246
+ }
247
+ async setModel(model) {
248
+ const updated = await this.sandbox.setSessionModel(this.id, model);
249
+ this.apply(updated.session.toRecord());
250
+ return updated.response;
251
+ }
252
+ async setThoughtLevel(thoughtLevel) {
253
+ const updated = await this.sandbox.setSessionThoughtLevel(this.id, thoughtLevel);
254
+ this.apply(updated.session.toRecord());
255
+ return updated.response;
256
+ }
257
+ async getConfigOptions() {
258
+ return this.sandbox.getSessionConfigOptions(this.id);
259
+ }
260
+ async getModes() {
261
+ return this.sandbox.getSessionModes(this.id);
262
+ }
182
263
  onEvent(listener) {
183
264
  return this.sandbox.onSessionEvent(this.id, listener);
184
265
  }
@@ -382,10 +463,15 @@ var SandboxAgent = class _SandboxAgent {
382
463
  token;
383
464
  fetcher;
384
465
  defaultHeaders;
466
+ healthWait;
467
+ healthWaitAbortController = new AbortController();
385
468
  persist;
386
469
  replayMaxEvents;
387
470
  replayMaxChars;
388
471
  spawnHandle;
472
+ healthPromise;
473
+ healthError;
474
+ disposed = false;
389
475
  liveConnections = /* @__PURE__ */ new Map();
390
476
  pendingLiveConnections = /* @__PURE__ */ new Map();
391
477
  sessionHandles = /* @__PURE__ */ new Map();
@@ -405,9 +491,11 @@ var SandboxAgent = class _SandboxAgent {
405
491
  }
406
492
  this.fetcher = resolvedFetch;
407
493
  this.defaultHeaders = options.headers;
494
+ this.healthWait = normalizeHealthWaitOptions(options.waitForHealth, options.signal);
408
495
  this.persist = options.persist ?? new InMemorySessionPersistDriver();
409
496
  this.replayMaxEvents = normalizePositiveInt(options.replayMaxEvents, DEFAULT_REPLAY_MAX_EVENTS);
410
497
  this.replayMaxChars = normalizePositiveInt(options.replayMaxChars, DEFAULT_REPLAY_MAX_CHARS);
498
+ this.startHealthWait();
411
499
  }
412
500
  static async connect(options) {
413
501
  return new _SandboxAgent(options);
@@ -425,6 +513,7 @@ var SandboxAgent = class _SandboxAgent {
425
513
  token: handle.token,
426
514
  fetch: options.fetch,
427
515
  headers: options.headers,
516
+ waitForHealth: false,
428
517
  persist: options.persist,
429
518
  replayMaxEvents: options.replayMaxEvents,
430
519
  replayMaxChars: options.replayMaxChars
@@ -433,6 +522,8 @@ var SandboxAgent = class _SandboxAgent {
433
522
  return client;
434
523
  }
435
524
  async dispose() {
525
+ this.disposed = true;
526
+ this.healthWaitAbortController.abort(createAbortError("SandboxAgent was disposed."));
436
527
  const connections = [...this.liveConnections.values()];
437
528
  this.liveConnections.clear();
438
529
  const pending = [...this.pendingLiveConnections.values()];
@@ -484,12 +575,32 @@ var SandboxAgent = class _SandboxAgent {
484
575
  agentSessionId: response.sessionId,
485
576
  lastConnectionId: live.connectionId,
486
577
  createdAt: nowMs(),
487
- sessionInit
578
+ sessionInit,
579
+ configOptions: cloneConfigOptions(response.configOptions),
580
+ modes: cloneModes(response.modes)
488
581
  };
489
582
  await this.persist.updateSession(record);
490
583
  this.nextSessionEventIndexBySession.set(record.id, 1);
491
584
  live.bindSession(record.id, record.agentSessionId);
492
- return this.upsertSessionHandle(record);
585
+ let session = this.upsertSessionHandle(record);
586
+ try {
587
+ if (request.mode) {
588
+ session = (await this.setSessionMode(session.id, request.mode)).session;
589
+ }
590
+ if (request.model) {
591
+ session = (await this.setSessionModel(session.id, request.model)).session;
592
+ }
593
+ if (request.thoughtLevel) {
594
+ session = (await this.setSessionThoughtLevel(session.id, request.thoughtLevel)).session;
595
+ }
596
+ } catch (err) {
597
+ try {
598
+ await this.destroySession(session.id);
599
+ } catch {
600
+ }
601
+ throw err;
602
+ }
603
+ return session;
493
604
  }
494
605
  async resumeSession(id) {
495
606
  const existing = await this.persist.getSession(id);
@@ -507,7 +618,9 @@ var SandboxAgent = class _SandboxAgent {
507
618
  ...existing,
508
619
  agentSessionId: recreated.sessionId,
509
620
  lastConnectionId: live.connectionId,
510
- destroyedAt: void 0
621
+ destroyedAt: void 0,
622
+ configOptions: cloneConfigOptions(recreated.configOptions),
623
+ modes: cloneModes(recreated.modes)
511
624
  };
512
625
  await this.persist.updateSession(updated);
513
626
  live.bindSession(updated.id, updated.agentSessionId);
@@ -517,15 +630,26 @@ var SandboxAgent = class _SandboxAgent {
517
630
  async resumeOrCreateSession(request) {
518
631
  const existing = await this.persist.getSession(request.id);
519
632
  if (existing) {
520
- return this.resumeSession(existing.id);
633
+ let session = await this.resumeSession(existing.id);
634
+ if (request.mode) {
635
+ session = (await this.setSessionMode(session.id, request.mode)).session;
636
+ }
637
+ if (request.model) {
638
+ session = (await this.setSessionModel(session.id, request.model)).session;
639
+ }
640
+ if (request.thoughtLevel) {
641
+ session = (await this.setSessionThoughtLevel(session.id, request.thoughtLevel)).session;
642
+ }
643
+ return session;
521
644
  }
522
645
  return this.createSession(request);
523
646
  }
524
647
  async destroySession(id) {
525
- const existing = await this.persist.getSession(id);
526
- if (!existing) {
527
- throw new Error(`session '${id}' not found`);
648
+ try {
649
+ await this.sendSessionMethodInternal(id, SESSION_CANCEL_METHOD, {}, {}, true);
650
+ } catch {
528
651
  }
652
+ const existing = await this.requireSessionRecord(id);
529
653
  const updated = {
530
654
  ...existing,
531
655
  destroyedAt: nowMs()
@@ -533,7 +657,132 @@ var SandboxAgent = class _SandboxAgent {
533
657
  await this.persist.updateSession(updated);
534
658
  return this.upsertSessionHandle(updated);
535
659
  }
660
+ async setSessionMode(sessionId, modeId) {
661
+ const mode = modeId.trim();
662
+ if (!mode) {
663
+ throw new Error("setSessionMode requires a non-empty modeId");
664
+ }
665
+ const record = await this.requireSessionRecord(sessionId);
666
+ const knownModeIds = extractKnownModeIds(record.modes);
667
+ if (knownModeIds.length > 0 && !knownModeIds.includes(mode)) {
668
+ throw new UnsupportedSessionValueError(sessionId, "mode", "mode", mode, knownModeIds);
669
+ }
670
+ try {
671
+ return await this.sendSessionMethodInternal(
672
+ sessionId,
673
+ "session/set_mode",
674
+ { modeId: mode },
675
+ {},
676
+ false
677
+ );
678
+ } catch (error) {
679
+ if (!(error instanceof AcpRpcError) || error.code !== -32601) {
680
+ throw error;
681
+ }
682
+ return this.setSessionCategoryValue(sessionId, "mode", mode);
683
+ }
684
+ }
685
+ async setSessionConfigOption(sessionId, configId, value) {
686
+ const resolvedConfigId = configId.trim();
687
+ if (!resolvedConfigId) {
688
+ throw new Error("setSessionConfigOption requires a non-empty configId");
689
+ }
690
+ const resolvedValue = value.trim();
691
+ if (!resolvedValue) {
692
+ throw new Error("setSessionConfigOption requires a non-empty value");
693
+ }
694
+ const options = await this.getSessionConfigOptions(sessionId);
695
+ const option = findConfigOptionById(options, resolvedConfigId);
696
+ if (!option) {
697
+ throw new UnsupportedSessionConfigOptionError(
698
+ sessionId,
699
+ resolvedConfigId,
700
+ options.map((item) => item.id)
701
+ );
702
+ }
703
+ const allowedValues = extractConfigValues(option);
704
+ if (allowedValues.length > 0 && !allowedValues.includes(resolvedValue)) {
705
+ throw new UnsupportedSessionValueError(
706
+ sessionId,
707
+ option.category ?? "uncategorized",
708
+ option.id,
709
+ resolvedValue,
710
+ allowedValues
711
+ );
712
+ }
713
+ return await this.sendSessionMethodInternal(
714
+ sessionId,
715
+ "session/set_config_option",
716
+ {
717
+ configId: resolvedConfigId,
718
+ value: resolvedValue
719
+ },
720
+ {},
721
+ false
722
+ );
723
+ }
724
+ async setSessionModel(sessionId, model) {
725
+ return this.setSessionCategoryValue(sessionId, "model", model);
726
+ }
727
+ async setSessionThoughtLevel(sessionId, thoughtLevel) {
728
+ return this.setSessionCategoryValue(sessionId, "thought_level", thoughtLevel);
729
+ }
730
+ async getSessionConfigOptions(sessionId) {
731
+ const record = await this.requireSessionRecord(sessionId);
732
+ const hydrated = await this.hydrateSessionConfigOptions(record.id, record);
733
+ return cloneConfigOptions(hydrated.configOptions) ?? [];
734
+ }
735
+ async getSessionModes(sessionId) {
736
+ const record = await this.requireSessionRecord(sessionId);
737
+ return cloneModes(record.modes);
738
+ }
739
+ async setSessionCategoryValue(sessionId, category, value) {
740
+ const resolvedValue = value.trim();
741
+ if (!resolvedValue) {
742
+ throw new Error(`setSession${toTitleCase(category)} requires a non-empty value`);
743
+ }
744
+ const options = await this.getSessionConfigOptions(sessionId);
745
+ const option = findConfigOptionByCategory(options, category);
746
+ if (!option) {
747
+ const categories = uniqueCategories(options);
748
+ throw new UnsupportedSessionCategoryError(sessionId, category, categories);
749
+ }
750
+ const allowedValues = extractConfigValues(option);
751
+ if (allowedValues.length > 0 && !allowedValues.includes(resolvedValue)) {
752
+ throw new UnsupportedSessionValueError(
753
+ sessionId,
754
+ category,
755
+ option.id,
756
+ resolvedValue,
757
+ allowedValues
758
+ );
759
+ }
760
+ return this.setSessionConfigOption(sessionId, option.id, resolvedValue);
761
+ }
762
+ async hydrateSessionConfigOptions(sessionId, snapshot) {
763
+ if (snapshot.configOptions !== void 0) {
764
+ return snapshot;
765
+ }
766
+ const info = await this.getAgent(snapshot.agent, { config: true });
767
+ const configOptions = normalizeSessionConfigOptions(info.configOptions) ?? [];
768
+ const record = await this.persist.getSession(sessionId);
769
+ if (!record) {
770
+ return { ...snapshot, configOptions };
771
+ }
772
+ const updated = {
773
+ ...record,
774
+ configOptions
775
+ };
776
+ await this.persist.updateSession(updated);
777
+ return updated;
778
+ }
536
779
  async sendSessionMethod(sessionId, method, params, options = {}) {
780
+ return this.sendSessionMethodInternal(sessionId, method, params, options, false);
781
+ }
782
+ async sendSessionMethodInternal(sessionId, method, params, options, allowManagedCancel) {
783
+ if (method === SESSION_CANCEL_METHOD && !allowManagedCancel) {
784
+ throw new Error(MANUAL_CANCEL_ERROR);
785
+ }
537
786
  const record = await this.persist.getSession(sessionId);
538
787
  if (!record) {
539
788
  throw new Error(`session '${sessionId}' not found`);
@@ -541,15 +790,73 @@ var SandboxAgent = class _SandboxAgent {
541
790
  const live = await this.getLiveConnection(record.agent);
542
791
  if (!live.hasBoundSession(record.id, record.agentSessionId)) {
543
792
  const restored = await this.resumeSession(record.id);
544
- return this.sendSessionMethod(restored.id, method, params, options);
793
+ return this.sendSessionMethodInternal(restored.id, method, params, options, allowManagedCancel);
545
794
  }
546
795
  const response = await live.sendSessionMethod(record.id, method, params, options);
796
+ await this.persistSessionStateFromMethod(record.id, method, params, response);
547
797
  const refreshed = await this.requireSessionRecord(record.id);
548
798
  return {
549
799
  session: this.upsertSessionHandle(refreshed),
550
800
  response
551
801
  };
552
802
  }
803
+ async persistSessionStateFromMethod(sessionId, method, params, response) {
804
+ const record = await this.persist.getSession(sessionId);
805
+ if (!record) {
806
+ return;
807
+ }
808
+ if (method === "session/set_config_option") {
809
+ const configId = typeof params.configId === "string" ? params.configId : null;
810
+ const value = typeof params.value === "string" ? params.value : null;
811
+ const updates = {};
812
+ const serverConfigOptions = extractConfigOptionsFromSetResponse(response);
813
+ if (serverConfigOptions) {
814
+ updates.configOptions = cloneConfigOptions(serverConfigOptions);
815
+ } else if (record.configOptions && configId && value) {
816
+ const updated = applyConfigOptionValue(record.configOptions, configId, value);
817
+ if (updated) {
818
+ updates.configOptions = updated;
819
+ }
820
+ }
821
+ if (configId && value) {
822
+ const source = updates.configOptions ?? record.configOptions;
823
+ const option = source ? findConfigOptionById(source, configId) : null;
824
+ if (option?.category === "mode") {
825
+ const nextModes = applyCurrentMode(record.modes, value);
826
+ if (nextModes) {
827
+ updates.modes = nextModes;
828
+ }
829
+ }
830
+ }
831
+ if (Object.keys(updates).length > 0) {
832
+ await this.persist.updateSession({ ...record, ...updates });
833
+ }
834
+ return;
835
+ }
836
+ if (method === "session/set_mode") {
837
+ const modeId = typeof params.modeId === "string" ? params.modeId : null;
838
+ if (!modeId) {
839
+ return;
840
+ }
841
+ const updates = {};
842
+ const nextModes = applyCurrentMode(record.modes, modeId);
843
+ if (nextModes) {
844
+ updates.modes = nextModes;
845
+ }
846
+ if (record.configOptions) {
847
+ const modeOption = findConfigOptionByCategory(record.configOptions, "mode");
848
+ if (modeOption) {
849
+ const updated = applyConfigOptionValue(record.configOptions, modeOption.id, modeId);
850
+ if (updated) {
851
+ updates.configOptions = updated;
852
+ }
853
+ }
854
+ }
855
+ if (Object.keys(updates).length > 0) {
856
+ await this.persist.updateSession({ ...record, ...updates });
857
+ }
858
+ }
859
+ }
553
860
  onSessionEvent(sessionId, listener) {
554
861
  const listeners = this.eventListeners.get(sessionId) ?? /* @__PURE__ */ new Set();
555
862
  listeners.add(listener);
@@ -566,16 +873,16 @@ var SandboxAgent = class _SandboxAgent {
566
873
  };
567
874
  }
568
875
  async getHealth() {
569
- return this.requestJson("GET", `${API_PREFIX}/health`);
876
+ return this.requestHealth();
570
877
  }
571
878
  async listAgents(options) {
572
879
  return this.requestJson("GET", `${API_PREFIX}/agents`, {
573
- query: options?.config ? { config: "true" } : void 0
880
+ query: toAgentQuery(options)
574
881
  });
575
882
  }
576
883
  async getAgent(agent, options) {
577
884
  return this.requestJson("GET", `${API_PREFIX}/agents/${encodeURIComponent(agent)}`, {
578
- query: options?.config ? { config: "true" } : void 0
885
+ query: toAgentQuery(options)
579
886
  });
580
887
  }
581
888
  async installAgent(agent, request = {}) {
@@ -647,7 +954,104 @@ var SandboxAgent = class _SandboxAgent {
647
954
  async deleteSkillsConfig(query) {
648
955
  await this.requestRaw("DELETE", `${API_PREFIX}/config/skills`, { query });
649
956
  }
957
+ async getProcessConfig() {
958
+ return this.requestJson("GET", `${API_PREFIX}/processes/config`);
959
+ }
960
+ async setProcessConfig(config) {
961
+ return this.requestJson("POST", `${API_PREFIX}/processes/config`, {
962
+ body: config
963
+ });
964
+ }
965
+ async createProcess(request) {
966
+ return this.requestJson("POST", `${API_PREFIX}/processes`, {
967
+ body: request
968
+ });
969
+ }
970
+ async runProcess(request) {
971
+ return this.requestJson("POST", `${API_PREFIX}/processes/run`, {
972
+ body: request
973
+ });
974
+ }
975
+ async listProcesses() {
976
+ return this.requestJson("GET", `${API_PREFIX}/processes`);
977
+ }
978
+ async getProcess(id) {
979
+ return this.requestJson("GET", `${API_PREFIX}/processes/${encodeURIComponent(id)}`);
980
+ }
981
+ async stopProcess(id, query) {
982
+ return this.requestJson("POST", `${API_PREFIX}/processes/${encodeURIComponent(id)}/stop`, {
983
+ query
984
+ });
985
+ }
986
+ async killProcess(id, query) {
987
+ return this.requestJson("POST", `${API_PREFIX}/processes/${encodeURIComponent(id)}/kill`, {
988
+ query
989
+ });
990
+ }
991
+ async deleteProcess(id) {
992
+ await this.requestRaw("DELETE", `${API_PREFIX}/processes/${encodeURIComponent(id)}`);
993
+ }
994
+ async getProcessLogs(id, query = {}) {
995
+ return this.requestJson("GET", `${API_PREFIX}/processes/${encodeURIComponent(id)}/logs`, {
996
+ query
997
+ });
998
+ }
999
+ async followProcessLogs(id, listener, query = {}) {
1000
+ const abortController = new AbortController();
1001
+ const response = await this.requestRaw(
1002
+ "GET",
1003
+ `${API_PREFIX}/processes/${encodeURIComponent(id)}/logs`,
1004
+ {
1005
+ query: { ...query, follow: true },
1006
+ accept: "text/event-stream",
1007
+ signal: abortController.signal
1008
+ }
1009
+ );
1010
+ if (!response.body) {
1011
+ abortController.abort();
1012
+ throw new Error("SSE stream is not readable in this environment.");
1013
+ }
1014
+ const closed = consumeProcessLogSse(response.body, listener, abortController.signal);
1015
+ return {
1016
+ close: () => abortController.abort(),
1017
+ closed
1018
+ };
1019
+ }
1020
+ async sendProcessInput(id, request) {
1021
+ return this.requestJson("POST", `${API_PREFIX}/processes/${encodeURIComponent(id)}/input`, {
1022
+ body: request
1023
+ });
1024
+ }
1025
+ async resizeProcessTerminal(id, request) {
1026
+ return this.requestJson(
1027
+ "POST",
1028
+ `${API_PREFIX}/processes/${encodeURIComponent(id)}/terminal/resize`,
1029
+ {
1030
+ body: request
1031
+ }
1032
+ );
1033
+ }
1034
+ buildProcessTerminalWebSocketUrl(id, options = {}) {
1035
+ return toWebSocketUrl(
1036
+ this.buildUrl(`${API_PREFIX}/processes/${encodeURIComponent(id)}/terminal/ws`, {
1037
+ access_token: options.accessToken ?? this.token
1038
+ })
1039
+ );
1040
+ }
1041
+ connectProcessTerminalWebSocket(id, options = {}) {
1042
+ const WebSocketCtor = options.WebSocket ?? globalThis.WebSocket;
1043
+ if (!WebSocketCtor) {
1044
+ throw new Error("WebSocket API is not available; provide a WebSocket implementation.");
1045
+ }
1046
+ return new WebSocketCtor(
1047
+ this.buildProcessTerminalWebSocketUrl(id, {
1048
+ accessToken: options.accessToken
1049
+ }),
1050
+ options.protocols
1051
+ );
1052
+ }
650
1053
  async getLiveConnection(agent) {
1054
+ await this.awaitHealthy();
651
1055
  const existing = this.liveConnections.get(agent);
652
1056
  if (existing) {
653
1057
  return existing;
@@ -700,6 +1104,7 @@ var SandboxAgent = class _SandboxAgent {
700
1104
  payload: cloneEnvelope(envelope)
701
1105
  };
702
1106
  await this.persist.insertEvent(event);
1107
+ await this.persistSessionStateFromEvent(localSessionId, envelope, direction);
703
1108
  const listeners = this.eventListeners.get(localSessionId);
704
1109
  if (!listeners || listeners.size === 0) {
705
1110
  return;
@@ -708,6 +1113,46 @@ var SandboxAgent = class _SandboxAgent {
708
1113
  listener(event);
709
1114
  }
710
1115
  }
1116
+ async persistSessionStateFromEvent(sessionId, envelope, direction) {
1117
+ if (direction !== "inbound") {
1118
+ return;
1119
+ }
1120
+ if (envelopeMethod(envelope) !== "session/update") {
1121
+ return;
1122
+ }
1123
+ const update = envelopeSessionUpdate(envelope);
1124
+ if (!update || typeof update.sessionUpdate !== "string") {
1125
+ return;
1126
+ }
1127
+ const record = await this.persist.getSession(sessionId);
1128
+ if (!record) {
1129
+ return;
1130
+ }
1131
+ if (update.sessionUpdate === "config_option_update") {
1132
+ const configOptions = normalizeSessionConfigOptions(update.configOptions);
1133
+ if (configOptions) {
1134
+ await this.persist.updateSession({
1135
+ ...record,
1136
+ configOptions
1137
+ });
1138
+ }
1139
+ return;
1140
+ }
1141
+ if (update.sessionUpdate === "current_mode_update") {
1142
+ const modeId = typeof update.currentModeId === "string" ? update.currentModeId : null;
1143
+ if (!modeId) {
1144
+ return;
1145
+ }
1146
+ const nextModes = applyCurrentMode(record.modes, modeId);
1147
+ if (!nextModes) {
1148
+ return;
1149
+ }
1150
+ await this.persist.updateSession({
1151
+ ...record,
1152
+ modes: nextModes
1153
+ });
1154
+ }
1155
+ }
711
1156
  async allocateSessionEventIndex(sessionId) {
712
1157
  await this.ensureSessionEventIndexSeeded(sessionId);
713
1158
  const nextIndex = this.nextSessionEventIndexBySession.get(sessionId) ?? 1;
@@ -793,7 +1238,8 @@ var SandboxAgent = class _SandboxAgent {
793
1238
  body: options.body,
794
1239
  headers: options.headers,
795
1240
  accept: options.accept ?? "application/json",
796
- signal: options.signal
1241
+ signal: options.signal,
1242
+ skipReadyWait: options.skipReadyWait
797
1243
  });
798
1244
  if (response.status === 204) {
799
1245
  return void 0;
@@ -801,6 +1247,9 @@ var SandboxAgent = class _SandboxAgent {
801
1247
  return await response.json();
802
1248
  }
803
1249
  async requestRaw(method, path, options = {}) {
1250
+ if (!options.skipReadyWait) {
1251
+ await this.awaitHealthy(options.signal);
1252
+ }
804
1253
  const url = this.buildUrl(path, options.query);
805
1254
  const headers = this.buildHeaders(options.headers);
806
1255
  if (options.accept) {
@@ -830,6 +1279,64 @@ var SandboxAgent = class _SandboxAgent {
830
1279
  }
831
1280
  return response;
832
1281
  }
1282
+ startHealthWait() {
1283
+ if (!this.healthWait.enabled || this.healthPromise) {
1284
+ return;
1285
+ }
1286
+ this.healthPromise = this.runHealthWait().catch((error) => {
1287
+ this.healthError = error instanceof Error ? error : new Error(String(error));
1288
+ });
1289
+ }
1290
+ async awaitHealthy(signal) {
1291
+ if (!this.healthPromise) {
1292
+ throwIfAborted(signal);
1293
+ return;
1294
+ }
1295
+ await waitForAbortable(this.healthPromise, signal);
1296
+ throwIfAborted(signal);
1297
+ if (this.healthError) {
1298
+ throw this.healthError;
1299
+ }
1300
+ }
1301
+ async runHealthWait() {
1302
+ const signal = this.healthWait.enabled ? anyAbortSignal([this.healthWait.signal, this.healthWaitAbortController.signal]) : void 0;
1303
+ const startedAt = Date.now();
1304
+ const deadline = typeof this.healthWait.timeoutMs === "number" ? startedAt + this.healthWait.timeoutMs : void 0;
1305
+ let delayMs = HEALTH_WAIT_MIN_DELAY_MS;
1306
+ let nextLogAt = startedAt + HEALTH_WAIT_LOG_AFTER_MS;
1307
+ let lastError;
1308
+ while (!this.disposed && (deadline === void 0 || Date.now() < deadline)) {
1309
+ throwIfAborted(signal);
1310
+ try {
1311
+ const health = await this.requestHealth({ signal });
1312
+ if (health.status === "ok") {
1313
+ return;
1314
+ }
1315
+ lastError = new Error(`Unexpected health response: ${JSON.stringify(health)}`);
1316
+ } catch (error) {
1317
+ if (isAbortError(error)) {
1318
+ throw error;
1319
+ }
1320
+ lastError = error;
1321
+ }
1322
+ const now = Date.now();
1323
+ if (now >= nextLogAt) {
1324
+ const details = formatHealthWaitError(lastError);
1325
+ console.warn(
1326
+ `sandbox-agent at ${this.baseUrl} is not healthy after ${now - startedAt}ms; still waiting (${details})`
1327
+ );
1328
+ nextLogAt = now + HEALTH_WAIT_LOG_EVERY_MS;
1329
+ }
1330
+ await sleep(delayMs, signal);
1331
+ delayMs = Math.min(HEALTH_WAIT_MAX_DELAY_MS, delayMs * 2);
1332
+ }
1333
+ if (this.disposed) {
1334
+ return;
1335
+ }
1336
+ throw new Error(
1337
+ `Timed out waiting for sandbox-agent health after ${this.healthWait.timeoutMs}ms (${formatHealthWaitError(lastError)})`
1338
+ );
1339
+ }
833
1340
  buildHeaders(extra) {
834
1341
  const headers = new Headers(this.defaultHeaders ?? void 0);
835
1342
  if (this.token) {
@@ -853,6 +1360,12 @@ var SandboxAgent = class _SandboxAgent {
853
1360
  }
854
1361
  return url.toString();
855
1362
  }
1363
+ async requestHealth(options = {}) {
1364
+ return this.requestJson("GET", `${API_PREFIX}/health`, {
1365
+ signal: options.signal,
1366
+ skipReadyWait: true
1367
+ });
1368
+ }
856
1369
  };
857
1370
  async function autoAuthenticate(acp, methods) {
858
1371
  const envBased = methods.find(
@@ -866,6 +1379,15 @@ async function autoAuthenticate(acp, methods) {
866
1379
  } catch {
867
1380
  }
868
1381
  }
1382
+ function toAgentQuery(options) {
1383
+ if (!options) {
1384
+ return void 0;
1385
+ }
1386
+ return {
1387
+ config: options.config,
1388
+ no_cache: options.noCache
1389
+ };
1390
+ }
869
1391
  function normalizeSessionInit(value) {
870
1392
  if (!value) {
871
1393
  return {
@@ -973,6 +1495,20 @@ function normalizePositiveInt(value, fallback) {
973
1495
  }
974
1496
  return Math.floor(value);
975
1497
  }
1498
+ function normalizeHealthWaitOptions(value, signal) {
1499
+ if (value === false) {
1500
+ return { enabled: false };
1501
+ }
1502
+ if (value === true || value === void 0) {
1503
+ return { enabled: true, signal };
1504
+ }
1505
+ const timeoutMs = typeof value.timeoutMs === "number" && Number.isFinite(value.timeoutMs) && value.timeoutMs > 0 ? Math.floor(value.timeoutMs) : void 0;
1506
+ return {
1507
+ enabled: true,
1508
+ signal,
1509
+ timeoutMs
1510
+ };
1511
+ }
976
1512
  function normalizeSpawnOptions(spawn, defaultEnabled) {
977
1513
  if (spawn === false) {
978
1514
  return { enabled: false };
@@ -996,9 +1532,282 @@ async function readProblem(response) {
996
1532
  return void 0;
997
1533
  }
998
1534
  }
1535
+ function normalizeSessionConfigOptions(value) {
1536
+ if (!Array.isArray(value)) {
1537
+ return void 0;
1538
+ }
1539
+ const normalized = value.filter(isSessionConfigOption);
1540
+ return cloneConfigOptions(normalized) ?? [];
1541
+ }
1542
+ function extractConfigOptionsFromSetResponse(response) {
1543
+ if (!isRecord(response)) {
1544
+ return void 0;
1545
+ }
1546
+ return normalizeSessionConfigOptions(response.configOptions);
1547
+ }
1548
+ function findConfigOptionByCategory(options, category) {
1549
+ return options.find((option) => option.category === category);
1550
+ }
1551
+ function findConfigOptionById(options, configId) {
1552
+ return options.find((option) => option.id === configId);
1553
+ }
1554
+ function uniqueCategories(options) {
1555
+ return [...new Set(options.map((option) => option.category).filter((value) => !!value))].sort();
1556
+ }
1557
+ function extractConfigValues(option) {
1558
+ if (!isRecord(option) || option.type !== "select" || !Array.isArray(option.options)) {
1559
+ return [];
1560
+ }
1561
+ const values = [];
1562
+ for (const entry of option.options) {
1563
+ if (isRecord(entry) && typeof entry.value === "string") {
1564
+ values.push(entry.value);
1565
+ continue;
1566
+ }
1567
+ if (isRecord(entry) && Array.isArray(entry.options)) {
1568
+ for (const nested of entry.options) {
1569
+ if (isRecord(nested) && typeof nested.value === "string") {
1570
+ values.push(nested.value);
1571
+ }
1572
+ }
1573
+ }
1574
+ }
1575
+ return [...new Set(values)];
1576
+ }
1577
+ function extractKnownModeIds(modes) {
1578
+ if (!modes || !Array.isArray(modes.availableModes)) {
1579
+ return [];
1580
+ }
1581
+ return modes.availableModes.map((mode) => typeof mode.id === "string" ? mode.id : null).filter((value) => !!value);
1582
+ }
1583
+ function applyCurrentMode(modes, currentModeId) {
1584
+ if (modes && Array.isArray(modes.availableModes)) {
1585
+ return {
1586
+ ...modes,
1587
+ currentModeId
1588
+ };
1589
+ }
1590
+ return {
1591
+ currentModeId,
1592
+ availableModes: []
1593
+ };
1594
+ }
1595
+ function applyConfigOptionValue(configOptions, configId, value) {
1596
+ const idx = configOptions.findIndex((o) => o.id === configId);
1597
+ if (idx === -1) {
1598
+ return null;
1599
+ }
1600
+ const updated = cloneConfigOptions(configOptions) ?? [];
1601
+ updated[idx] = { ...updated[idx], currentValue: value };
1602
+ return updated;
1603
+ }
1604
+ function envelopeSessionUpdate(message) {
1605
+ if (!isRecord(message) || !("params" in message) || !isRecord(message.params)) {
1606
+ return null;
1607
+ }
1608
+ if (!("update" in message.params) || !isRecord(message.params.update)) {
1609
+ return null;
1610
+ }
1611
+ return message.params.update;
1612
+ }
1613
+ function cloneConfigOptions(value) {
1614
+ if (!value) {
1615
+ return void 0;
1616
+ }
1617
+ return JSON.parse(JSON.stringify(value));
1618
+ }
1619
+ function cloneModes(value) {
1620
+ if (!value) {
1621
+ return null;
1622
+ }
1623
+ return JSON.parse(JSON.stringify(value));
1624
+ }
1625
+ function isSessionConfigOption(value) {
1626
+ return isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && typeof value.type === "string";
1627
+ }
1628
+ function toTitleCase(input) {
1629
+ if (!input) {
1630
+ return "";
1631
+ }
1632
+ return input.split(/[_\s-]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join("");
1633
+ }
1634
+ function formatHealthWaitError(error) {
1635
+ if (error instanceof Error && error.message) {
1636
+ return error.message;
1637
+ }
1638
+ if (error === void 0 || error === null) {
1639
+ return "unknown error";
1640
+ }
1641
+ return String(error);
1642
+ }
1643
+ function anyAbortSignal(signals) {
1644
+ const active = signals.filter((signal) => Boolean(signal));
1645
+ if (active.length === 0) {
1646
+ return void 0;
1647
+ }
1648
+ if (active.length === 1) {
1649
+ return active[0];
1650
+ }
1651
+ const controller = new AbortController();
1652
+ const onAbort = (event) => {
1653
+ cleanup();
1654
+ const signal = event.target;
1655
+ controller.abort(signal.reason ?? createAbortError());
1656
+ };
1657
+ const cleanup = () => {
1658
+ for (const signal of active) {
1659
+ signal.removeEventListener("abort", onAbort);
1660
+ }
1661
+ };
1662
+ for (const signal of active) {
1663
+ if (signal.aborted) {
1664
+ controller.abort(signal.reason ?? createAbortError());
1665
+ return controller.signal;
1666
+ }
1667
+ }
1668
+ for (const signal of active) {
1669
+ signal.addEventListener("abort", onAbort, { once: true });
1670
+ }
1671
+ return controller.signal;
1672
+ }
1673
+ function throwIfAborted(signal) {
1674
+ if (!signal?.aborted) {
1675
+ return;
1676
+ }
1677
+ throw signal.reason instanceof Error ? signal.reason : createAbortError(signal.reason);
1678
+ }
1679
+ async function waitForAbortable(promise, signal) {
1680
+ if (!signal) {
1681
+ return promise;
1682
+ }
1683
+ throwIfAborted(signal);
1684
+ return new Promise((resolve, reject) => {
1685
+ const onAbort = () => {
1686
+ cleanup();
1687
+ reject(signal.reason instanceof Error ? signal.reason : createAbortError(signal.reason));
1688
+ };
1689
+ const cleanup = () => {
1690
+ signal.removeEventListener("abort", onAbort);
1691
+ };
1692
+ signal.addEventListener("abort", onAbort, { once: true });
1693
+ promise.then(
1694
+ (value) => {
1695
+ cleanup();
1696
+ resolve(value);
1697
+ },
1698
+ (error) => {
1699
+ cleanup();
1700
+ reject(error);
1701
+ }
1702
+ );
1703
+ });
1704
+ }
1705
+ async function consumeProcessLogSse(body, listener, signal) {
1706
+ const reader = body.getReader();
1707
+ const decoder = new TextDecoder();
1708
+ let buffer = "";
1709
+ try {
1710
+ while (!signal.aborted) {
1711
+ const { done, value } = await reader.read();
1712
+ if (done) {
1713
+ return;
1714
+ }
1715
+ buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
1716
+ let separatorIndex = buffer.indexOf("\n\n");
1717
+ while (separatorIndex !== -1) {
1718
+ const chunk = buffer.slice(0, separatorIndex);
1719
+ buffer = buffer.slice(separatorIndex + 2);
1720
+ const entry = parseProcessLogSseChunk(chunk);
1721
+ if (entry) {
1722
+ listener(entry);
1723
+ }
1724
+ separatorIndex = buffer.indexOf("\n\n");
1725
+ }
1726
+ }
1727
+ } catch (error) {
1728
+ if (signal.aborted || isAbortError(error)) {
1729
+ return;
1730
+ }
1731
+ throw error;
1732
+ } finally {
1733
+ reader.releaseLock();
1734
+ }
1735
+ }
1736
+ function parseProcessLogSseChunk(chunk) {
1737
+ if (!chunk.trim()) {
1738
+ return null;
1739
+ }
1740
+ let eventName = "message";
1741
+ const dataLines = [];
1742
+ for (const line of chunk.split("\n")) {
1743
+ if (!line || line.startsWith(":")) {
1744
+ continue;
1745
+ }
1746
+ if (line.startsWith("event:")) {
1747
+ eventName = line.slice(6).trim();
1748
+ continue;
1749
+ }
1750
+ if (line.startsWith("data:")) {
1751
+ dataLines.push(line.slice(5).trimStart());
1752
+ }
1753
+ }
1754
+ if (eventName !== "log") {
1755
+ return null;
1756
+ }
1757
+ const data = dataLines.join("\n");
1758
+ if (!data.trim()) {
1759
+ return null;
1760
+ }
1761
+ return JSON.parse(data);
1762
+ }
1763
+ function toWebSocketUrl(url) {
1764
+ const parsed = new URL(url);
1765
+ if (parsed.protocol === "http:") {
1766
+ parsed.protocol = "ws:";
1767
+ } else if (parsed.protocol === "https:") {
1768
+ parsed.protocol = "wss:";
1769
+ }
1770
+ return parsed.toString();
1771
+ }
1772
+ function isAbortError(error) {
1773
+ return error instanceof Error && error.name === "AbortError";
1774
+ }
1775
+ function createAbortError(reason) {
1776
+ if (reason instanceof Error) {
1777
+ return reason;
1778
+ }
1779
+ const message = typeof reason === "string" ? reason : "This operation was aborted.";
1780
+ if (typeof DOMException !== "undefined") {
1781
+ return new DOMException(message, "AbortError");
1782
+ }
1783
+ const error = new Error(message);
1784
+ error.name = "AbortError";
1785
+ return error;
1786
+ }
1787
+ function sleep(ms, signal) {
1788
+ if (!signal) {
1789
+ return new Promise((resolve) => setTimeout(resolve, ms));
1790
+ }
1791
+ throwIfAborted(signal);
1792
+ return new Promise((resolve, reject) => {
1793
+ const timer = setTimeout(() => {
1794
+ cleanup();
1795
+ resolve();
1796
+ }, ms);
1797
+ const onAbort = () => {
1798
+ cleanup();
1799
+ reject(signal.reason instanceof Error ? signal.reason : createAbortError(signal.reason));
1800
+ };
1801
+ const cleanup = () => {
1802
+ clearTimeout(timer);
1803
+ signal.removeEventListener("abort", onAbort);
1804
+ };
1805
+ signal.addEventListener("abort", onAbort, { once: true });
1806
+ });
1807
+ }
999
1808
 
1000
1809
  // src/index.ts
1001
- import { AcpRpcError } from "acp-http-client";
1810
+ import { AcpRpcError as AcpRpcError2 } from "acp-http-client";
1002
1811
 
1003
1812
  // src/inspector.ts
1004
1813
  function buildInspectorUrl(options) {
@@ -1014,12 +1823,15 @@ function buildInspectorUrl(options) {
1014
1823
  return `${normalized}/ui/${queryString ? `?${queryString}` : ""}`;
1015
1824
  }
1016
1825
  export {
1017
- AcpRpcError,
1826
+ AcpRpcError2 as AcpRpcError,
1018
1827
  InMemorySessionPersistDriver,
1019
1828
  LiveAcpConnection,
1020
1829
  SandboxAgent,
1021
1830
  SandboxAgentError,
1022
1831
  Session,
1832
+ UnsupportedSessionCategoryError,
1833
+ UnsupportedSessionConfigOptionError,
1834
+ UnsupportedSessionValueError,
1023
1835
  buildInspectorUrl
1024
1836
  };
1025
1837
  //# sourceMappingURL=index.js.map