@band-ai/sdk 0.4.0 → 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;
@@ -6141,9 +6291,11 @@ var ACPClientAdapter = class extends SimpleAdapter {
6141
6291
  resolvePermission;
6142
6292
  resolveSessionMode;
6143
6293
  resolveSessionModel;
6294
+ resolveSessionConfig;
6144
6295
  permissionTimeoutMs;
6145
6296
  turnTimeoutMs;
6146
6297
  logger;
6298
+ customSection;
6147
6299
  backend = null;
6148
6300
  backendPromise = null;
6149
6301
  client = null;
@@ -6154,6 +6306,7 @@ var ACPClientAdapter = class extends SimpleAdapter {
6154
6306
  started = false;
6155
6307
  systemPrompt = "";
6156
6308
  spawnPromise = null;
6309
+ connectionRetirements = /* @__PURE__ */ new WeakMap();
6157
6310
  // Bumped by `stop()` and on every successful spawn install. Cleanup/timeout
6158
6311
  // and permission maps key by this plus session id so a stale generation
6159
6312
  // cannot alias a same-id session on a newer connection.
@@ -6178,12 +6331,14 @@ var ACPClientAdapter = class extends SimpleAdapter {
6178
6331
  this.resolvePermission = options.resolvePermission;
6179
6332
  this.resolveSessionMode = options.resolveSessionMode;
6180
6333
  this.resolveSessionModel = options.resolveSessionModel;
6334
+ this.resolveSessionConfig = options.resolveSessionConfig;
6181
6335
  this.logger = resolveLogger(options.logger);
6336
+ this.customSection = options.customSection;
6182
6337
  this.permissionTimeoutMs = options.permissionTimeoutMs ?? DEFAULT_PERMISSION_TIMEOUT_MS;
6183
- if ((this.resolvePermission || this.resolveSessionMode || this.resolveSessionModel) && (!Number.isFinite(this.permissionTimeoutMs) || this.permissionTimeoutMs <= 0)) {
6338
+ if ((this.resolvePermission || this.resolveSessionMode || this.resolveSessionModel || this.resolveSessionConfig) && (!Number.isFinite(this.permissionTimeoutMs) || this.permissionTimeoutMs <= 0)) {
6184
6339
  throw new ValidationError(`permissionTimeoutMs must be a positive finite number, got ${options.permissionTimeoutMs}`);
6185
6340
  }
6186
- if (this.resolvePermission || this.resolveSessionMode || this.resolveSessionModel) {
6341
+ if (this.resolvePermission || this.resolveSessionMode || this.resolveSessionModel || this.resolveSessionConfig) {
6187
6342
  assertWithinSetTimeoutBound(
6188
6343
  `permissionTimeoutMs must be at most ${MAX_SETTIMEOUT_DELAY_MS}, got ${options.permissionTimeoutMs}`,
6189
6344
  this.permissionTimeoutMs
@@ -6210,7 +6365,8 @@ var ACPClientAdapter = class extends SimpleAdapter {
6210
6365
  this.systemPrompt = renderSystemPrompt({
6211
6366
  agentName,
6212
6367
  agentDescription,
6213
- includeBaseInstructions: false
6368
+ includeBaseInstructions: false,
6369
+ customSection: this.customSection
6214
6370
  });
6215
6371
  await this.ensureConnection();
6216
6372
  }
@@ -6243,13 +6399,17 @@ var ACPClientAdapter = class extends SimpleAdapter {
6243
6399
 
6244
6400
  ${messageWithContext}`;
6245
6401
  this.bootstrappedSessions.add(sessionKey);
6246
- const response = await withTimeout(connection.prompt({
6247
- sessionId,
6248
- prompt: [{
6249
- type: "text",
6250
- text: promptText
6251
- }]
6252
- }), 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
+ );
6253
6413
  await this.flushChunks({
6254
6414
  client,
6255
6415
  tools,
@@ -6290,12 +6450,17 @@ ${messageWithContext}`;
6290
6450
  }
6291
6451
  }
6292
6452
  }
6293
- const acpError = asAcpJsonRpcError(error);
6453
+ const configError = error instanceof AcpSessionConfigError ? error : void 0;
6454
+ const acpError = configError ? void 0 : asAcpJsonRpcError(error);
6294
6455
  await reportTurnFailure(
6295
6456
  tools,
6296
- 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)),
6297
6458
  this.logger,
6298
- { roomId: context.roomId, sessionId }
6459
+ {
6460
+ roomId: context.roomId,
6461
+ sessionId: configError?.sessionId ?? sessionId,
6462
+ ...configError ? { optionId: configError.optionId, selectedValue: configError.selectedValue } : {}
6463
+ }
6299
6464
  );
6300
6465
  }
6301
6466
  }
@@ -6314,17 +6479,12 @@ ${messageWithContext}`;
6314
6479
  // would risk blocking this room's turn lock forever on the very process
6315
6480
  // that just proved it can hang.
6316
6481
  async abandonTimedOutTurn(connection, sessionId, generation) {
6317
- const key = this.sessionKey(generation, sessionId);
6318
- this.activeSessions.delete(key);
6319
- this.abandonedSessions.add(key);
6320
- const owner = [...this.roomToSession.entries()].find(([, value]) => value.sessionId === sessionId && value.generation === generation);
6321
- if (owner) {
6322
- this.unlinkOwner(owner[0], owner[1]);
6323
- }
6324
- abandon(
6325
- () => connection.cancel({ sessionId }),
6326
- (error) => this.safeWarn("acp_client.cancel_failed", { sessionId, error: asErrorMessage(error) })
6327
- );
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
+ });
6328
6488
  }
6329
6489
  // A per-room async mutex: `fn` for a given `roomId` never overlaps another
6330
6490
  // call for that same room, while different rooms stay fully concurrent.
@@ -6450,6 +6610,11 @@ ${messageWithContext}`;
6450
6610
  void connection.closed.then(() => reject(new Error("ACP connection closed while a session operation was still in flight")));
6451
6611
  return Promise.race([operation, closedRejection]);
6452
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
+ }
6453
6618
  unlinkRoom(roomId) {
6454
6619
  const owner = this.roomToSession.get(roomId);
6455
6620
  if (owner) {
@@ -6594,7 +6759,10 @@ ${messageWithContext}`;
6594
6759
  this.activeSessions.add(restoredKey);
6595
6760
  this.bootstrappedSessions.add(restoredKey);
6596
6761
  await this.configureSessionMode(roomId, existingSessionId, restored.modes, connection);
6597
- await this.configureSessionModel(roomId, existingSessionId, restored.configOptions, connection);
6762
+ await this.configureSessionConfig(roomId, existingSessionId, restored.configOptions, connection, connectionGeneration, client);
6763
+ if (!this.resolveSessionConfig) {
6764
+ await this.configureSessionModel(roomId, existingSessionId, restored.configOptions, connection);
6765
+ }
6598
6766
  return existingSessionId;
6599
6767
  }
6600
6768
  }
@@ -6605,9 +6773,97 @@ ${messageWithContext}`;
6605
6773
  this.linkOrAbandon(roomId, created.sessionId, generation, connectionGeneration, client);
6606
6774
  this.activeSessions.add(this.sessionKey(connectionGeneration, created.sessionId));
6607
6775
  await this.configureSessionMode(roomId, created.sessionId, created.modes, connection);
6608
- await this.configureSessionModel(roomId, created.sessionId, created.configOptions, connection);
6776
+ await this.configureSessionConfig(roomId, created.sessionId, created.configOptions, connection, connectionGeneration, client);
6777
+ if (!this.resolveSessionConfig) {
6778
+ await this.configureSessionModel(roomId, created.sessionId, created.configOptions, connection);
6779
+ }
6609
6780
  return created.sessionId;
6610
6781
  }
6782
+ async configureSessionConfig(roomId, sessionId, configOptions, connection, connectionGeneration, client) {
6783
+ if (!this.resolveSessionConfig || !Array.isArray(configOptions) || configOptions.length === 0) {
6784
+ return;
6785
+ }
6786
+ const advertisedOptions = configOptions;
6787
+ const selections = await this.resolveManualSelection(
6788
+ "resolveSessionConfig",
6789
+ (signal) => this.resolveSessionConfig({ roomId, sessionId, configOptions: advertisedOptions }, signal),
6790
+ connection.signal
6791
+ );
6792
+ if (!selections) {
6793
+ return;
6794
+ }
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);
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
+ );
6865
+ }
6866
+ }
6611
6867
  // The single gate an establishment must pass before it's allowed to claim
6612
6868
  // the room: it must still be the room's current generation (not
6613
6869
  // superseded by a teardown or a fresher establishment while this one was
@@ -7219,39 +7475,12 @@ async function createTcpConnection(client, endpoint, signal) {
7219
7475
  };
7220
7476
  }
7221
7477
  var MODEL_CONFIG_OPTION_KEY = "model";
7222
- function isSessionConfigSelect(option) {
7223
- return option?.type === "select";
7224
- }
7225
7478
  function isModelConfigOption(option) {
7226
7479
  return isSessionConfigSelect(option) && option.category === MODEL_CONFIG_OPTION_KEY;
7227
7480
  }
7228
7481
  function isModelConfigOptionById(option) {
7229
7482
  return isSessionConfigSelect(option) && option.id === MODEL_CONFIG_OPTION_KEY;
7230
7483
  }
7231
- function flattenConfigSelectOptions(options) {
7232
- if (!Array.isArray(options)) {
7233
- return [];
7234
- }
7235
- return options.flatMap((entry) => {
7236
- if (!asOptionalRecord(entry)) {
7237
- return [];
7238
- }
7239
- if ("group" in entry) {
7240
- return Array.isArray(entry.options) ? entry.options : [];
7241
- }
7242
- return [entry];
7243
- });
7244
- }
7245
- function isAcpErrorResponse(error) {
7246
- return typeof error === "object" && error !== null && typeof error.code === "number" && typeof error.message === "string";
7247
- }
7248
- function asAcpJsonRpcError(error) {
7249
- if (isAcpErrorResponse(error)) {
7250
- return error;
7251
- }
7252
- const nested = asOptionalRecord(error)?.error;
7253
- return isAcpErrorResponse(nested) ? nested : void 0;
7254
- }
7255
7484
 
7256
7485
  // src/adapters/acp/BandACPServerAdapter.ts
7257
7486
  import { randomUUID as randomUUID2 } from "crypto";
@@ -8101,6 +8330,10 @@ export {
8101
8330
  HttpOpencodeClient,
8102
8331
  OpencodeAdapter,
8103
8332
  ClaudeSDKAdapter,
8333
+ FAILURE_CODE_SESSION_CONFIG,
8334
+ MISSING_CONFIG_OPTIONS_REASON,
8335
+ AcpSessionConfigError,
8336
+ applySessionConfigSelections,
8104
8337
  ACPClientAdapter,
8105
8338
  BandACPServerAdapter,
8106
8339
  ACPServer,