@genesislcap/ai-assistant 15.24.0 → 15.24.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.
@@ -4329,6 +4329,78 @@ function accumulate(messages, into) {
4329
4329
  // src/utils/tool-fold.ts
4330
4330
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
4331
4331
 
4332
+ // src/components/chat-driver/turn-failures.ts
4333
+ var TURN_FAILURE_EXITS = {
4334
+ contextGuard: {
4335
+ subReason: "context_exhausted",
4336
+ resultReason: "context-exhausted",
4337
+ copy: "I had to stop here \u2014 this conversation has filled the available context. Compact it to free up room, then ask me to continue."
4338
+ },
4339
+ contextOverflow: {
4340
+ subReason: "context_exhausted",
4341
+ resultReason: "context-exhausted",
4342
+ copy: DEFAULT_CONTEXT_OVERFLOW_MESSAGE
4343
+ },
4344
+ malformedToolCall: {
4345
+ subReason: "malformed_tool_call",
4346
+ // Not `malformed-tool-call`: the main-turn reason is named after the provider finish
4347
+ // reason (`MALFORMED_FUNCTION_CALL`) that produces it, and is part of the published
4348
+ // TurnFailureReason union, so the two names cannot be brought into line here.
4349
+ resultReason: "malformed-function-call",
4350
+ copy: "While working on your request, I repeatedly called my tools incorrectly. This often works on a second try \u2014 would you like me to try again? If it happens again, try breaking your request into smaller steps."
4351
+ },
4352
+ responseTruncated: {
4353
+ subReason: "response_truncated",
4354
+ resultReason: "response-truncated",
4355
+ copy: "My response was cut off because a single step reached the model output limit. This usually means one step tried to produce too much at once \u2014 try breaking your request into smaller steps."
4356
+ },
4357
+ timeout: {
4358
+ subReason: "timeout",
4359
+ // There is no `timeout` member of TurnFailureReason, so the main turn reports the
4360
+ // generic `exception`. A sub-agent gets the specific reason because its parent
4361
+ // branches on it to decide whether a retry is worth attempting.
4362
+ resultReason: "exception",
4363
+ copy: "The request timed out. You can ask me to try again, or break this into a smaller step."
4364
+ },
4365
+ unknownToolLimit: {
4366
+ subReason: "unknown_tool_limit",
4367
+ resultReason: "unknown-tool-limit",
4368
+ copy: "I'm sorry, I repeatedly tried to use tools that aren't available to me, so I couldn't complete that. If a 'Download agent log' option appears in the Settings (cog) menu, you can download the log and share it with whoever set up this assistant to help fix the issue."
4369
+ },
4370
+ maxIterations: {
4371
+ subReason: "max_iterations",
4372
+ resultReason: "max-iterations",
4373
+ copy: "I've reached my limit for this response. You can ask me to continue and I'll pick up where I left off."
4374
+ },
4375
+ refusal: {
4376
+ subReason: "refusal",
4377
+ resultReason: "refusal",
4378
+ copy: "This request was declined. Try rephrasing it, or breaking it into smaller steps."
4379
+ },
4380
+ emptyResponse: {
4381
+ subReason: "empty_response",
4382
+ resultReason: "empty-response",
4383
+ copy: "While working on your request, I repeatedly generated a blank response. This often works on a second try \u2014 would you like me to try again? If it happens again, try breaking your request into smaller steps."
4384
+ },
4385
+ providerRefused: {
4386
+ subReason: "provider_refused",
4387
+ resultReason: "provider-refused",
4388
+ // Overridden per wall by `ChatDriver.providerRefusedBubble`, which prefers a
4389
+ // host-supplied message; this is the fallback that method returns.
4390
+ copy: DEFAULT_PROVIDER_REFUSED_MESSAGE
4391
+ },
4392
+ budgetExhausted: {
4393
+ subReason: "budget_exhausted",
4394
+ resultReason: "budget-exhausted",
4395
+ // Overridden per wall by `ChatDriver.budgetExhaustedBubble`, which upgrades this with
4396
+ // "switch to another AI" when a second vendor is reachable.
4397
+ copy: DEFAULT_BUDGET_EXHAUSTED_MESSAGE
4398
+ }
4399
+ // `satisfies`, not an annotation: an annotation would widen `resultReason` to
4400
+ // `TurnFailureReason` and cost the literal types that let `turn-failures.test.ts` pin the
4401
+ // two deliberate divergences at compile time instead of at runtime.
4402
+ };
4403
+
4332
4404
  // src/components/chat-driver/chat-driver.ts
4333
4405
  var toolResultImages = (result) => {
4334
4406
  if (!result || typeof result !== "object") return void 0;
@@ -4703,6 +4775,33 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4703
4775
  ...providerRefused ? { providerRefused } : {}
4704
4776
  };
4705
4777
  }
4778
+ /**
4779
+ * End the turn on a terminal failure: fail upward when this driver is a sub-agent, speak
4780
+ * to the user when it is not, and hand the reason to the caller either way.
4781
+ *
4782
+ * Only that fork lives here. Each exit keeps its own `recordTurnError` call, because the
4783
+ * telemetry payload is what legitimately differs between them — attempt counts, token
4784
+ * figures, tool names — and folding it in would mean one optional field per condition.
4785
+ *
4786
+ * `overrides.copy` serves the two walls whose prose is composed per occurrence:
4787
+ * `budgetExhaustedBubble` names the vendor and can offer another, `providerRefusedBubble`
4788
+ * prefers a host-supplied message. Every other exit reads its copy from the table.
4789
+ *
4790
+ * One exit stays out: the `BudgetExhaustedError` catch. Not because its shape differs —
4791
+ * its two telemetry payloads are field-for-field identical — but because its main-turn
4792
+ * branch calls `reportBudgetExhausted`, which is public and also serves
4793
+ * `OrchestratingDriver`'s classification phase, where no tool loop is running. That
4794
+ * method has to survive either way, so routing the catch through here would relocate the
4795
+ * duplication rather than remove it.
4796
+ */
4797
+ endTurnWithFailure(exit, overrides) {
4798
+ if (this.isSubAgent) {
4799
+ this.failSubAgent(exit.subReason, overrides?.budget, overrides?.providerRefused);
4800
+ } else {
4801
+ this.appendDriverNotice(overrides?.copy ?? exit.copy);
4802
+ }
4803
+ return this.turnDone(exit.resultReason, overrides?.budget, overrides?.providerRefused);
4804
+ }
4706
4805
  /**
4707
4806
  * Terminal budget outcome for a wall hit **outside** the tool loop — today,
4708
4807
  * `OrchestratingDriver`'s classification phase, which calls the provider
@@ -4731,7 +4830,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4731
4830
  */
4732
4831
  budgetExhaustedBubble(wall) {
4733
4832
  if (this.budgetExhaustedMessageOverride != null) return this.budgetExhaustedMessageOverride;
4734
- if (wall?.otherVendorAvailable !== true) return DEFAULT_BUDGET_EXHAUSTED_MESSAGE;
4833
+ if (wall?.otherVendorAvailable !== true) return TURN_FAILURE_EXITS.budgetExhausted.copy;
4735
4834
  const subject = wall?.vendorLabel ? `${wall.vendorLabel}'s AI usage limit` : "your AI usage limit";
4736
4835
  return `You've reached ${subject}. Switch to another AI provider in Settings to keep going, or contact your administrator to raise it.`;
4737
4836
  }
@@ -4765,32 +4864,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4765
4864
  * has gone unpaid — so there is nothing here to compose. A host override wins verbatim.
4766
4865
  */
4767
4866
  providerRefusedBubble() {
4768
- return this.providerRefusedMessageOverride ?? DEFAULT_PROVIDER_REFUSED_MESSAGE;
4769
- }
4770
- /**
4771
- * Terminal provider-refusal outcome. Mirrors `reportBudgetExhausted` so the two walls behave
4772
- * identically from the caller's side, while keeping their diagnostics distinct.
4773
- *
4774
- * `kind` reaches the debug log and the result but never the transcript: it is what lets an operator
4775
- * tell "top up the account" from "rotate the key", and with cause-free user copy this is the only
4776
- * place that distinction survives.
4777
- */
4778
- reportProviderRefused(e) {
4779
- const detail = providerRefusedDetailOf(e);
4780
- this.providerRefusedThisTurn = true;
4781
- this.providerRefusedDetail = detail;
4782
- logger2.error("ChatDriver: provider refused the request", e);
4783
- recordTurnError(this.sessionKey, "provider-refused", {
4784
- agent: this.activeAgentName,
4785
- provider: this.lastResolvedProviderName,
4786
- vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
4787
- kind: e.kind,
4788
- upstreamStatus: e.upstreamStatus,
4789
- upstreamType: e.upstreamType,
4790
- isSubAgent: this.isSubAgent
4791
- });
4792
- this.appendDriverNotice(this.providerRefusedBubble());
4793
- return this.turnDone("provider-refused", void 0, detail);
4867
+ return this.providerRefusedMessageOverride ?? TURN_FAILURE_EXITS.providerRefused.copy;
4794
4868
  }
4795
4869
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
4796
4870
  static failureReasonOf(result) {
@@ -6081,12 +6155,10 @@ Output format (strict):
6081
6155
  via: "sub-agent",
6082
6156
  isSubAgent: this.isSubAgent
6083
6157
  });
6084
- if (this.isSubAgent) {
6085
- this.failSubAgent("provider_refused", void 0, this.providerRefusedDetail);
6086
- } else {
6087
- this.appendDriverNotice(this.providerRefusedBubble());
6088
- }
6089
- return this.turnDone("provider-refused", void 0, this.providerRefusedDetail);
6158
+ return this.endTurnWithFailure(TURN_FAILURE_EXITS.providerRefused, {
6159
+ copy: this.providerRefusedBubble(),
6160
+ providerRefused: this.providerRefusedDetail
6161
+ });
6090
6162
  }
6091
6163
  if (this.budgetExhaustedThisTurn) {
6092
6164
  logger2.error("ChatDriver: ending the turn \u2014 a sub-agent hit the AI budget wall");
@@ -6106,12 +6178,10 @@ Output format (strict):
6106
6178
  via: "sub-agent",
6107
6179
  isSubAgent: this.isSubAgent
6108
6180
  });
6109
- if (this.isSubAgent) {
6110
- this.failSubAgent("budget_exhausted", this.budgetWallDetail);
6111
- } else {
6112
- this.appendDriverNotice(this.budgetExhaustedBubble(this.budgetWallDetail));
6113
- }
6114
- return this.turnDone("budget-exhausted", this.budgetWallDetail);
6181
+ return this.endTurnWithFailure(TURN_FAILURE_EXITS.budgetExhausted, {
6182
+ copy: this.budgetExhaustedBubble(this.budgetWallDetail),
6183
+ budget: this.budgetWallDetail
6184
+ });
6115
6185
  }
6116
6186
  const promptCtx = {
6117
6187
  agentName: this.activeAgentName ?? "",
@@ -6276,14 +6346,7 @@ ${tailBody}
6276
6346
  iterations,
6277
6347
  isSubAgent: this.isSubAgent
6278
6348
  });
6279
- if (this.isSubAgent) {
6280
- this.failSubAgent("context_exhausted");
6281
- } else {
6282
- this.appendDriverNotice(
6283
- "I had to stop here \u2014 this conversation has filled the available context. Compact it to free up room, then ask me to continue."
6284
- );
6285
- }
6286
- return this.turnDone("context-exhausted");
6349
+ return this.endTurnWithFailure(TURN_FAILURE_EXITS.contextGuard);
6287
6350
  }
6288
6351
  let response;
6289
6352
  try {
@@ -6313,14 +6376,7 @@ ${tailBody}
6313
6376
  finishMessage: e.finishMessage,
6314
6377
  isSubAgent: this.isSubAgent
6315
6378
  });
6316
- if (this.isSubAgent) {
6317
- this.failSubAgent("malformed_tool_call");
6318
- } else {
6319
- this.appendDriverNotice(
6320
- "While working on your request, I repeatedly called my tools incorrectly. This often works on a second try \u2014 would you like me to try again? If it happens again, try breaking your request into smaller steps."
6321
- );
6322
- }
6323
- return this.turnDone("malformed-function-call");
6379
+ return this.endTurnWithFailure(TURN_FAILURE_EXITS.malformedToolCall);
6324
6380
  }
6325
6381
  if (e instanceof ResponseTruncatedError) {
6326
6382
  logger2.error("ChatDriver: response truncated at the output-token cap", e);
@@ -6333,14 +6389,7 @@ ${tailBody}
6333
6389
  tools: e.toolNames,
6334
6390
  isSubAgent: this.isSubAgent
6335
6391
  });
6336
- if (this.isSubAgent) {
6337
- this.failSubAgent("response_truncated");
6338
- } else {
6339
- this.appendDriverNotice(
6340
- "My response was cut off because a single step reached the model output limit. This usually means one step tried to produce too much at once \u2014 try breaking your request into smaller steps."
6341
- );
6342
- }
6343
- return this.turnDone("response-truncated");
6392
+ return this.endTurnWithFailure(TURN_FAILURE_EXITS.responseTruncated);
6344
6393
  }
6345
6394
  if (e instanceof ContextOverflowError) {
6346
6395
  logger2.error("ChatDriver: the prompt exceeded the model context window", e);
@@ -6353,30 +6402,28 @@ ${tailBody}
6353
6402
  via: "provider",
6354
6403
  isSubAgent: this.isSubAgent
6355
6404
  });
6356
- if (this.isSubAgent) {
6357
- this.failSubAgent("context_exhausted");
6358
- } else {
6359
- this.appendDriverNotice(DEFAULT_CONTEXT_OVERFLOW_MESSAGE);
6360
- }
6361
- return this.turnDone("context-exhausted");
6405
+ return this.endTurnWithFailure(TURN_FAILURE_EXITS.contextOverflow);
6362
6406
  }
6363
6407
  if (e instanceof ProviderRefusedError) {
6364
- if (this.isSubAgent) {
6365
- logger2.error("ChatDriver: provider refused the request", e);
6366
- recordTurnError(this.sessionKey, "provider-refused", {
6367
- agent: this.activeAgentName,
6368
- provider: this.lastResolvedProviderName,
6369
- vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
6370
- kind: e.kind,
6371
- upstreamStatus: e.upstreamStatus,
6372
- upstreamType: e.upstreamType,
6373
- isSubAgent: true
6374
- });
6375
- const detail = providerRefusedDetailOf(e);
6376
- this.failSubAgent("provider_refused", void 0, detail);
6377
- return this.turnDone("provider-refused", void 0, detail);
6408
+ const detail = providerRefusedDetailOf(e);
6409
+ if (!this.isSubAgent) {
6410
+ this.providerRefusedThisTurn = true;
6411
+ this.providerRefusedDetail = detail;
6378
6412
  }
6379
- return this.reportProviderRefused(e);
6413
+ logger2.error("ChatDriver: provider refused the request", e);
6414
+ recordTurnError(this.sessionKey, "provider-refused", {
6415
+ agent: this.activeAgentName,
6416
+ provider: this.lastResolvedProviderName,
6417
+ vendor: vendorTypeOfLabel(e.vendorLabel) ?? this.lastResolvedProvider,
6418
+ kind: e.kind,
6419
+ upstreamStatus: e.upstreamStatus,
6420
+ upstreamType: e.upstreamType,
6421
+ isSubAgent: this.isSubAgent
6422
+ });
6423
+ return this.endTurnWithFailure(TURN_FAILURE_EXITS.providerRefused, {
6424
+ copy: this.providerRefusedBubble(),
6425
+ providerRefused: detail
6426
+ });
6380
6427
  }
6381
6428
  if (e instanceof BudgetExhaustedError) {
6382
6429
  this.budgetExhaustedThisTurn = true;
@@ -6406,14 +6453,7 @@ ${tailBody}
6406
6453
  message: e.message,
6407
6454
  isSubAgent: this.isSubAgent
6408
6455
  });
6409
- if (this.isSubAgent) {
6410
- this.failSubAgent("timeout");
6411
- } else {
6412
- this.appendDriverNotice(
6413
- "The request timed out. You can ask me to try again, or break this into a smaller step."
6414
- );
6415
- }
6416
- return this.turnDone("exception");
6456
+ return this.endTurnWithFailure(TURN_FAILURE_EXITS.timeout);
6417
6457
  }
6418
6458
  if (e instanceof DOMException && e.name === "AbortError") {
6419
6459
  return this.completeAbortedTurn();
@@ -6465,11 +6505,11 @@ ${tailBody}
6465
6505
  iterations -= 1;
6466
6506
  continue;
6467
6507
  }
6468
- const failureReason2 = isRefusal ? "refusal" : "empty-response";
6508
+ const exit = isRefusal ? TURN_FAILURE_EXITS.refusal : TURN_FAILURE_EXITS.emptyResponse;
6469
6509
  logger2.error(
6470
6510
  isRefusal ? "ChatDriver: model refused the request (safety decline) \u2014 not retrying" : "ChatDriver: empty model response after all retries"
6471
6511
  );
6472
- recordTurnError(this.sessionKey, failureReason2, {
6512
+ recordTurnError(this.sessionKey, exit.resultReason, {
6473
6513
  agent: this.activeAgentName,
6474
6514
  provider: this.lastResolvedProviderName,
6475
6515
  attempts: emptyResponseAttempts,
@@ -6479,14 +6519,7 @@ ${tailBody}
6479
6519
  isSubAgent: this.isSubAgent,
6480
6520
  ...response.responseMeta
6481
6521
  });
6482
- if (this.isSubAgent) {
6483
- this.failSubAgent(isRefusal ? "refusal" : "empty_response");
6484
- } else {
6485
- this.appendDriverNotice(
6486
- isRefusal ? "This request was declined. Try rephrasing it, or breaking it into smaller steps." : "While working on your request, I repeatedly generated a blank response. This often works on a second try \u2014 would you like me to try again? If it happens again, try breaking your request into smaller steps."
6487
- );
6488
- }
6489
- return this.turnDone(failureReason2);
6522
+ return this.endTurnWithFailure(exit);
6490
6523
  } else {
6491
6524
  const { reasoning, ...rest } = response;
6492
6525
  const displayOnly = {
@@ -6786,14 +6819,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6786
6819
  availableTools: Object.keys(this.toolHandlers),
6787
6820
  isSubAgent: this.isSubAgent
6788
6821
  });
6789
- if (this.isSubAgent) {
6790
- this.failSubAgent("unknown_tool_limit");
6791
- } else {
6792
- this.appendDriverNotice(
6793
- "I'm sorry, I repeatedly tried to use tools that aren't available to me, so I couldn't complete that. If a 'Download agent log' option appears in the Settings (cog) menu, you can download the log and share it with whoever set up this assistant to help fix the issue."
6794
- );
6795
- }
6796
- return this.turnDone("unknown-tool-limit");
6822
+ return this.endTurnWithFailure(TURN_FAILURE_EXITS.unknownToolLimit);
6797
6823
  }
6798
6824
  const firstContinuation = systemCalls[0];
6799
6825
  if (firstContinuation) {
@@ -6804,7 +6830,6 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6804
6830
  return this.turnDone();
6805
6831
  }
6806
6832
  }
6807
- let failureReason;
6808
6833
  if (iterations >= this.maxToolIterations) {
6809
6834
  logger2.warn("ChatDriver: reached max tool iterations, stopping");
6810
6835
  recordTurnError(this.sessionKey, "max-iterations", {
@@ -6814,16 +6839,9 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6814
6839
  limit: this.maxToolIterations,
6815
6840
  isSubAgent: this.isSubAgent
6816
6841
  });
6817
- if (this.isSubAgent) {
6818
- this.failSubAgent("max_iterations");
6819
- } else {
6820
- this.appendDriverNotice(
6821
- "I've reached my limit for this response. You can ask me to continue and I'll pick up where I left off."
6822
- );
6823
- }
6824
- failureReason = "max-iterations";
6842
+ return this.endTurnWithFailure(TURN_FAILURE_EXITS.maxIterations);
6825
6843
  }
6826
- return this.turnDone(failureReason);
6844
+ return this.turnDone();
6827
6845
  }
6828
6846
  /**
6829
6847
  * Append prose the DRIVER wrote rather than the model — a failure notice, a limit explanation,