@genesislcap/ai-assistant 15.20.2 → 15.21.1

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.
Files changed (29) hide show
  1. package/dist/ai-assistant.api.json +135 -1
  2. package/dist/ai-assistant.d.ts +152 -0
  3. package/dist/chat-driver.cjs +281 -81
  4. package/dist/chat-driver.cjs.map +2 -2
  5. package/dist/chat-driver.mjs +277 -81
  6. package/dist/chat-driver.mjs.map +2 -2
  7. package/dist/custom-elements.json +175 -8
  8. package/dist/dts/components/chat-driver/chat-driver.d.ts +39 -0
  9. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  10. package/dist/dts/components/orchestrating-driver/index.d.ts +2 -1
  11. package/dist/dts/components/orchestrating-driver/index.d.ts.map +1 -1
  12. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +101 -0
  13. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  14. package/dist/dts/config/fallback-agents.d.ts +7 -0
  15. package/dist/dts/config/fallback-agents.d.ts.map +1 -1
  16. package/dist/dts/state/debug-event-log.d.ts +1 -1
  17. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  18. package/dist/dts/utils/context-tokens.d.ts +25 -3
  19. package/dist/dts/utils/context-tokens.d.ts.map +1 -1
  20. package/dist/dts/utils/history-transform.d.ts +23 -3
  21. package/dist/dts/utils/history-transform.d.ts.map +1 -1
  22. package/dist/esm/components/chat-driver/chat-driver.js +72 -56
  23. package/dist/esm/components/orchestrating-driver/index.js +1 -1
  24. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +266 -36
  25. package/dist/esm/config/fallback-agents.js +12 -1
  26. package/dist/esm/state/debug-event-log.js +7 -2
  27. package/dist/esm/utils/context-tokens.js +27 -3
  28. package/dist/esm/utils/history-transform.js +54 -5
  29. package/package.json +16 -16
@@ -27,10 +27,12 @@ __export(chat_driver_node_exports, {
27
27
  AnthropicTransport: () => AnthropicTransport,
28
28
  ChatDriver: () => ChatDriver,
29
29
  DEFAULT_PROVIDER_REFUSED_MESSAGE: () => DEFAULT_PROVIDER_REFUSED_MESSAGE,
30
+ EMPTY_RESPONSE_NUDGE: () => EMPTY_RESPONSE_NUDGE,
30
31
  GEMINI_CACHED_INPUT_MULTIPLIER: () => GEMINI_CACHED_INPUT_MULTIPLIER,
31
32
  GEMINI_LONG_CONTEXT_THRESHOLD: () => GEMINI_LONG_CONTEXT_THRESHOLD,
32
33
  GeminiProvider: () => GeminiProvider,
33
34
  GeminiTransport: () => GeminiTransport,
35
+ MALFORMED_CALL_NUDGE: () => MALFORMED_CALL_NUDGE,
34
36
  MutableAIProviderRegistry: () => MutableAIProviderRegistry,
35
37
  NOOP_ACTIVITY_BUS: () => NOOP_ACTIVITY_BUS,
36
38
  OrchestratingDriver: () => OrchestratingDriver,
@@ -39,6 +41,8 @@ __export(chat_driver_node_exports, {
39
41
  REQUEST_CONTINUATION_TOOL: () => REQUEST_CONTINUATION_TOOL,
40
42
  SUPPORTED_ANTHROPIC_MODEL_IDS: () => SUPPORTED_ANTHROPIC_MODEL_IDS,
41
43
  SUPPORTED_GEMINI_MODEL_IDS: () => SUPPORTED_GEMINI_MODEL_IDS,
44
+ UNROUTABLE_INSTRUCTION: () => UNROUTABLE_INSTRUCTION,
45
+ UNROUTABLE_PLACEHOLDER: () => UNROUTABLE_PLACEHOLDER,
42
46
  addUsage: () => addUsage,
43
47
  agenticActivityBus: () => agenticActivityBus,
44
48
  anthropicRatesFor: () => anthropicRatesFor,
@@ -2240,7 +2244,8 @@ ${att.content}` });
2240
2244
  return { images, texts };
2241
2245
  };
2242
2246
  for (const msg of history) {
2243
- if (msg.role === "system" || msg.role === "system-event" || msg.role === "synthetic-user" || msg.role === "compacted-summary" || msg.category === "reasoning" || msg.category === "narration" || msg.thinking)
2247
+ if (msg.role === "system" || msg.role === "system-event" || msg.role === "synthetic-user" || msg.role === "compacted-summary" || msg.category === "reasoning" || msg.category === "narration" || // Driver-authored prose no model produced — see ChatMessage.excludeFromProvider.
2248
+ msg.excludeFromProvider || msg.thinking)
2244
2249
  continue;
2245
2250
  if (msg.toolResult) {
2246
2251
  const resultImages = (_a = msg.toolResult.attachments) !== null && _a !== void 0 ? _a : [];
@@ -2503,12 +2508,16 @@ ${att.content}` });
2503
2508
  if (cost != null)
2504
2509
  base.cost = cost;
2505
2510
  base.model = servingModel;
2506
- if (response.stop_reason === "max_tokens") {
2507
- base.responseMeta = { finishReason: "max_tokens" };
2508
- }
2509
- if (response.stop_reason === "refusal") {
2510
- base.responseMeta = { finishReason: "refusal" };
2511
- }
2511
+ const responseMeta = {
2512
+ parts: {
2513
+ functionCall: blocks.filter((b) => b.type === "tool_use").length,
2514
+ thought: blocks.filter((b) => b.type === "thinking" || b.type === "redacted_thinking").length,
2515
+ text: blocks.filter((b) => b.type === "text").length
2516
+ }
2517
+ };
2518
+ if (response.stop_reason)
2519
+ responseMeta.finishReason = response.stop_reason;
2520
+ base.responseMeta = responseMeta;
2512
2521
  return base;
2513
2522
  }
2514
2523
  buildEndpoint() {
@@ -3057,7 +3066,8 @@ var GeminiTransport = class _GeminiTransport {
3057
3066
  const contents = [];
3058
3067
  const toolCallNameById = /* @__PURE__ */ new Map();
3059
3068
  for (const msg of history) {
3060
- if (msg.role === "system" || msg.role === "system-event" || msg.role === "synthetic-user" || msg.role === "compacted-summary" || msg.category === "reasoning" || msg.category === "narration" || msg.thinking)
3069
+ if (msg.role === "system" || msg.role === "system-event" || msg.role === "synthetic-user" || msg.role === "compacted-summary" || msg.category === "reasoning" || msg.category === "narration" || // Driver-authored prose no model produced — see ChatMessage.excludeFromProvider.
3070
+ msg.excludeFromProvider || msg.thinking)
3061
3071
  continue;
3062
3072
  const role = msg.role === "user" ? "user" : "model";
3063
3073
  if (msg.toolResult) {
@@ -3642,6 +3652,9 @@ var META_EVENT_IMPORTANCE = {
3642
3652
  "tool.unresolved": "normal",
3643
3653
  "subagent.started": "normal",
3644
3654
  "subagent.completed": "normal",
3655
+ // One per routing decision, with what the classifier answered and what the call cost. Same
3656
+ // level as the handoff it produces: you read them together to follow how a turn was routed.
3657
+ "routing.decided": "normal",
3645
3658
  "agent.handoff": "normal",
3646
3659
  "agent.pinned": "normal",
3647
3660
  "agent.unpinned": "normal",
@@ -3803,6 +3816,7 @@ var COMPACTION_SUMMARY_TOKEN_ESTIMATE = 1200;
3803
3816
  function reachesProvider(m) {
3804
3817
  if (m.role === "system-event" || m.role === "synthetic-user") return false;
3805
3818
  if (m.category === "reasoning" || m.category === "narration") return false;
3819
+ if (m.excludeFromProvider) return false;
3806
3820
  return !m.thinking;
3807
3821
  }
3808
3822
  function imageCount(m) {
@@ -4132,10 +4146,27 @@ function maskToolPayload(msg) {
4132
4146
  }
4133
4147
  return msg;
4134
4148
  }
4149
+ function foreignToolCallText(msg) {
4150
+ const names = (msg.toolCalls ?? []).map((tc) => tc.name).join(", ");
4151
+ const trace = names ? `[Earlier tool call: ${names}]` : "";
4152
+ return msg.content && trace ? `${msg.content}
4153
+ ${trace}` : msg.content || trace;
4154
+ }
4135
4155
  function transformHistoryForAgent(history, agentName) {
4156
+ const isForeign = (msg) => !!msg.agentName && msg.agentName !== agentName;
4157
+ const foreignCallIds = /* @__PURE__ */ new Set();
4158
+ for (const msg of history) {
4159
+ if (!isForeign(msg)) continue;
4160
+ for (const tc of msg.toolCalls ?? []) foreignCallIds.add(tc.id);
4161
+ if (msg.toolResult) foreignCallIds.add(msg.toolResult.toolCallId);
4162
+ }
4136
4163
  return history.map((msg) => {
4137
- if (!msg.agentName || msg.agentName === agentName) return msg;
4138
- return maskToolPayload(msg);
4164
+ const pairIsForeign = (msg.toolCalls ?? []).some((tc) => foreignCallIds.has(tc.id)) || (msg.toolResult ? foreignCallIds.has(msg.toolResult.toolCallId) : false);
4165
+ if (!pairIsForeign) return msg;
4166
+ if (msg.toolResult) {
4167
+ return { ...maskToolPayload(msg), excludeFromProvider: true };
4168
+ }
4169
+ return { ...msg, content: foreignToolCallText(msg), toolCalls: void 0 };
4139
4170
  });
4140
4171
  }
4141
4172
  function applyHistoryCap(history, cap) {
@@ -4339,6 +4370,8 @@ var DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
4339
4370
  var MAX_STALE_TOOL_CALLS = DEFAULT_MAX_UNKNOWN_TOOL_CALLS * 2;
4340
4371
  var MAX_MALFORMED_RETRIES = 5;
4341
4372
  var MAX_EMPTY_RESPONSE_RETRIES = 3;
4373
+ var MALFORMED_CALL_NUDGE = "IMPORTANT: Use only the structured function-call API to invoke tools. Do not write Python code or use Python-style syntax to call tools.";
4374
+ var EMPTY_RESPONSE_NUDGE = "IMPORTANT: Your previous attempt returned nothing at all, which is never a valid response. Answer the user's last message now, in prose \u2014 even if all you can honestly say is that you cannot help with it and why. If a tool is the right way to answer, call it. Do not return an empty response.";
4342
4375
  var MAX_SETUP_TRANSPORT_RETRIES = 3;
4343
4376
  var SUGGESTIONS_HISTORY_WINDOW = 8;
4344
4377
  var HARVESTED_SUBAGENT_EVENTS = /* @__PURE__ */ new Set([
@@ -4725,7 +4758,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4725
4758
  spentUsd: e.spentUsd,
4726
4759
  isSubAgent: this.isSubAgent
4727
4760
  });
4728
- this.appendToHistory({ role: "assistant", content: this.budgetExhaustedBubble(e) });
4761
+ this.appendDriverNotice(this.budgetExhaustedBubble(e));
4729
4762
  return this.turnDone("budget-exhausted", budgetDetailOf(e));
4730
4763
  }
4731
4764
  /**
@@ -4761,7 +4794,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4761
4794
  upstreamType: e.upstreamType,
4762
4795
  isSubAgent: this.isSubAgent
4763
4796
  });
4764
- this.appendToHistory({ role: "assistant", content: this.providerRefusedBubble() });
4797
+ this.appendDriverNotice(this.providerRefusedBubble());
4765
4798
  return this.turnDone("provider-refused", void 0, detail);
4766
4799
  }
4767
4800
  /** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
@@ -5285,12 +5318,12 @@ Output format (strict):
5285
5318
  summaryText = (await defaultProvider.prompt(userMessage, { systemPrompt }))?.trim() ?? "";
5286
5319
  } catch (e) {
5287
5320
  logger2.warn(`ChatDriver: compaction summary failed: ${e instanceof Error ? e.message : e}`);
5288
- this.appendToHistory({ role: "assistant", content: failMessage });
5321
+ this.appendDriverNotice(failMessage);
5289
5322
  return null;
5290
5323
  }
5291
5324
  if (!summaryText) {
5292
5325
  logger2.warn("ChatDriver: compaction summary was empty; leaving history untouched");
5293
- this.appendToHistory({ role: "assistant", content: failMessage });
5326
+ this.appendDriverNotice(failMessage);
5294
5327
  return null;
5295
5328
  }
5296
5329
  let tokensBefore;
@@ -5607,10 +5640,9 @@ Output format (strict):
5607
5640
  name: e instanceof Error ? e.name : void 0,
5608
5641
  message: e instanceof Error ? e.message : String(e)
5609
5642
  });
5610
- this.appendToHistory({
5611
- role: "assistant",
5612
- content: "Sorry, something went wrong on my end. Please try again in a moment."
5613
- });
5643
+ this.appendDriverNotice(
5644
+ "Sorry, something went wrong on my end. Please try again in a moment."
5645
+ );
5614
5646
  result = this.turnDone("exception");
5615
5647
  return result;
5616
5648
  } finally {
@@ -5872,10 +5904,9 @@ Output format (strict):
5872
5904
  name: e instanceof Error ? e.name : void 0,
5873
5905
  message: e instanceof Error ? e.message : String(e)
5874
5906
  });
5875
- this.appendToHistory({
5876
- role: "assistant",
5877
- content: "Sorry, something went wrong on my end. Please try again in a moment."
5878
- });
5907
+ this.appendDriverNotice(
5908
+ "Sorry, something went wrong on my end. Please try again in a moment."
5909
+ );
5879
5910
  result = this.turnDone("exception");
5880
5911
  return result;
5881
5912
  } finally {
@@ -6054,7 +6085,7 @@ Output format (strict):
6054
6085
  if (this.isSubAgent) {
6055
6086
  this.failSubAgent("provider_refused", void 0, this.providerRefusedDetail);
6056
6087
  } else {
6057
- this.appendToHistory({ role: "assistant", content: this.providerRefusedBubble() });
6088
+ this.appendDriverNotice(this.providerRefusedBubble());
6058
6089
  }
6059
6090
  return this.turnDone("provider-refused", void 0, this.providerRefusedDetail);
6060
6091
  }
@@ -6079,10 +6110,7 @@ Output format (strict):
6079
6110
  if (this.isSubAgent) {
6080
6111
  this.failSubAgent("budget_exhausted", this.budgetWallDetail);
6081
6112
  } else {
6082
- this.appendToHistory({
6083
- role: "assistant",
6084
- content: this.budgetExhaustedBubble(this.budgetWallDetail)
6085
- });
6113
+ this.appendDriverNotice(this.budgetExhaustedBubble(this.budgetWallDetail));
6086
6114
  }
6087
6115
  return this.turnDone("budget-exhausted", this.budgetWallDetail);
6088
6116
  }
@@ -6119,7 +6147,11 @@ Output format (strict):
6119
6147
  this.activeAgentLabel = await this.displayName(promptCtx);
6120
6148
  }
6121
6149
  const foldSuffix = this.buildFoldSystemPromptSuffix();
6122
- const retryNudge = malformedAttempts > 0 ? "\n\nIMPORTANT: Use only the structured function-call API to invoke tools. Do not write Python code or use Python-style syntax to call tools." : emptyResponseAttempts > 0 ? "\n\nIMPORTANT: You must respond to the user's message. Call the appropriate tool or provide a text response \u2014 do not return an empty response." : "";
6150
+ const retryNudge = malformedAttempts > 0 ? `
6151
+
6152
+ ${MALFORMED_CALL_NUDGE}` : emptyResponseAttempts > 0 ? `
6153
+
6154
+ ${EMPTY_RESPONSE_NUDGE}` : "";
6123
6155
  const frameworkSystemSuffix = `${foldSuffix}${retryNudge}`;
6124
6156
  const primer = normalizeForProvider([
6125
6157
  ...this.primerHistory ?? [],
@@ -6243,10 +6275,9 @@ ${tailBody}
6243
6275
  if (this.isSubAgent) {
6244
6276
  this.failSubAgent("context_exhausted");
6245
6277
  } else {
6246
- this.appendToHistory({
6247
- role: "assistant",
6248
- content: "I had to stop here \u2014 this conversation has filled the available context. Compact it to free up room, then ask me to continue."
6249
- });
6278
+ this.appendDriverNotice(
6279
+ "I had to stop here \u2014 this conversation has filled the available context. Compact it to free up room, then ask me to continue."
6280
+ );
6250
6281
  }
6251
6282
  return this.turnDone("context-exhausted");
6252
6283
  }
@@ -6281,10 +6312,9 @@ ${tailBody}
6281
6312
  if (this.isSubAgent) {
6282
6313
  this.failSubAgent("malformed_tool_call");
6283
6314
  } else {
6284
- this.appendToHistory({
6285
- role: "assistant",
6286
- content: "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."
6287
- });
6315
+ this.appendDriverNotice(
6316
+ "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."
6317
+ );
6288
6318
  }
6289
6319
  return this.turnDone("malformed-function-call");
6290
6320
  }
@@ -6302,10 +6332,9 @@ ${tailBody}
6302
6332
  if (this.isSubAgent) {
6303
6333
  this.failSubAgent("response_truncated");
6304
6334
  } else {
6305
- this.appendToHistory({
6306
- role: "assistant",
6307
- content: "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."
6308
- });
6335
+ this.appendDriverNotice(
6336
+ "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."
6337
+ );
6309
6338
  }
6310
6339
  return this.turnDone("response-truncated");
6311
6340
  }
@@ -6323,10 +6352,7 @@ ${tailBody}
6323
6352
  if (this.isSubAgent) {
6324
6353
  this.failSubAgent("context_exhausted");
6325
6354
  } else {
6326
- this.appendToHistory({
6327
- role: "assistant",
6328
- content: DEFAULT_CONTEXT_OVERFLOW_MESSAGE
6329
- });
6355
+ this.appendDriverNotice(DEFAULT_CONTEXT_OVERFLOW_MESSAGE);
6330
6356
  }
6331
6357
  return this.turnDone("context-exhausted");
6332
6358
  }
@@ -6379,10 +6405,9 @@ ${tailBody}
6379
6405
  if (this.isSubAgent) {
6380
6406
  this.failSubAgent("timeout");
6381
6407
  } else {
6382
- this.appendToHistory({
6383
- role: "assistant",
6384
- content: "The request timed out. You can ask me to try again, or break this into a smaller step."
6385
- });
6408
+ this.appendDriverNotice(
6409
+ "The request timed out. You can ask me to try again, or break this into a smaller step."
6410
+ );
6386
6411
  }
6387
6412
  return this.turnDone("exception");
6388
6413
  }
@@ -6422,6 +6447,11 @@ ${tailBody}
6422
6447
  provider: this.lastResolvedProviderName,
6423
6448
  attempt: emptyResponseAttempts,
6424
6449
  maxAttempts: MAX_EMPTY_RESPONSE_RETRIES,
6450
+ // How many tools the agent was holding. Every empty response in the session that
6451
+ // motivated this (GENC-1573) came from an agent with ZERO — a tool-less agent asked
6452
+ // something its prompt gives it no instruction for has nothing it is allowed to do,
6453
+ // and reads as a model fault unless this number is on the record.
6454
+ toolCount: this.toolDefinitions.length,
6425
6455
  // Provider diagnostic (Gemini: finishReason, thoughtsTokens, parts,
6426
6456
  // blockReason) so the timeline shows *why* the turn came back blank —
6427
6457
  // e.g. a high thoughtsTokens with a 'STOP' finish is "thought, then
@@ -6439,16 +6469,18 @@ ${tailBody}
6439
6469
  agent: this.activeAgentName,
6440
6470
  provider: this.lastResolvedProviderName,
6441
6471
  attempts: emptyResponseAttempts,
6472
+ // See the retry above: a zero here says the agent had no tools, which is most of the
6473
+ // explanation for a blank turn and is otherwise invisible.
6474
+ toolCount: this.toolDefinitions.length,
6442
6475
  isSubAgent: this.isSubAgent,
6443
6476
  ...response.responseMeta
6444
6477
  });
6445
6478
  if (this.isSubAgent) {
6446
6479
  this.failSubAgent(isRefusal ? "refusal" : "empty_response");
6447
6480
  } else {
6448
- this.appendToHistory({
6449
- role: "assistant",
6450
- content: 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."
6451
- });
6481
+ this.appendDriverNotice(
6482
+ 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."
6483
+ );
6452
6484
  }
6453
6485
  return this.turnDone(failureReason2);
6454
6486
  } else {
@@ -6753,10 +6785,9 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6753
6785
  if (this.isSubAgent) {
6754
6786
  this.failSubAgent("unknown_tool_limit");
6755
6787
  } else {
6756
- this.appendToHistory({
6757
- role: "assistant",
6758
- content: "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."
6759
- });
6788
+ this.appendDriverNotice(
6789
+ "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."
6790
+ );
6760
6791
  }
6761
6792
  return this.turnDone("unknown-tool-limit");
6762
6793
  }
@@ -6782,15 +6813,33 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6782
6813
  if (this.isSubAgent) {
6783
6814
  this.failSubAgent("max_iterations");
6784
6815
  } else {
6785
- this.appendToHistory({
6786
- role: "assistant",
6787
- content: "I've reached my limit for this response. You can ask me to continue and I'll pick up where I left off."
6788
- });
6816
+ this.appendDriverNotice(
6817
+ "I've reached my limit for this response. You can ask me to continue and I'll pick up where I left off."
6818
+ );
6789
6819
  }
6790
6820
  failureReason = "max-iterations";
6791
6821
  }
6792
6822
  return this.turnDone(failureReason);
6793
6823
  }
6824
+ /**
6825
+ * Append prose the DRIVER wrote rather than the model — a failure notice, a limit explanation,
6826
+ * the budget wall. Renders as an ordinary assistant bubble and is flagged
6827
+ * `excludeFromProvider`, so it never re-enters the next request.
6828
+ *
6829
+ * Every such notice must go through here. Written as one funnel rather than a flag repeated at
6830
+ * eleven call sites because the failure mode is silent: a notice that keeps the flag reads
6831
+ * identically in the transcript to one that loses it, and the damage only shows up a turn later
6832
+ * as the model echoing its own error message. Measured (GENC-1573): with the empty-response
6833
+ * notice in history, the next turn reproduced it verbatim in 45 output tokens — succeeding,
6834
+ * with no retry and no `turn.error`, while still showing the user a failure. From there the
6835
+ * loop sustains itself whether or not the model answers, which is why "try again" never worked.
6836
+ *
6837
+ * Not for anything the model produced, and not for a widget/interaction message: those are real
6838
+ * turn content and the model must keep seeing them.
6839
+ */
6840
+ appendDriverNotice(content) {
6841
+ this.appendToHistory({ role: "assistant", content, excludeFromProvider: true });
6842
+ }
6794
6843
  appendToHistory(message) {
6795
6844
  const tagged = {
6796
6845
  ...message,
@@ -6815,6 +6864,8 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6815
6864
  var DEFAULT_MAX_HANDOFFS = 3;
6816
6865
  var DEFAULT_CLASSIFIER_HISTORY_LENGTH = 4;
6817
6866
  var DEFAULT_CLASSIFIER_RETRIES = 2;
6867
+ var CLASSIFIER_MESSAGE_CHARS = 600;
6868
+ var CLASSIFIER_TRANSCRIPT_CHARS = 3e3;
6818
6869
  var REQUEST_CONTINUATION_DEFINITION = {
6819
6870
  name: REQUEST_CONTINUATION_TOOL,
6820
6871
  description: "Call this when you have completed your part of the task but fulfilling the user's full request requires capabilities outside your domain. Do not call this if you can complete the request yourself. Pass a plain-language description of what still needs to be done \u2014 another specialist will be selected automatically.",
@@ -6839,10 +6890,19 @@ function isSpecialist(agent) {
6839
6890
  function isFallback(agent) {
6840
6891
  return agent.fallback === true;
6841
6892
  }
6893
+ var UNROUTABLE_PLACEHOLDER = "{{unroutable}}";
6894
+ var UNROUTABLE_INSTRUCTION = `When the request is substantive but nothing above covers it, say so directly. Do not offer to do it yourself, and do not imply a specialist will pick it up.
6895
+
6896
+ If the user's message is a short follow-up to something already discussed \u2014 "fix it", "try again", "why?" \u2014 read it against the conversation above and answer that, rather than treating it as a new request with no context.
6897
+
6898
+ Never end your turn without saying something. An empty reply is never a valid response: if you genuinely cannot help, say briefly that you cannot, and why.`;
6842
6899
  function buildFallbackSystemPrompt(fallback, specialists) {
6843
6900
  const agentList = specialists.map((s) => `- ${s.name}: ${s.description}`).join("\n");
6844
6901
  if (typeof fallback.systemPrompt === "string") {
6845
- return fallback.systemPrompt.replace("{{agents}}", agentList);
6902
+ const withAgents = fallback.systemPrompt.replace("{{agents}}", agentList);
6903
+ return withAgents.includes(UNROUTABLE_PLACEHOLDER) ? withAgents.replace(UNROUTABLE_PLACEHOLDER, UNROUTABLE_INSTRUCTION) : `${withAgents}
6904
+
6905
+ ${UNROUTABLE_INSTRUCTION}`;
6846
6906
  }
6847
6907
  if (typeof fallback.systemPrompt === "function") {
6848
6908
  return fallback.systemPrompt;
@@ -6851,7 +6911,9 @@ function buildFallbackSystemPrompt(fallback, specialists) {
6851
6911
 
6852
6912
  ${agentList}
6853
6913
 
6854
- Politely let the user know what you can help with and invite them to rephrase their request.`;
6914
+ Politely let the user know what you can help with and invite them to rephrase their request.
6915
+
6916
+ ${UNROUTABLE_INSTRUCTION}`;
6855
6917
  }
6856
6918
  var OrchestratingDriver = class extends EventTarget {
6857
6919
  constructor(providerRegistry, agents, options = {}) {
@@ -7082,11 +7144,19 @@ var OrchestratingDriver = class extends EventTarget {
7082
7144
  );
7083
7145
  const pinned = this.resolvePinnedAgent();
7084
7146
  if (!pinned) this.dispatchEvent(new CustomEvent("orchestrating-start"));
7085
- let currentAgent = pinned ?? await this.classify(input, history, this.activeAgent);
7147
+ let routed;
7148
+ if (pinned) {
7149
+ this.recordRouting("pinned", pinned, { durationMs: 0 });
7150
+ routed = { agent: pinned, decision: "pinned" };
7151
+ } else {
7152
+ routed = await this.classify(input, history, this.activeAgent);
7153
+ }
7154
+ let currentAgent = routed.agent;
7086
7155
  let isHandoff = false;
7087
7156
  let handoffs = 0;
7088
7157
  let handoffSummary = "";
7089
7158
  let remainingTask = "";
7159
+ const attempted = [];
7090
7160
  let lastResult;
7091
7161
  while (true) {
7092
7162
  if (this.cancelled) break;
@@ -7094,6 +7164,14 @@ var OrchestratingDriver = class extends EventTarget {
7094
7164
  let result;
7095
7165
  if (isHandoff) {
7096
7166
  const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
7167
+ if (isFallback(currentAgent)) {
7168
+ const work = remainingTask?.trim() || handoffSummary?.trim();
7169
+ contextPrimer.push({
7170
+ role: "user",
7171
+ content: (work ? `[Routing note]: no specialist is available for the remaining work: ${work}
7172
+ ` : "[Routing note]: no specialist is available for the work that remains.\n") + "Tell the user plainly what was established, that this part falls outside what any specialist here can do, and what they could do next. Do not claim you will do it yourself, and do not return an empty response."
7173
+ });
7174
+ }
7097
7175
  result = await this.chatDriver.continueFromHistory(contextPrimer);
7098
7176
  } else {
7099
7177
  this.userMessageAppended = true;
@@ -7117,9 +7195,13 @@ var OrchestratingDriver = class extends EventTarget {
7117
7195
  handoffSummary = result.summary;
7118
7196
  remainingTask = result.remainingTask;
7119
7197
  isHandoff = true;
7198
+ if (isSpecialist(currentAgent) && !attempted.includes(currentAgent.name)) {
7199
+ attempted.push(currentAgent.name);
7200
+ }
7120
7201
  const updatedHistory = this.chatDriver.getHistory();
7121
7202
  this.dispatchEvent(new CustomEvent("orchestrating-start"));
7122
- currentAgent = await this.classify(remainingTask, updatedHistory);
7203
+ routed = await this.classify(remainingTask, updatedHistory, void 0, attempted);
7204
+ currentAgent = routed.agent;
7123
7205
  }
7124
7206
  return lastResult?.reason === "done" ? lastResult : { reason: "done" };
7125
7207
  }
@@ -7265,28 +7347,99 @@ var OrchestratingDriver = class extends EventTarget {
7265
7347
  this.chatDriver.dispose();
7266
7348
  this.activeAgent = void 0;
7267
7349
  }
7268
- async classify(input, history, contextAgent) {
7350
+ /**
7351
+ * Record one routing decision on the debug timeline — what was chosen, why, what it cost, and
7352
+ * (on a failure) what went wrong.
7353
+ *
7354
+ * This is the event whose absence made GENC-1573 undiagnosable. `classify` reaches the fallback
7355
+ * by four different routes and every one of them only warned to the console; because it runs
7356
+ * outside any ChatDriver turn it produces no `kind:'turn'` entry either, so both the decision
7357
+ * and its spend were invisible in an exported log.
7358
+ *
7359
+ * `usage` is why the spend line exists at all. A classifier call is a real billed request that
7360
+ * never enters the transcript, and `sessionUsage` is derived from the transcript — so classifier
7361
+ * spend is absent from the session total by construction. Recording it per decision keeps it
7362
+ * auditable and tells a reconciler exactly where the gap comes from: sum these to close it.
7363
+ */
7364
+ recordRouting(decision, agent, detail = {}) {
7365
+ const { response, ballot, ...rest } = detail;
7366
+ const usage = response ? messageUsage(response) : void 0;
7367
+ recordMetaEvent(this.sessionKey, "routing.decided", {
7368
+ decision,
7369
+ to: agent.name,
7370
+ fallback: isFallback(agent),
7371
+ // The BALLOT, not `this.specialists`. `index` is a position in the ballot (see its
7372
+ // construction in `classify`), so publishing the unfiltered specialist list beside it made
7373
+ // `candidates[index]` resolve to the wrong agent whenever `excluded` was non-empty — and the
7374
+ // README sentence invited exactly that join. `this.specialists` stays recoverable as
7375
+ // `candidates ∪ excluded`. Absent where nothing was put to a vote, which is more truthful
7376
+ // than a full list. Reviewer catch on #2496.
7377
+ ...ballot ? { candidates: ballot } : {},
7378
+ ...rest,
7379
+ ...usage ? { usage } : {},
7380
+ ...response?.model ? { model: response.model } : {}
7381
+ });
7382
+ }
7383
+ /**
7384
+ * The recent-conversation excerpt for the classifier prompt, bounded in size as well as in
7385
+ * message count — see the `CLASSIFIER_MESSAGE_CHARS` docblock for why.
7386
+ */
7387
+ classifierTranscript(history) {
7388
+ const lines = history.filter((m) => (m.role === "user" || m.role === "assistant") && reachesProvider(m)).filter((m) => !!m.content?.trim()).slice(-this.classifierHistoryLength).map((m) => {
7389
+ const speaker = m.role === "user" ? "User" : "Assistant";
7390
+ const content = m.content ?? "";
7391
+ const body = content.length > CLASSIFIER_MESSAGE_CHARS ? `${content.slice(0, CLASSIFIER_MESSAGE_CHARS)}\u2026` : content;
7392
+ return `${speaker}: ${body}`;
7393
+ });
7394
+ while (lines.length > 1 && lines.join("\n").length > CLASSIFIER_TRANSCRIPT_CHARS) {
7395
+ lines.shift();
7396
+ }
7397
+ return lines.join("\n");
7398
+ }
7399
+ async classify(input, history, contextAgent, excludeAgents = []) {
7400
+ const classifyStartedAt = Date.now();
7269
7401
  if (this.specialists.length === 1 && !this.fallback) {
7270
- return this.specialists[0];
7402
+ const only = this.specialists[0];
7403
+ this.recordRouting("single-candidate", only, {
7404
+ durationMs: Date.now() - classifyStartedAt
7405
+ });
7406
+ return { agent: only, decision: "single-candidate" };
7271
7407
  }
7272
7408
  if (this.specialists.length === 0) {
7273
7409
  if (!this.fallback) {
7274
7410
  const routable = this.agents.filter((a) => !isFallback(a));
7275
7411
  if (routable.length === 1) {
7276
- return routable[0];
7412
+ this.recordRouting("no-specialists", routable[0], {
7413
+ durationMs: Date.now() - classifyStartedAt
7414
+ });
7415
+ return { agent: routable[0], decision: "no-specialists" };
7277
7416
  }
7278
7417
  }
7279
- return this.fallback ?? { name: "Assistant", fallback: true };
7418
+ const leaf = this.fallback ?? { name: "Assistant", fallback: true };
7419
+ this.recordRouting("no-specialists", leaf, { durationMs: Date.now() - classifyStartedAt });
7420
+ return { agent: leaf, decision: "no-specialists" };
7280
7421
  }
7281
- const agentList = this.specialists.map((a, i) => `${i}: ${a.name} \u2014 ${a.description}`).join("\n");
7282
- const recentMessages = history.filter((m) => m.role === "user" || m.role === "assistant").slice(-this.classifierHistoryLength).map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
7283
- const continuationHint = contextAgent && isSpecialist(contextAgent) ? `
7422
+ const ballot = excludeAgents.length ? this.specialists.filter((a) => !excludeAgents.includes(a.name)) : this.specialists;
7423
+ if (ballot.length === 0) {
7424
+ const leaf = this.fallback ?? this.specialists[0];
7425
+ this.recordRouting("ballot-exhausted", leaf, {
7426
+ excluded: excludeAgents,
7427
+ durationMs: Date.now() - classifyStartedAt
7428
+ });
7429
+ return { agent: leaf, decision: "ballot-exhausted" };
7430
+ }
7431
+ const agentList = ballot.map((a, i) => `${i}: ${a.name} \u2014 ${a.description}`).join("\n");
7432
+ const recentMessages = this.classifierTranscript(history);
7433
+ const continuationHint = contextAgent && isSpecialist(contextAgent) && !excludeAgents.includes(contextAgent.name) ? `
7284
7434
 
7285
7435
  The previous turn was handled by "${contextAgent.name}". If the new message is a refinement, correction, or continuation of that work, prefer "${contextAgent.name}" \u2014 unless the topic has clearly shifted to another specialist's domain.` : "";
7436
+ const attemptedHint = excludeAgents.length ? `
7437
+
7438
+ Already attempted this turn and handed the work on, so not listed above: ${excludeAgents.join(", ")}. Do not expect another agent to repeat their part.` : "";
7286
7439
  const classifierPrompt = `You are a routing classifier. Call select_agent with the index of the best matching agent for the user message. Use -1 only if the request is entirely unrelated to every agent listed \u2014 prefer the closest match when in doubt. If the request spans multiple agents, pick the one that should act first; I will re-classify and route again once that agent is done.
7287
7440
 
7288
7441
  Agents:
7289
- ${agentList}${continuationHint}${recentMessages ? `
7442
+ ${agentList}${continuationHint}${attemptedHint}${recentMessages ? `
7290
7443
 
7291
7444
  Recent conversation:
7292
7445
  ${recentMessages}` : ""}`;
@@ -7304,8 +7457,12 @@ ${recentMessages}` : ""}`;
7304
7457
  }
7305
7458
  }
7306
7459
  };
7307
- let routedNoMatch = false;
7460
+ let decision = "classifier-failed";
7461
+ let lastError;
7462
+ let attempts = 0;
7463
+ let lastResponse;
7308
7464
  for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
7465
+ attempts = attempt + 1;
7309
7466
  try {
7310
7467
  const options = {
7311
7468
  systemPrompt: classifierPrompt,
@@ -7316,32 +7473,56 @@ ${recentMessages}` : ""}`;
7316
7473
  logger2.warn(
7317
7474
  "OrchestratingDriver: default AI provider does not implement chat() \u2014 cannot classify."
7318
7475
  );
7476
+ decision = "provider-unavailable";
7319
7477
  break;
7320
7478
  }
7321
7479
  const response = await classifierProvider.chat([], input, options);
7480
+ lastResponse = response;
7322
7481
  const tc = response.toolCalls?.[0];
7323
7482
  const index = tc?.name === "select_agent" ? tc.args.agent_index : -1;
7324
- if (index >= 0 && index < this.specialists.length) {
7325
- return this.specialists[index];
7483
+ if (index >= 0 && index < ballot.length) {
7484
+ const chosen = ballot[index];
7485
+ this.recordRouting("classified", chosen, {
7486
+ index,
7487
+ attempts,
7488
+ durationMs: Date.now() - classifyStartedAt,
7489
+ response,
7490
+ ballot: ballot.map((a) => a.name),
7491
+ ...excludeAgents.length ? { excluded: excludeAgents } : {}
7492
+ });
7493
+ return { agent: chosen, decision: "classified" };
7326
7494
  }
7327
- routedNoMatch = true;
7495
+ decision = "no-match";
7328
7496
  break;
7329
7497
  } catch (e) {
7330
7498
  if (e instanceof BudgetExhaustedError) throw e;
7499
+ lastError = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
7331
7500
  logger2.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
7332
7501
  if (attempt === this.classifierRetries) {
7333
7502
  logger2.error("OrchestratingDriver: classifier failed after all retries, using fallback");
7334
7503
  }
7335
7504
  }
7336
7505
  }
7337
- if (this.fallback) return this.fallback;
7338
- if (routedNoMatch) {
7506
+ const routingDetail = {
7507
+ attempts,
7508
+ durationMs: Date.now() - classifyStartedAt,
7509
+ ballot: ballot.map((a) => a.name),
7510
+ ...lastError ? { error: lastError } : {},
7511
+ ...lastResponse ? { response: lastResponse } : {},
7512
+ ...excludeAgents.length ? { excluded: excludeAgents } : {}
7513
+ };
7514
+ if (this.fallback) {
7515
+ this.recordRouting(decision, this.fallback, routingDetail);
7516
+ return { agent: this.fallback, decision };
7517
+ }
7518
+ if (decision === "no-match") {
7339
7519
  const specialistNames = this.specialists.map((s) => s.name).join(", ");
7340
7520
  this.appendInlineMessage(
7341
7521
  `I'm not sure how to help with that. I can assist with: ${specialistNames}.`
7342
7522
  );
7343
7523
  }
7344
- return this.specialists[0];
7524
+ this.recordRouting(decision, this.specialists[0], routingDetail);
7525
+ return { agent: this.specialists[0], decision };
7345
7526
  }
7346
7527
  /**
7347
7528
  * Returns the agent that the current turn is locked to, if any. Flow-owner
@@ -7363,11 +7544,22 @@ ${recentMessages}` : ""}`;
7363
7544
  }
7364
7545
  return match;
7365
7546
  }
7547
+ /**
7548
+ * Append prose the ORCHESTRATOR wrote — the handoff cap, the no-specialist apology. Flagged
7549
+ * `excludeFromProvider` for the same reason the driver's own notices are: it is not something a
7550
+ * model said, and feeding it back invites the model to echo it. See
7551
+ * `ChatMessage.excludeFromProvider`.
7552
+ */
7366
7553
  appendInlineMessage(content) {
7367
7554
  const history = this.chatDriver.getHistory();
7368
7555
  this.chatDriver.loadHistory([
7369
7556
  ...history,
7370
- { role: "assistant", content, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
7557
+ {
7558
+ role: "assistant",
7559
+ content,
7560
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7561
+ excludeFromProvider: true
7562
+ }
7371
7563
  ]);
7372
7564
  this.dispatchEvent(
7373
7565
  new CustomEvent("history-updated", { detail: this.chatDriver.getHistory() })
@@ -7583,7 +7775,11 @@ Politely let the user know what you can help with and invite them to rephrase th
7583
7775
  var strictFallbackAgent = {
7584
7776
  name: "Assistant",
7585
7777
  fallback: true,
7586
- systemPrompt: `You are a concise assistant. The user's request is outside your scope. Respond with a single short sentence stating what you can assist with. Do not elaborate. The available specialists are: {{agents}}.`
7778
+ systemPrompt: `You are a concise assistant. The user's request is outside your scope. The available specialists are: {{agents}}.
7779
+
7780
+ {{unroutable}}
7781
+
7782
+ Answer in a single short sentence. Do not elaborate.`
7587
7783
  };
7588
7784
 
7589
7785
  // src/utils/usage-rows.ts
@@ -7749,10 +7945,12 @@ function withFreshMetaSnapshot(stored, fresh) {
7749
7945
  AnthropicTransport,
7750
7946
  ChatDriver,
7751
7947
  DEFAULT_PROVIDER_REFUSED_MESSAGE,
7948
+ EMPTY_RESPONSE_NUDGE,
7752
7949
  GEMINI_CACHED_INPUT_MULTIPLIER,
7753
7950
  GEMINI_LONG_CONTEXT_THRESHOLD,
7754
7951
  GeminiProvider,
7755
7952
  GeminiTransport,
7953
+ MALFORMED_CALL_NUDGE,
7756
7954
  MutableAIProviderRegistry,
7757
7955
  NOOP_ACTIVITY_BUS,
7758
7956
  OrchestratingDriver,
@@ -7761,6 +7959,8 @@ function withFreshMetaSnapshot(stored, fresh) {
7761
7959
  REQUEST_CONTINUATION_TOOL,
7762
7960
  SUPPORTED_ANTHROPIC_MODEL_IDS,
7763
7961
  SUPPORTED_GEMINI_MODEL_IDS,
7962
+ UNROUTABLE_INSTRUCTION,
7963
+ UNROUTABLE_PLACEHOLDER,
7764
7964
  addUsage,
7765
7965
  agenticActivityBus,
7766
7966
  anthropicRatesFor,