@band-ai/sdk 0.4.1 → 0.4.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.
@@ -5778,6 +5778,149 @@ function extractAssistantText3(event) {
5778
5778
  return blocks.map((block) => block.type === "text" ? block.text ?? "" : "").filter((text) => text.length > 0).join("\n");
5779
5779
  }
5780
5780
 
5781
+ // src/adapters/acp/sessionConfigReconciliation.ts
5782
+ var FAILURE_CODE_SESSION_CONFIG = "session_config";
5783
+ var MISSING_CONFIG_OPTIONS_REASON = "missing_config_options";
5784
+ var AcpSessionConfigTimeoutError = class extends Error {
5785
+ };
5786
+ var AcpSessionConfigError = class extends Error {
5787
+ provider;
5788
+ sessionId;
5789
+ optionId;
5790
+ selectedValue;
5791
+ acpCode;
5792
+ detail;
5793
+ timedOut;
5794
+ constructor(input) {
5795
+ super(input.message, input.cause !== void 0 ? { cause: input.cause } : void 0);
5796
+ this.name = "AcpSessionConfigError";
5797
+ this.provider = input.provider;
5798
+ this.sessionId = input.sessionId;
5799
+ this.optionId = input.optionId;
5800
+ this.selectedValue = input.selectedValue;
5801
+ this.acpCode = input.acpCode;
5802
+ this.detail = input.detail;
5803
+ this.timedOut = input.timedOut ?? false;
5804
+ }
5805
+ toAgentFailure() {
5806
+ return agentFailure(
5807
+ this.provider,
5808
+ this.message,
5809
+ this.acpCode !== void 0 ? String(this.acpCode) : FAILURE_CODE_SESSION_CONFIG,
5810
+ {
5811
+ sessionId: this.sessionId,
5812
+ optionId: this.optionId,
5813
+ selectedValue: this.selectedValue,
5814
+ detail: this.detail
5815
+ }
5816
+ );
5817
+ }
5818
+ };
5819
+ async function applySessionConfigSelections(input) {
5820
+ let catalog = input.catalog;
5821
+ for (const { configId, value: selectedValue } of sessionConfigSelectionEntries(input.selections)) {
5822
+ if (selectedValue === void 0) {
5823
+ continue;
5824
+ }
5825
+ const option = catalog.find((entry) => entry?.id === configId);
5826
+ if (!option || !isSessionConfigSelect(option)) {
5827
+ throw new AcpSessionConfigError({
5828
+ provider: input.provider,
5829
+ sessionId: input.sessionId,
5830
+ optionId: configId,
5831
+ selectedValue,
5832
+ message: `Session config option "${configId}" is not available after prior selections.`
5833
+ });
5834
+ }
5835
+ if (selectedValue === option.currentValue) {
5836
+ continue;
5837
+ }
5838
+ const availableValues = flattenConfigSelectOptions(option.options).map((entry) => entry.value);
5839
+ if (!availableValues.includes(selectedValue)) {
5840
+ throw new AcpSessionConfigError({
5841
+ provider: input.provider,
5842
+ sessionId: input.sessionId,
5843
+ optionId: configId,
5844
+ selectedValue,
5845
+ message: `Session config value "${selectedValue}" is not advertised for option "${configId}".`,
5846
+ detail: { availableValues }
5847
+ });
5848
+ }
5849
+ const timeoutMessage = `setSessionConfigOption did not respond within ${input.timeoutMs}ms`;
5850
+ try {
5851
+ const response = await withTimeout(
5852
+ input.setOption({ sessionId: input.sessionId, configId, value: selectedValue }),
5853
+ input.timeoutMs,
5854
+ () => new AcpSessionConfigTimeoutError(timeoutMessage)
5855
+ );
5856
+ if (!Array.isArray(response?.configOptions)) {
5857
+ throw new AcpSessionConfigError({
5858
+ provider: input.provider,
5859
+ sessionId: input.sessionId,
5860
+ optionId: configId,
5861
+ selectedValue,
5862
+ message: `Session config option "${configId}" response did not include a refreshed catalog.`,
5863
+ detail: { reason: MISSING_CONFIG_OPTIONS_REASON }
5864
+ });
5865
+ }
5866
+ catalog = response.configOptions;
5867
+ } catch (error) {
5868
+ if (error instanceof AcpSessionConfigError) {
5869
+ throw error;
5870
+ }
5871
+ const acpError = asAcpJsonRpcError(error);
5872
+ throw new AcpSessionConfigError({
5873
+ provider: input.provider,
5874
+ sessionId: input.sessionId,
5875
+ optionId: configId,
5876
+ selectedValue,
5877
+ acpCode: acpError?.code,
5878
+ detail: acpError?.data,
5879
+ message: acpError?.message ?? asErrorMessage(error),
5880
+ cause: error,
5881
+ timedOut: error instanceof AcpSessionConfigTimeoutError
5882
+ });
5883
+ }
5884
+ }
5885
+ return { catalog };
5886
+ }
5887
+ function sessionConfigSelectionEntries(selections) {
5888
+ if (isOrderedSessionConfigSelections(selections)) {
5889
+ return selections;
5890
+ }
5891
+ return Object.keys(selections).map((configId) => ({ configId, value: selections[configId] }));
5892
+ }
5893
+ function isOrderedSessionConfigSelections(selections) {
5894
+ return Array.isArray(selections);
5895
+ }
5896
+ function isSessionConfigSelect(option) {
5897
+ return option?.type === "select";
5898
+ }
5899
+ function flattenConfigSelectOptions(options) {
5900
+ if (!Array.isArray(options)) {
5901
+ return [];
5902
+ }
5903
+ return options.flatMap((entry) => {
5904
+ if (!asOptionalRecord(entry)) {
5905
+ return [];
5906
+ }
5907
+ if ("group" in entry) {
5908
+ return Array.isArray(entry.options) ? entry.options : [];
5909
+ }
5910
+ return [entry];
5911
+ });
5912
+ }
5913
+ function isAcpErrorResponse(error) {
5914
+ return typeof error === "object" && error !== null && typeof error.code === "number" && typeof error.message === "string";
5915
+ }
5916
+ function asAcpJsonRpcError(error) {
5917
+ if (isAcpErrorResponse(error)) {
5918
+ return error;
5919
+ }
5920
+ const nested = asOptionalRecord(error)?.error;
5921
+ return isAcpErrorResponse(nested) ? nested : void 0;
5922
+ }
5923
+
5781
5924
  // src/adapters/acp/ACPClientAdapter.ts
5782
5925
  import { spawn as spawn2 } from "child_process";
5783
5926
  import { createConnection } from "net";
@@ -6088,6 +6231,13 @@ var acpModule = new LazyAsyncValue({
6088
6231
  });
6089
6232
 
6090
6233
  // src/adapters/acp/ACPClientAdapter.ts
6234
+ function createConnectionRetirement() {
6235
+ let reject = () => void 0;
6236
+ const promise = new Promise((_resolve, rejectPromise) => {
6237
+ reject = rejectPromise;
6238
+ });
6239
+ return { promise, reject };
6240
+ }
6091
6241
  var DEFAULT_PERMISSION_TIMEOUT_MS = 5 * 6e4;
6092
6242
  var DEFAULT_TURN_TIMEOUT_MS = 60 * 6e4;
6093
6243
  var SET_SESSION_CONFIG_TIMEOUT_MS = 1e4;
@@ -6145,6 +6295,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6145
6295
  permissionTimeoutMs;
6146
6296
  turnTimeoutMs;
6147
6297
  logger;
6298
+ customSection;
6148
6299
  backend = null;
6149
6300
  backendPromise = null;
6150
6301
  client = null;
@@ -6155,6 +6306,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6155
6306
  started = false;
6156
6307
  systemPrompt = "";
6157
6308
  spawnPromise = null;
6309
+ connectionRetirements = /* @__PURE__ */ new WeakMap();
6158
6310
  // Bumped by `stop()` and on every successful spawn install. Cleanup/timeout
6159
6311
  // and permission maps key by this plus session id so a stale generation
6160
6312
  // cannot alias a same-id session on a newer connection.
@@ -6181,6 +6333,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6181
6333
  this.resolveSessionModel = options.resolveSessionModel;
6182
6334
  this.resolveSessionConfig = options.resolveSessionConfig;
6183
6335
  this.logger = resolveLogger(options.logger);
6336
+ this.customSection = options.customSection;
6184
6337
  this.permissionTimeoutMs = options.permissionTimeoutMs ?? DEFAULT_PERMISSION_TIMEOUT_MS;
6185
6338
  if ((this.resolvePermission || this.resolveSessionMode || this.resolveSessionModel || this.resolveSessionConfig) && (!Number.isFinite(this.permissionTimeoutMs) || this.permissionTimeoutMs <= 0)) {
6186
6339
  throw new ValidationError(`permissionTimeoutMs must be a positive finite number, got ${options.permissionTimeoutMs}`);
@@ -6212,7 +6365,8 @@ var ACPClientAdapter = class extends SimpleAdapter {
6212
6365
  this.systemPrompt = renderSystemPrompt({
6213
6366
  agentName,
6214
6367
  agentDescription,
6215
- includeBaseInstructions: false
6368
+ includeBaseInstructions: false,
6369
+ customSection: this.customSection
6216
6370
  });
6217
6371
  await this.ensureConnection();
6218
6372
  }
@@ -6245,13 +6399,17 @@ var ACPClientAdapter = class extends SimpleAdapter {
6245
6399
 
6246
6400
  ${messageWithContext}`;
6247
6401
  this.bootstrappedSessions.add(sessionKey);
6248
- const response = await withTimeout(connection.prompt({
6249
- sessionId,
6250
- prompt: [{
6251
- type: "text",
6252
- text: promptText
6253
- }]
6254
- }), this.turnTimeoutMs, () => new AcpTurnTimeoutError());
6402
+ const response = await withTimeout(
6403
+ this.raceAgainstConnectionRetirement(connection, connection.prompt({
6404
+ sessionId,
6405
+ prompt: [{
6406
+ type: "text",
6407
+ text: promptText
6408
+ }]
6409
+ })),
6410
+ this.turnTimeoutMs,
6411
+ () => new AcpTurnTimeoutError()
6412
+ );
6255
6413
  await this.flushChunks({
6256
6414
  client,
6257
6415
  tools,
@@ -6292,12 +6450,17 @@ ${messageWithContext}`;
6292
6450
  }
6293
6451
  }
6294
6452
  }
6295
- const acpError = asAcpJsonRpcError(error);
6453
+ const configError = error instanceof AcpSessionConfigError ? error : void 0;
6454
+ const acpError = configError ? void 0 : asAcpJsonRpcError(error);
6296
6455
  await reportTurnFailure(
6297
6456
  tools,
6298
- isTimeout ? agentFailure(this.provider, "ACP turn timed out.", FAILURE_CODE_TIMEOUT) : acpError ? agentFailure(this.provider, acpError.message, String(acpError.code), acpError.data) : agentFailure(this.provider, asErrorMessage(error)),
6457
+ isTimeout ? agentFailure(this.provider, "ACP turn timed out.", FAILURE_CODE_TIMEOUT) : configError ? configError.toAgentFailure() : acpError ? agentFailure(this.provider, acpError.message, String(acpError.code), acpError.data) : agentFailure(this.provider, asErrorMessage(error)),
6299
6458
  this.logger,
6300
- { roomId: context.roomId, sessionId }
6459
+ {
6460
+ roomId: context.roomId,
6461
+ sessionId: configError?.sessionId ?? sessionId,
6462
+ ...configError ? { optionId: configError.optionId, selectedValue: configError.selectedValue } : {}
6463
+ }
6301
6464
  );
6302
6465
  }
6303
6466
  }
@@ -6316,17 +6479,12 @@ ${messageWithContext}`;
6316
6479
  // would risk blocking this room's turn lock forever on the very process
6317
6480
  // that just proved it can hang.
6318
6481
  async abandonTimedOutTurn(connection, sessionId, generation) {
6319
- const key = this.sessionKey(generation, sessionId);
6320
- this.activeSessions.delete(key);
6321
- this.abandonedSessions.add(key);
6322
- const owner = [...this.roomToSession.entries()].find(([, value]) => value.sessionId === sessionId && value.generation === generation);
6323
- if (owner) {
6324
- this.unlinkOwner(owner[0], owner[1]);
6325
- }
6326
- abandon(
6327
- () => connection.cancel({ sessionId }),
6328
- (error) => this.safeWarn("acp_client.cancel_failed", { sessionId, error: asErrorMessage(error) })
6329
- );
6482
+ this.evictAbandonedSession(sessionId, generation, connection, () => {
6483
+ const owner = [...this.roomToSession.entries()].find(([, value]) => value.sessionId === sessionId && value.generation === generation);
6484
+ if (owner) {
6485
+ this.unlinkOwner(owner[0], owner[1]);
6486
+ }
6487
+ });
6330
6488
  }
6331
6489
  // A per-room async mutex: `fn` for a given `roomId` never overlaps another
6332
6490
  // call for that same room, while different rooms stay fully concurrent.
@@ -6452,6 +6610,11 @@ ${messageWithContext}`;
6452
6610
  void connection.closed.then(() => reject(new Error("ACP connection closed while a session operation was still in flight")));
6453
6611
  return Promise.race([operation, closedRejection]);
6454
6612
  }
6613
+ raceAgainstConnectionRetirement(connection, operation) {
6614
+ const retirement = this.connectionRetirements.get(connection) ?? createConnectionRetirement();
6615
+ this.connectionRetirements.set(connection, retirement);
6616
+ return Promise.race([operation, retirement.promise]);
6617
+ }
6455
6618
  unlinkRoom(roomId) {
6456
6619
  const owner = this.roomToSession.get(roomId);
6457
6620
  if (owner) {
@@ -6596,7 +6759,7 @@ ${messageWithContext}`;
6596
6759
  this.activeSessions.add(restoredKey);
6597
6760
  this.bootstrappedSessions.add(restoredKey);
6598
6761
  await this.configureSessionMode(roomId, existingSessionId, restored.modes, connection);
6599
- await this.configureSessionConfig(roomId, existingSessionId, restored.configOptions, connection);
6762
+ await this.configureSessionConfig(roomId, existingSessionId, restored.configOptions, connection, connectionGeneration, client);
6600
6763
  if (!this.resolveSessionConfig) {
6601
6764
  await this.configureSessionModel(roomId, existingSessionId, restored.configOptions, connection);
6602
6765
  }
@@ -6610,13 +6773,13 @@ ${messageWithContext}`;
6610
6773
  this.linkOrAbandon(roomId, created.sessionId, generation, connectionGeneration, client);
6611
6774
  this.activeSessions.add(this.sessionKey(connectionGeneration, created.sessionId));
6612
6775
  await this.configureSessionMode(roomId, created.sessionId, created.modes, connection);
6613
- await this.configureSessionConfig(roomId, created.sessionId, created.configOptions, connection);
6776
+ await this.configureSessionConfig(roomId, created.sessionId, created.configOptions, connection, connectionGeneration, client);
6614
6777
  if (!this.resolveSessionConfig) {
6615
6778
  await this.configureSessionModel(roomId, created.sessionId, created.configOptions, connection);
6616
6779
  }
6617
6780
  return created.sessionId;
6618
6781
  }
6619
- async configureSessionConfig(roomId, sessionId, configOptions, connection) {
6782
+ async configureSessionConfig(roomId, sessionId, configOptions, connection, connectionGeneration, client) {
6620
6783
  if (!this.resolveSessionConfig || !Array.isArray(configOptions) || configOptions.length === 0) {
6621
6784
  return;
6622
6785
  }
@@ -6629,35 +6792,76 @@ ${messageWithContext}`;
6629
6792
  if (!selections) {
6630
6793
  return;
6631
6794
  }
6632
- for (const option of advertisedOptions) {
6633
- const selectedValue = selections[option.id];
6634
- if (selectedValue === void 0 || selectedValue === option.currentValue || !isSessionConfigSelect(option)) {
6635
- continue;
6636
- }
6637
- const availableValues = flattenConfigSelectOptions(option.options).map((entry) => entry.value);
6638
- if (!availableValues.includes(selectedValue)) {
6639
- this.safeWarn("resolveSessionConfig selected a value this session does not advertise", {
6640
- sessionId,
6641
- configId: option.id,
6642
- selectedValue,
6643
- availableValues
6644
- });
6645
- continue;
6646
- }
6647
- try {
6648
- await withTimeout(
6649
- connection.setSessionConfigOption({ sessionId, configId: option.id, value: selectedValue }),
6650
- SET_SESSION_CONFIG_TIMEOUT_MS,
6651
- `setSessionConfigOption did not respond within ${SET_SESSION_CONFIG_TIMEOUT_MS}ms`
6652
- );
6653
- } catch (error) {
6654
- this.safeWarn("failed to switch session config option", {
6655
- sessionId,
6656
- configId: option.id,
6657
- selectedValue,
6658
- error: String(error)
6659
- });
6795
+ try {
6796
+ await applySessionConfigSelections({
6797
+ provider: this.provider,
6798
+ sessionId,
6799
+ catalog: advertisedOptions,
6800
+ selections,
6801
+ setOption: (params) => connection.setSessionConfigOption(params),
6802
+ timeoutMs: SET_SESSION_CONFIG_TIMEOUT_MS
6803
+ });
6804
+ } catch (error) {
6805
+ this.abandonFailedConfigSession(
6806
+ roomId,
6807
+ sessionId,
6808
+ connectionGeneration,
6809
+ client,
6810
+ connection,
6811
+ error instanceof AcpSessionConfigError && error.timedOut
6812
+ );
6813
+ throw error;
6814
+ }
6815
+ }
6816
+ // A config failure mid-establish must not leave a half-applied session
6817
+ // active for the room: the next turn needs a fresh `newSession` catalog.
6818
+ abandonFailedConfigSession(roomId, sessionId, connectionGeneration, client, connection, retireConnection) {
6819
+ const key = this.sessionKey(connectionGeneration, sessionId);
6820
+ this.bootstrappedSessions.delete(key);
6821
+ client.resetChunks(sessionId);
6822
+ this.evictAbandonedSession(sessionId, connectionGeneration, connection, () => {
6823
+ const owner = this.roomToSession.get(roomId);
6824
+ if (owner && owner.sessionId === sessionId && owner.generation === connectionGeneration) {
6825
+ this.unlinkOwner(roomId, owner);
6660
6826
  }
6827
+ });
6828
+ if (retireConnection) {
6829
+ this.retireConnection(connection, connectionGeneration);
6830
+ }
6831
+ }
6832
+ // Common half of timeout and config-failure abandon: mark the session
6833
+ // unusable for restore, unlink ownership, and best-effort cancel.
6834
+ evictAbandonedSession(sessionId, connectionGeneration, connection, unlink) {
6835
+ const key = this.sessionKey(connectionGeneration, sessionId);
6836
+ const wasActive = this.activeSessions.delete(key);
6837
+ if (wasActive) {
6838
+ this.abandonedSessions.add(key);
6839
+ }
6840
+ unlink();
6841
+ abandon(
6842
+ () => connection.cancel({ sessionId }),
6843
+ (error) => this.safeWarn("acp_client.cancel_failed", { sessionId, error: asErrorMessage(error) })
6844
+ );
6845
+ }
6846
+ // A timed-out config RPC means this transport has already failed to answer
6847
+ // one request. Retire it so the next turn cannot wait forever on another.
6848
+ retireConnection(connection, generation) {
6849
+ if (this.connection !== connection || this.connectionGeneration !== generation) {
6850
+ return;
6851
+ }
6852
+ const handle = this.connectionHandle;
6853
+ this.connectionGeneration++;
6854
+ this.connection = null;
6855
+ this.connectionHandle = null;
6856
+ this.connectionState = null;
6857
+ this.client = null;
6858
+ this.pruneConnectionGeneration(generation);
6859
+ this.connectionRetirements.get(connection)?.reject(new Error("ACP connection retired after a config timeout"));
6860
+ if (handle) {
6861
+ abandon(
6862
+ () => handle.stop(),
6863
+ (error) => this.safeWarn("acp_client.handle_stop_after_config_timeout", { error: asErrorMessage(error) })
6864
+ );
6661
6865
  }
6662
6866
  }
6663
6867
  // The single gate an establishment must pass before it's allowed to claim
@@ -7271,39 +7475,12 @@ async function createTcpConnection(client, endpoint, signal) {
7271
7475
  };
7272
7476
  }
7273
7477
  var MODEL_CONFIG_OPTION_KEY = "model";
7274
- function isSessionConfigSelect(option) {
7275
- return option?.type === "select";
7276
- }
7277
7478
  function isModelConfigOption(option) {
7278
7479
  return isSessionConfigSelect(option) && option.category === MODEL_CONFIG_OPTION_KEY;
7279
7480
  }
7280
7481
  function isModelConfigOptionById(option) {
7281
7482
  return isSessionConfigSelect(option) && option.id === MODEL_CONFIG_OPTION_KEY;
7282
7483
  }
7283
- function flattenConfigSelectOptions(options) {
7284
- if (!Array.isArray(options)) {
7285
- return [];
7286
- }
7287
- return options.flatMap((entry) => {
7288
- if (!asOptionalRecord(entry)) {
7289
- return [];
7290
- }
7291
- if ("group" in entry) {
7292
- return Array.isArray(entry.options) ? entry.options : [];
7293
- }
7294
- return [entry];
7295
- });
7296
- }
7297
- function isAcpErrorResponse(error) {
7298
- return typeof error === "object" && error !== null && typeof error.code === "number" && typeof error.message === "string";
7299
- }
7300
- function asAcpJsonRpcError(error) {
7301
- if (isAcpErrorResponse(error)) {
7302
- return error;
7303
- }
7304
- const nested = asOptionalRecord(error)?.error;
7305
- return isAcpErrorResponse(nested) ? nested : void 0;
7306
- }
7307
7484
 
7308
7485
  // src/adapters/acp/BandACPServerAdapter.ts
7309
7486
  import { randomUUID as randomUUID2 } from "crypto";
@@ -8153,6 +8330,10 @@ export {
8153
8330
  HttpOpencodeClient,
8154
8331
  OpencodeAdapter,
8155
8332
  ClaudeSDKAdapter,
8333
+ FAILURE_CODE_SESSION_CONFIG,
8334
+ MISSING_CONFIG_OPTIONS_REASON,
8335
+ AcpSessionConfigError,
8336
+ applySessionConfigSelections,
8156
8337
  ACPClientAdapter,
8157
8338
  BandACPServerAdapter,
8158
8339
  ACPServer,