@genesislcap/ai-assistant 15.21.0 → 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.
- package/dist/ai-assistant.api.json +135 -1
- package/dist/ai-assistant.d.ts +152 -0
- package/dist/chat-driver.cjs +281 -81
- package/dist/chat-driver.cjs.map +2 -2
- package/dist/chat-driver.mjs +277 -81
- package/dist/chat-driver.mjs.map +2 -2
- package/dist/custom-elements.json +175 -8
- package/dist/dts/components/chat-driver/chat-driver.d.ts +39 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/index.d.ts +2 -1
- package/dist/dts/components/orchestrating-driver/index.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +101 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/fallback-agents.d.ts +7 -0
- package/dist/dts/config/fallback-agents.d.ts.map +1 -1
- package/dist/dts/state/debug-event-log.d.ts +1 -1
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/dts/utils/context-tokens.d.ts +25 -3
- package/dist/dts/utils/context-tokens.d.ts.map +1 -1
- package/dist/dts/utils/history-transform.d.ts +23 -3
- package/dist/dts/utils/history-transform.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +72 -56
- package/dist/esm/components/orchestrating-driver/index.js +1 -1
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +266 -36
- package/dist/esm/config/fallback-agents.js +12 -1
- package/dist/esm/state/debug-event-log.js +7 -2
- package/dist/esm/utils/context-tokens.js +27 -3
- package/dist/esm/utils/history-transform.js +54 -5
- package/package.json +16 -16
package/dist/chat-driver.mjs
CHANGED
|
@@ -2173,7 +2173,8 @@ ${att.content}` });
|
|
|
2173
2173
|
return { images, texts };
|
|
2174
2174
|
};
|
|
2175
2175
|
for (const msg of history) {
|
|
2176
|
-
if (msg.role === "system" || msg.role === "system-event" || msg.role === "synthetic-user" || msg.role === "compacted-summary" || msg.category === "reasoning" || msg.category === "narration" ||
|
|
2176
|
+
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.
|
|
2177
|
+
msg.excludeFromProvider || msg.thinking)
|
|
2177
2178
|
continue;
|
|
2178
2179
|
if (msg.toolResult) {
|
|
2179
2180
|
const resultImages = (_a = msg.toolResult.attachments) !== null && _a !== void 0 ? _a : [];
|
|
@@ -2436,12 +2437,16 @@ ${att.content}` });
|
|
|
2436
2437
|
if (cost != null)
|
|
2437
2438
|
base.cost = cost;
|
|
2438
2439
|
base.model = servingModel;
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2440
|
+
const responseMeta = {
|
|
2441
|
+
parts: {
|
|
2442
|
+
functionCall: blocks.filter((b) => b.type === "tool_use").length,
|
|
2443
|
+
thought: blocks.filter((b) => b.type === "thinking" || b.type === "redacted_thinking").length,
|
|
2444
|
+
text: blocks.filter((b) => b.type === "text").length
|
|
2445
|
+
}
|
|
2446
|
+
};
|
|
2447
|
+
if (response.stop_reason)
|
|
2448
|
+
responseMeta.finishReason = response.stop_reason;
|
|
2449
|
+
base.responseMeta = responseMeta;
|
|
2445
2450
|
return base;
|
|
2446
2451
|
}
|
|
2447
2452
|
buildEndpoint() {
|
|
@@ -2990,7 +2995,8 @@ var GeminiTransport = class _GeminiTransport {
|
|
|
2990
2995
|
const contents = [];
|
|
2991
2996
|
const toolCallNameById = /* @__PURE__ */ new Map();
|
|
2992
2997
|
for (const msg of history) {
|
|
2993
|
-
if (msg.role === "system" || msg.role === "system-event" || msg.role === "synthetic-user" || msg.role === "compacted-summary" || msg.category === "reasoning" || msg.category === "narration" ||
|
|
2998
|
+
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.
|
|
2999
|
+
msg.excludeFromProvider || msg.thinking)
|
|
2994
3000
|
continue;
|
|
2995
3001
|
const role = msg.role === "user" ? "user" : "model";
|
|
2996
3002
|
if (msg.toolResult) {
|
|
@@ -3575,6 +3581,9 @@ var META_EVENT_IMPORTANCE = {
|
|
|
3575
3581
|
"tool.unresolved": "normal",
|
|
3576
3582
|
"subagent.started": "normal",
|
|
3577
3583
|
"subagent.completed": "normal",
|
|
3584
|
+
// One per routing decision, with what the classifier answered and what the call cost. Same
|
|
3585
|
+
// level as the handoff it produces: you read them together to follow how a turn was routed.
|
|
3586
|
+
"routing.decided": "normal",
|
|
3578
3587
|
"agent.handoff": "normal",
|
|
3579
3588
|
"agent.pinned": "normal",
|
|
3580
3589
|
"agent.unpinned": "normal",
|
|
@@ -3736,6 +3745,7 @@ var COMPACTION_SUMMARY_TOKEN_ESTIMATE = 1200;
|
|
|
3736
3745
|
function reachesProvider(m) {
|
|
3737
3746
|
if (m.role === "system-event" || m.role === "synthetic-user") return false;
|
|
3738
3747
|
if (m.category === "reasoning" || m.category === "narration") return false;
|
|
3748
|
+
if (m.excludeFromProvider) return false;
|
|
3739
3749
|
return !m.thinking;
|
|
3740
3750
|
}
|
|
3741
3751
|
function imageCount(m) {
|
|
@@ -4065,10 +4075,27 @@ function maskToolPayload(msg) {
|
|
|
4065
4075
|
}
|
|
4066
4076
|
return msg;
|
|
4067
4077
|
}
|
|
4078
|
+
function foreignToolCallText(msg) {
|
|
4079
|
+
const names = (msg.toolCalls ?? []).map((tc) => tc.name).join(", ");
|
|
4080
|
+
const trace = names ? `[Earlier tool call: ${names}]` : "";
|
|
4081
|
+
return msg.content && trace ? `${msg.content}
|
|
4082
|
+
${trace}` : msg.content || trace;
|
|
4083
|
+
}
|
|
4068
4084
|
function transformHistoryForAgent(history, agentName) {
|
|
4085
|
+
const isForeign = (msg) => !!msg.agentName && msg.agentName !== agentName;
|
|
4086
|
+
const foreignCallIds = /* @__PURE__ */ new Set();
|
|
4087
|
+
for (const msg of history) {
|
|
4088
|
+
if (!isForeign(msg)) continue;
|
|
4089
|
+
for (const tc of msg.toolCalls ?? []) foreignCallIds.add(tc.id);
|
|
4090
|
+
if (msg.toolResult) foreignCallIds.add(msg.toolResult.toolCallId);
|
|
4091
|
+
}
|
|
4069
4092
|
return history.map((msg) => {
|
|
4070
|
-
|
|
4071
|
-
return
|
|
4093
|
+
const pairIsForeign = (msg.toolCalls ?? []).some((tc) => foreignCallIds.has(tc.id)) || (msg.toolResult ? foreignCallIds.has(msg.toolResult.toolCallId) : false);
|
|
4094
|
+
if (!pairIsForeign) return msg;
|
|
4095
|
+
if (msg.toolResult) {
|
|
4096
|
+
return { ...maskToolPayload(msg), excludeFromProvider: true };
|
|
4097
|
+
}
|
|
4098
|
+
return { ...msg, content: foreignToolCallText(msg), toolCalls: void 0 };
|
|
4072
4099
|
});
|
|
4073
4100
|
}
|
|
4074
4101
|
function applyHistoryCap(history, cap) {
|
|
@@ -4272,6 +4299,8 @@ var DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
|
|
|
4272
4299
|
var MAX_STALE_TOOL_CALLS = DEFAULT_MAX_UNKNOWN_TOOL_CALLS * 2;
|
|
4273
4300
|
var MAX_MALFORMED_RETRIES = 5;
|
|
4274
4301
|
var MAX_EMPTY_RESPONSE_RETRIES = 3;
|
|
4302
|
+
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.";
|
|
4303
|
+
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.";
|
|
4275
4304
|
var MAX_SETUP_TRANSPORT_RETRIES = 3;
|
|
4276
4305
|
var SUGGESTIONS_HISTORY_WINDOW = 8;
|
|
4277
4306
|
var HARVESTED_SUBAGENT_EVENTS = /* @__PURE__ */ new Set([
|
|
@@ -4658,7 +4687,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
4658
4687
|
spentUsd: e.spentUsd,
|
|
4659
4688
|
isSubAgent: this.isSubAgent
|
|
4660
4689
|
});
|
|
4661
|
-
this.
|
|
4690
|
+
this.appendDriverNotice(this.budgetExhaustedBubble(e));
|
|
4662
4691
|
return this.turnDone("budget-exhausted", budgetDetailOf(e));
|
|
4663
4692
|
}
|
|
4664
4693
|
/**
|
|
@@ -4694,7 +4723,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
4694
4723
|
upstreamType: e.upstreamType,
|
|
4695
4724
|
isSubAgent: this.isSubAgent
|
|
4696
4725
|
});
|
|
4697
|
-
this.
|
|
4726
|
+
this.appendDriverNotice(this.providerRefusedBubble());
|
|
4698
4727
|
return this.turnDone("provider-refused", void 0, detail);
|
|
4699
4728
|
}
|
|
4700
4729
|
/** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
|
|
@@ -5218,12 +5247,12 @@ Output format (strict):
|
|
|
5218
5247
|
summaryText = (await defaultProvider.prompt(userMessage, { systemPrompt }))?.trim() ?? "";
|
|
5219
5248
|
} catch (e) {
|
|
5220
5249
|
logger2.warn(`ChatDriver: compaction summary failed: ${e instanceof Error ? e.message : e}`);
|
|
5221
|
-
this.
|
|
5250
|
+
this.appendDriverNotice(failMessage);
|
|
5222
5251
|
return null;
|
|
5223
5252
|
}
|
|
5224
5253
|
if (!summaryText) {
|
|
5225
5254
|
logger2.warn("ChatDriver: compaction summary was empty; leaving history untouched");
|
|
5226
|
-
this.
|
|
5255
|
+
this.appendDriverNotice(failMessage);
|
|
5227
5256
|
return null;
|
|
5228
5257
|
}
|
|
5229
5258
|
let tokensBefore;
|
|
@@ -5540,10 +5569,9 @@ Output format (strict):
|
|
|
5540
5569
|
name: e instanceof Error ? e.name : void 0,
|
|
5541
5570
|
message: e instanceof Error ? e.message : String(e)
|
|
5542
5571
|
});
|
|
5543
|
-
this.
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
});
|
|
5572
|
+
this.appendDriverNotice(
|
|
5573
|
+
"Sorry, something went wrong on my end. Please try again in a moment."
|
|
5574
|
+
);
|
|
5547
5575
|
result = this.turnDone("exception");
|
|
5548
5576
|
return result;
|
|
5549
5577
|
} finally {
|
|
@@ -5805,10 +5833,9 @@ Output format (strict):
|
|
|
5805
5833
|
name: e instanceof Error ? e.name : void 0,
|
|
5806
5834
|
message: e instanceof Error ? e.message : String(e)
|
|
5807
5835
|
});
|
|
5808
|
-
this.
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
});
|
|
5836
|
+
this.appendDriverNotice(
|
|
5837
|
+
"Sorry, something went wrong on my end. Please try again in a moment."
|
|
5838
|
+
);
|
|
5812
5839
|
result = this.turnDone("exception");
|
|
5813
5840
|
return result;
|
|
5814
5841
|
} finally {
|
|
@@ -5987,7 +6014,7 @@ Output format (strict):
|
|
|
5987
6014
|
if (this.isSubAgent) {
|
|
5988
6015
|
this.failSubAgent("provider_refused", void 0, this.providerRefusedDetail);
|
|
5989
6016
|
} else {
|
|
5990
|
-
this.
|
|
6017
|
+
this.appendDriverNotice(this.providerRefusedBubble());
|
|
5991
6018
|
}
|
|
5992
6019
|
return this.turnDone("provider-refused", void 0, this.providerRefusedDetail);
|
|
5993
6020
|
}
|
|
@@ -6012,10 +6039,7 @@ Output format (strict):
|
|
|
6012
6039
|
if (this.isSubAgent) {
|
|
6013
6040
|
this.failSubAgent("budget_exhausted", this.budgetWallDetail);
|
|
6014
6041
|
} else {
|
|
6015
|
-
this.
|
|
6016
|
-
role: "assistant",
|
|
6017
|
-
content: this.budgetExhaustedBubble(this.budgetWallDetail)
|
|
6018
|
-
});
|
|
6042
|
+
this.appendDriverNotice(this.budgetExhaustedBubble(this.budgetWallDetail));
|
|
6019
6043
|
}
|
|
6020
6044
|
return this.turnDone("budget-exhausted", this.budgetWallDetail);
|
|
6021
6045
|
}
|
|
@@ -6052,7 +6076,11 @@ Output format (strict):
|
|
|
6052
6076
|
this.activeAgentLabel = await this.displayName(promptCtx);
|
|
6053
6077
|
}
|
|
6054
6078
|
const foldSuffix = this.buildFoldSystemPromptSuffix();
|
|
6055
|
-
const retryNudge = malformedAttempts > 0 ?
|
|
6079
|
+
const retryNudge = malformedAttempts > 0 ? `
|
|
6080
|
+
|
|
6081
|
+
${MALFORMED_CALL_NUDGE}` : emptyResponseAttempts > 0 ? `
|
|
6082
|
+
|
|
6083
|
+
${EMPTY_RESPONSE_NUDGE}` : "";
|
|
6056
6084
|
const frameworkSystemSuffix = `${foldSuffix}${retryNudge}`;
|
|
6057
6085
|
const primer = normalizeForProvider([
|
|
6058
6086
|
...this.primerHistory ?? [],
|
|
@@ -6176,10 +6204,9 @@ ${tailBody}
|
|
|
6176
6204
|
if (this.isSubAgent) {
|
|
6177
6205
|
this.failSubAgent("context_exhausted");
|
|
6178
6206
|
} else {
|
|
6179
|
-
this.
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
});
|
|
6207
|
+
this.appendDriverNotice(
|
|
6208
|
+
"I had to stop here \u2014 this conversation has filled the available context. Compact it to free up room, then ask me to continue."
|
|
6209
|
+
);
|
|
6183
6210
|
}
|
|
6184
6211
|
return this.turnDone("context-exhausted");
|
|
6185
6212
|
}
|
|
@@ -6214,10 +6241,9 @@ ${tailBody}
|
|
|
6214
6241
|
if (this.isSubAgent) {
|
|
6215
6242
|
this.failSubAgent("malformed_tool_call");
|
|
6216
6243
|
} else {
|
|
6217
|
-
this.
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
});
|
|
6244
|
+
this.appendDriverNotice(
|
|
6245
|
+
"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."
|
|
6246
|
+
);
|
|
6221
6247
|
}
|
|
6222
6248
|
return this.turnDone("malformed-function-call");
|
|
6223
6249
|
}
|
|
@@ -6235,10 +6261,9 @@ ${tailBody}
|
|
|
6235
6261
|
if (this.isSubAgent) {
|
|
6236
6262
|
this.failSubAgent("response_truncated");
|
|
6237
6263
|
} else {
|
|
6238
|
-
this.
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
});
|
|
6264
|
+
this.appendDriverNotice(
|
|
6265
|
+
"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."
|
|
6266
|
+
);
|
|
6242
6267
|
}
|
|
6243
6268
|
return this.turnDone("response-truncated");
|
|
6244
6269
|
}
|
|
@@ -6256,10 +6281,7 @@ ${tailBody}
|
|
|
6256
6281
|
if (this.isSubAgent) {
|
|
6257
6282
|
this.failSubAgent("context_exhausted");
|
|
6258
6283
|
} else {
|
|
6259
|
-
this.
|
|
6260
|
-
role: "assistant",
|
|
6261
|
-
content: DEFAULT_CONTEXT_OVERFLOW_MESSAGE
|
|
6262
|
-
});
|
|
6284
|
+
this.appendDriverNotice(DEFAULT_CONTEXT_OVERFLOW_MESSAGE);
|
|
6263
6285
|
}
|
|
6264
6286
|
return this.turnDone("context-exhausted");
|
|
6265
6287
|
}
|
|
@@ -6312,10 +6334,9 @@ ${tailBody}
|
|
|
6312
6334
|
if (this.isSubAgent) {
|
|
6313
6335
|
this.failSubAgent("timeout");
|
|
6314
6336
|
} else {
|
|
6315
|
-
this.
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
});
|
|
6337
|
+
this.appendDriverNotice(
|
|
6338
|
+
"The request timed out. You can ask me to try again, or break this into a smaller step."
|
|
6339
|
+
);
|
|
6319
6340
|
}
|
|
6320
6341
|
return this.turnDone("exception");
|
|
6321
6342
|
}
|
|
@@ -6355,6 +6376,11 @@ ${tailBody}
|
|
|
6355
6376
|
provider: this.lastResolvedProviderName,
|
|
6356
6377
|
attempt: emptyResponseAttempts,
|
|
6357
6378
|
maxAttempts: MAX_EMPTY_RESPONSE_RETRIES,
|
|
6379
|
+
// How many tools the agent was holding. Every empty response in the session that
|
|
6380
|
+
// motivated this (GENC-1573) came from an agent with ZERO — a tool-less agent asked
|
|
6381
|
+
// something its prompt gives it no instruction for has nothing it is allowed to do,
|
|
6382
|
+
// and reads as a model fault unless this number is on the record.
|
|
6383
|
+
toolCount: this.toolDefinitions.length,
|
|
6358
6384
|
// Provider diagnostic (Gemini: finishReason, thoughtsTokens, parts,
|
|
6359
6385
|
// blockReason) so the timeline shows *why* the turn came back blank —
|
|
6360
6386
|
// e.g. a high thoughtsTokens with a 'STOP' finish is "thought, then
|
|
@@ -6372,16 +6398,18 @@ ${tailBody}
|
|
|
6372
6398
|
agent: this.activeAgentName,
|
|
6373
6399
|
provider: this.lastResolvedProviderName,
|
|
6374
6400
|
attempts: emptyResponseAttempts,
|
|
6401
|
+
// See the retry above: a zero here says the agent had no tools, which is most of the
|
|
6402
|
+
// explanation for a blank turn and is otherwise invisible.
|
|
6403
|
+
toolCount: this.toolDefinitions.length,
|
|
6375
6404
|
isSubAgent: this.isSubAgent,
|
|
6376
6405
|
...response.responseMeta
|
|
6377
6406
|
});
|
|
6378
6407
|
if (this.isSubAgent) {
|
|
6379
6408
|
this.failSubAgent(isRefusal ? "refusal" : "empty_response");
|
|
6380
6409
|
} else {
|
|
6381
|
-
this.
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
});
|
|
6410
|
+
this.appendDriverNotice(
|
|
6411
|
+
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."
|
|
6412
|
+
);
|
|
6385
6413
|
}
|
|
6386
6414
|
return this.turnDone(failureReason2);
|
|
6387
6415
|
} else {
|
|
@@ -6686,10 +6714,9 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
|
|
|
6686
6714
|
if (this.isSubAgent) {
|
|
6687
6715
|
this.failSubAgent("unknown_tool_limit");
|
|
6688
6716
|
} else {
|
|
6689
|
-
this.
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
});
|
|
6717
|
+
this.appendDriverNotice(
|
|
6718
|
+
"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."
|
|
6719
|
+
);
|
|
6693
6720
|
}
|
|
6694
6721
|
return this.turnDone("unknown-tool-limit");
|
|
6695
6722
|
}
|
|
@@ -6715,15 +6742,33 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
|
|
|
6715
6742
|
if (this.isSubAgent) {
|
|
6716
6743
|
this.failSubAgent("max_iterations");
|
|
6717
6744
|
} else {
|
|
6718
|
-
this.
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
});
|
|
6745
|
+
this.appendDriverNotice(
|
|
6746
|
+
"I've reached my limit for this response. You can ask me to continue and I'll pick up where I left off."
|
|
6747
|
+
);
|
|
6722
6748
|
}
|
|
6723
6749
|
failureReason = "max-iterations";
|
|
6724
6750
|
}
|
|
6725
6751
|
return this.turnDone(failureReason);
|
|
6726
6752
|
}
|
|
6753
|
+
/**
|
|
6754
|
+
* Append prose the DRIVER wrote rather than the model — a failure notice, a limit explanation,
|
|
6755
|
+
* the budget wall. Renders as an ordinary assistant bubble and is flagged
|
|
6756
|
+
* `excludeFromProvider`, so it never re-enters the next request.
|
|
6757
|
+
*
|
|
6758
|
+
* Every such notice must go through here. Written as one funnel rather than a flag repeated at
|
|
6759
|
+
* eleven call sites because the failure mode is silent: a notice that keeps the flag reads
|
|
6760
|
+
* identically in the transcript to one that loses it, and the damage only shows up a turn later
|
|
6761
|
+
* as the model echoing its own error message. Measured (GENC-1573): with the empty-response
|
|
6762
|
+
* notice in history, the next turn reproduced it verbatim in 45 output tokens — succeeding,
|
|
6763
|
+
* with no retry and no `turn.error`, while still showing the user a failure. From there the
|
|
6764
|
+
* loop sustains itself whether or not the model answers, which is why "try again" never worked.
|
|
6765
|
+
*
|
|
6766
|
+
* Not for anything the model produced, and not for a widget/interaction message: those are real
|
|
6767
|
+
* turn content and the model must keep seeing them.
|
|
6768
|
+
*/
|
|
6769
|
+
appendDriverNotice(content) {
|
|
6770
|
+
this.appendToHistory({ role: "assistant", content, excludeFromProvider: true });
|
|
6771
|
+
}
|
|
6727
6772
|
appendToHistory(message) {
|
|
6728
6773
|
const tagged = {
|
|
6729
6774
|
...message,
|
|
@@ -6748,6 +6793,8 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
|
|
|
6748
6793
|
var DEFAULT_MAX_HANDOFFS = 3;
|
|
6749
6794
|
var DEFAULT_CLASSIFIER_HISTORY_LENGTH = 4;
|
|
6750
6795
|
var DEFAULT_CLASSIFIER_RETRIES = 2;
|
|
6796
|
+
var CLASSIFIER_MESSAGE_CHARS = 600;
|
|
6797
|
+
var CLASSIFIER_TRANSCRIPT_CHARS = 3e3;
|
|
6751
6798
|
var REQUEST_CONTINUATION_DEFINITION = {
|
|
6752
6799
|
name: REQUEST_CONTINUATION_TOOL,
|
|
6753
6800
|
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.",
|
|
@@ -6772,10 +6819,19 @@ function isSpecialist(agent) {
|
|
|
6772
6819
|
function isFallback(agent) {
|
|
6773
6820
|
return agent.fallback === true;
|
|
6774
6821
|
}
|
|
6822
|
+
var UNROUTABLE_PLACEHOLDER = "{{unroutable}}";
|
|
6823
|
+
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.
|
|
6824
|
+
|
|
6825
|
+
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.
|
|
6826
|
+
|
|
6827
|
+
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.`;
|
|
6775
6828
|
function buildFallbackSystemPrompt(fallback, specialists) {
|
|
6776
6829
|
const agentList = specialists.map((s) => `- ${s.name}: ${s.description}`).join("\n");
|
|
6777
6830
|
if (typeof fallback.systemPrompt === "string") {
|
|
6778
|
-
|
|
6831
|
+
const withAgents = fallback.systemPrompt.replace("{{agents}}", agentList);
|
|
6832
|
+
return withAgents.includes(UNROUTABLE_PLACEHOLDER) ? withAgents.replace(UNROUTABLE_PLACEHOLDER, UNROUTABLE_INSTRUCTION) : `${withAgents}
|
|
6833
|
+
|
|
6834
|
+
${UNROUTABLE_INSTRUCTION}`;
|
|
6779
6835
|
}
|
|
6780
6836
|
if (typeof fallback.systemPrompt === "function") {
|
|
6781
6837
|
return fallback.systemPrompt;
|
|
@@ -6784,7 +6840,9 @@ function buildFallbackSystemPrompt(fallback, specialists) {
|
|
|
6784
6840
|
|
|
6785
6841
|
${agentList}
|
|
6786
6842
|
|
|
6787
|
-
Politely let the user know what you can help with and invite them to rephrase their request
|
|
6843
|
+
Politely let the user know what you can help with and invite them to rephrase their request.
|
|
6844
|
+
|
|
6845
|
+
${UNROUTABLE_INSTRUCTION}`;
|
|
6788
6846
|
}
|
|
6789
6847
|
var OrchestratingDriver = class extends EventTarget {
|
|
6790
6848
|
constructor(providerRegistry, agents, options = {}) {
|
|
@@ -7015,11 +7073,19 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
7015
7073
|
);
|
|
7016
7074
|
const pinned = this.resolvePinnedAgent();
|
|
7017
7075
|
if (!pinned) this.dispatchEvent(new CustomEvent("orchestrating-start"));
|
|
7018
|
-
let
|
|
7076
|
+
let routed;
|
|
7077
|
+
if (pinned) {
|
|
7078
|
+
this.recordRouting("pinned", pinned, { durationMs: 0 });
|
|
7079
|
+
routed = { agent: pinned, decision: "pinned" };
|
|
7080
|
+
} else {
|
|
7081
|
+
routed = await this.classify(input, history, this.activeAgent);
|
|
7082
|
+
}
|
|
7083
|
+
let currentAgent = routed.agent;
|
|
7019
7084
|
let isHandoff = false;
|
|
7020
7085
|
let handoffs = 0;
|
|
7021
7086
|
let handoffSummary = "";
|
|
7022
7087
|
let remainingTask = "";
|
|
7088
|
+
const attempted = [];
|
|
7023
7089
|
let lastResult;
|
|
7024
7090
|
while (true) {
|
|
7025
7091
|
if (this.cancelled) break;
|
|
@@ -7027,6 +7093,14 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
7027
7093
|
let result;
|
|
7028
7094
|
if (isHandoff) {
|
|
7029
7095
|
const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
|
|
7096
|
+
if (isFallback(currentAgent)) {
|
|
7097
|
+
const work = remainingTask?.trim() || handoffSummary?.trim();
|
|
7098
|
+
contextPrimer.push({
|
|
7099
|
+
role: "user",
|
|
7100
|
+
content: (work ? `[Routing note]: no specialist is available for the remaining work: ${work}
|
|
7101
|
+
` : "[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."
|
|
7102
|
+
});
|
|
7103
|
+
}
|
|
7030
7104
|
result = await this.chatDriver.continueFromHistory(contextPrimer);
|
|
7031
7105
|
} else {
|
|
7032
7106
|
this.userMessageAppended = true;
|
|
@@ -7050,9 +7124,13 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
7050
7124
|
handoffSummary = result.summary;
|
|
7051
7125
|
remainingTask = result.remainingTask;
|
|
7052
7126
|
isHandoff = true;
|
|
7127
|
+
if (isSpecialist(currentAgent) && !attempted.includes(currentAgent.name)) {
|
|
7128
|
+
attempted.push(currentAgent.name);
|
|
7129
|
+
}
|
|
7053
7130
|
const updatedHistory = this.chatDriver.getHistory();
|
|
7054
7131
|
this.dispatchEvent(new CustomEvent("orchestrating-start"));
|
|
7055
|
-
|
|
7132
|
+
routed = await this.classify(remainingTask, updatedHistory, void 0, attempted);
|
|
7133
|
+
currentAgent = routed.agent;
|
|
7056
7134
|
}
|
|
7057
7135
|
return lastResult?.reason === "done" ? lastResult : { reason: "done" };
|
|
7058
7136
|
}
|
|
@@ -7198,28 +7276,99 @@ var OrchestratingDriver = class extends EventTarget {
|
|
|
7198
7276
|
this.chatDriver.dispose();
|
|
7199
7277
|
this.activeAgent = void 0;
|
|
7200
7278
|
}
|
|
7201
|
-
|
|
7279
|
+
/**
|
|
7280
|
+
* Record one routing decision on the debug timeline — what was chosen, why, what it cost, and
|
|
7281
|
+
* (on a failure) what went wrong.
|
|
7282
|
+
*
|
|
7283
|
+
* This is the event whose absence made GENC-1573 undiagnosable. `classify` reaches the fallback
|
|
7284
|
+
* by four different routes and every one of them only warned to the console; because it runs
|
|
7285
|
+
* outside any ChatDriver turn it produces no `kind:'turn'` entry either, so both the decision
|
|
7286
|
+
* and its spend were invisible in an exported log.
|
|
7287
|
+
*
|
|
7288
|
+
* `usage` is why the spend line exists at all. A classifier call is a real billed request that
|
|
7289
|
+
* never enters the transcript, and `sessionUsage` is derived from the transcript — so classifier
|
|
7290
|
+
* spend is absent from the session total by construction. Recording it per decision keeps it
|
|
7291
|
+
* auditable and tells a reconciler exactly where the gap comes from: sum these to close it.
|
|
7292
|
+
*/
|
|
7293
|
+
recordRouting(decision, agent, detail = {}) {
|
|
7294
|
+
const { response, ballot, ...rest } = detail;
|
|
7295
|
+
const usage = response ? messageUsage(response) : void 0;
|
|
7296
|
+
recordMetaEvent(this.sessionKey, "routing.decided", {
|
|
7297
|
+
decision,
|
|
7298
|
+
to: agent.name,
|
|
7299
|
+
fallback: isFallback(agent),
|
|
7300
|
+
// The BALLOT, not `this.specialists`. `index` is a position in the ballot (see its
|
|
7301
|
+
// construction in `classify`), so publishing the unfiltered specialist list beside it made
|
|
7302
|
+
// `candidates[index]` resolve to the wrong agent whenever `excluded` was non-empty — and the
|
|
7303
|
+
// README sentence invited exactly that join. `this.specialists` stays recoverable as
|
|
7304
|
+
// `candidates ∪ excluded`. Absent where nothing was put to a vote, which is more truthful
|
|
7305
|
+
// than a full list. Reviewer catch on #2496.
|
|
7306
|
+
...ballot ? { candidates: ballot } : {},
|
|
7307
|
+
...rest,
|
|
7308
|
+
...usage ? { usage } : {},
|
|
7309
|
+
...response?.model ? { model: response.model } : {}
|
|
7310
|
+
});
|
|
7311
|
+
}
|
|
7312
|
+
/**
|
|
7313
|
+
* The recent-conversation excerpt for the classifier prompt, bounded in size as well as in
|
|
7314
|
+
* message count — see the `CLASSIFIER_MESSAGE_CHARS` docblock for why.
|
|
7315
|
+
*/
|
|
7316
|
+
classifierTranscript(history) {
|
|
7317
|
+
const lines = history.filter((m) => (m.role === "user" || m.role === "assistant") && reachesProvider(m)).filter((m) => !!m.content?.trim()).slice(-this.classifierHistoryLength).map((m) => {
|
|
7318
|
+
const speaker = m.role === "user" ? "User" : "Assistant";
|
|
7319
|
+
const content = m.content ?? "";
|
|
7320
|
+
const body = content.length > CLASSIFIER_MESSAGE_CHARS ? `${content.slice(0, CLASSIFIER_MESSAGE_CHARS)}\u2026` : content;
|
|
7321
|
+
return `${speaker}: ${body}`;
|
|
7322
|
+
});
|
|
7323
|
+
while (lines.length > 1 && lines.join("\n").length > CLASSIFIER_TRANSCRIPT_CHARS) {
|
|
7324
|
+
lines.shift();
|
|
7325
|
+
}
|
|
7326
|
+
return lines.join("\n");
|
|
7327
|
+
}
|
|
7328
|
+
async classify(input, history, contextAgent, excludeAgents = []) {
|
|
7329
|
+
const classifyStartedAt = Date.now();
|
|
7202
7330
|
if (this.specialists.length === 1 && !this.fallback) {
|
|
7203
|
-
|
|
7331
|
+
const only = this.specialists[0];
|
|
7332
|
+
this.recordRouting("single-candidate", only, {
|
|
7333
|
+
durationMs: Date.now() - classifyStartedAt
|
|
7334
|
+
});
|
|
7335
|
+
return { agent: only, decision: "single-candidate" };
|
|
7204
7336
|
}
|
|
7205
7337
|
if (this.specialists.length === 0) {
|
|
7206
7338
|
if (!this.fallback) {
|
|
7207
7339
|
const routable = this.agents.filter((a) => !isFallback(a));
|
|
7208
7340
|
if (routable.length === 1) {
|
|
7209
|
-
|
|
7341
|
+
this.recordRouting("no-specialists", routable[0], {
|
|
7342
|
+
durationMs: Date.now() - classifyStartedAt
|
|
7343
|
+
});
|
|
7344
|
+
return { agent: routable[0], decision: "no-specialists" };
|
|
7210
7345
|
}
|
|
7211
7346
|
}
|
|
7212
|
-
|
|
7347
|
+
const leaf = this.fallback ?? { name: "Assistant", fallback: true };
|
|
7348
|
+
this.recordRouting("no-specialists", leaf, { durationMs: Date.now() - classifyStartedAt });
|
|
7349
|
+
return { agent: leaf, decision: "no-specialists" };
|
|
7213
7350
|
}
|
|
7214
|
-
const
|
|
7215
|
-
|
|
7216
|
-
|
|
7351
|
+
const ballot = excludeAgents.length ? this.specialists.filter((a) => !excludeAgents.includes(a.name)) : this.specialists;
|
|
7352
|
+
if (ballot.length === 0) {
|
|
7353
|
+
const leaf = this.fallback ?? this.specialists[0];
|
|
7354
|
+
this.recordRouting("ballot-exhausted", leaf, {
|
|
7355
|
+
excluded: excludeAgents,
|
|
7356
|
+
durationMs: Date.now() - classifyStartedAt
|
|
7357
|
+
});
|
|
7358
|
+
return { agent: leaf, decision: "ballot-exhausted" };
|
|
7359
|
+
}
|
|
7360
|
+
const agentList = ballot.map((a, i) => `${i}: ${a.name} \u2014 ${a.description}`).join("\n");
|
|
7361
|
+
const recentMessages = this.classifierTranscript(history);
|
|
7362
|
+
const continuationHint = contextAgent && isSpecialist(contextAgent) && !excludeAgents.includes(contextAgent.name) ? `
|
|
7217
7363
|
|
|
7218
7364
|
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.` : "";
|
|
7365
|
+
const attemptedHint = excludeAgents.length ? `
|
|
7366
|
+
|
|
7367
|
+
Already attempted this turn and handed the work on, so not listed above: ${excludeAgents.join(", ")}. Do not expect another agent to repeat their part.` : "";
|
|
7219
7368
|
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.
|
|
7220
7369
|
|
|
7221
7370
|
Agents:
|
|
7222
|
-
${agentList}${continuationHint}${recentMessages ? `
|
|
7371
|
+
${agentList}${continuationHint}${attemptedHint}${recentMessages ? `
|
|
7223
7372
|
|
|
7224
7373
|
Recent conversation:
|
|
7225
7374
|
${recentMessages}` : ""}`;
|
|
@@ -7237,8 +7386,12 @@ ${recentMessages}` : ""}`;
|
|
|
7237
7386
|
}
|
|
7238
7387
|
}
|
|
7239
7388
|
};
|
|
7240
|
-
let
|
|
7389
|
+
let decision = "classifier-failed";
|
|
7390
|
+
let lastError;
|
|
7391
|
+
let attempts = 0;
|
|
7392
|
+
let lastResponse;
|
|
7241
7393
|
for (let attempt = 0; attempt <= this.classifierRetries; attempt += 1) {
|
|
7394
|
+
attempts = attempt + 1;
|
|
7242
7395
|
try {
|
|
7243
7396
|
const options = {
|
|
7244
7397
|
systemPrompt: classifierPrompt,
|
|
@@ -7249,32 +7402,56 @@ ${recentMessages}` : ""}`;
|
|
|
7249
7402
|
logger2.warn(
|
|
7250
7403
|
"OrchestratingDriver: default AI provider does not implement chat() \u2014 cannot classify."
|
|
7251
7404
|
);
|
|
7405
|
+
decision = "provider-unavailable";
|
|
7252
7406
|
break;
|
|
7253
7407
|
}
|
|
7254
7408
|
const response = await classifierProvider.chat([], input, options);
|
|
7409
|
+
lastResponse = response;
|
|
7255
7410
|
const tc = response.toolCalls?.[0];
|
|
7256
7411
|
const index = tc?.name === "select_agent" ? tc.args.agent_index : -1;
|
|
7257
|
-
if (index >= 0 && index <
|
|
7258
|
-
|
|
7412
|
+
if (index >= 0 && index < ballot.length) {
|
|
7413
|
+
const chosen = ballot[index];
|
|
7414
|
+
this.recordRouting("classified", chosen, {
|
|
7415
|
+
index,
|
|
7416
|
+
attempts,
|
|
7417
|
+
durationMs: Date.now() - classifyStartedAt,
|
|
7418
|
+
response,
|
|
7419
|
+
ballot: ballot.map((a) => a.name),
|
|
7420
|
+
...excludeAgents.length ? { excluded: excludeAgents } : {}
|
|
7421
|
+
});
|
|
7422
|
+
return { agent: chosen, decision: "classified" };
|
|
7259
7423
|
}
|
|
7260
|
-
|
|
7424
|
+
decision = "no-match";
|
|
7261
7425
|
break;
|
|
7262
7426
|
} catch (e) {
|
|
7263
7427
|
if (e instanceof BudgetExhaustedError) throw e;
|
|
7428
|
+
lastError = e instanceof Error ? `${e.name}: ${e.message}` : String(e);
|
|
7264
7429
|
logger2.warn(`OrchestratingDriver: classifier attempt ${attempt + 1} failed:`, e);
|
|
7265
7430
|
if (attempt === this.classifierRetries) {
|
|
7266
7431
|
logger2.error("OrchestratingDriver: classifier failed after all retries, using fallback");
|
|
7267
7432
|
}
|
|
7268
7433
|
}
|
|
7269
7434
|
}
|
|
7270
|
-
|
|
7271
|
-
|
|
7435
|
+
const routingDetail = {
|
|
7436
|
+
attempts,
|
|
7437
|
+
durationMs: Date.now() - classifyStartedAt,
|
|
7438
|
+
ballot: ballot.map((a) => a.name),
|
|
7439
|
+
...lastError ? { error: lastError } : {},
|
|
7440
|
+
...lastResponse ? { response: lastResponse } : {},
|
|
7441
|
+
...excludeAgents.length ? { excluded: excludeAgents } : {}
|
|
7442
|
+
};
|
|
7443
|
+
if (this.fallback) {
|
|
7444
|
+
this.recordRouting(decision, this.fallback, routingDetail);
|
|
7445
|
+
return { agent: this.fallback, decision };
|
|
7446
|
+
}
|
|
7447
|
+
if (decision === "no-match") {
|
|
7272
7448
|
const specialistNames = this.specialists.map((s) => s.name).join(", ");
|
|
7273
7449
|
this.appendInlineMessage(
|
|
7274
7450
|
`I'm not sure how to help with that. I can assist with: ${specialistNames}.`
|
|
7275
7451
|
);
|
|
7276
7452
|
}
|
|
7277
|
-
|
|
7453
|
+
this.recordRouting(decision, this.specialists[0], routingDetail);
|
|
7454
|
+
return { agent: this.specialists[0], decision };
|
|
7278
7455
|
}
|
|
7279
7456
|
/**
|
|
7280
7457
|
* Returns the agent that the current turn is locked to, if any. Flow-owner
|
|
@@ -7296,11 +7473,22 @@ ${recentMessages}` : ""}`;
|
|
|
7296
7473
|
}
|
|
7297
7474
|
return match;
|
|
7298
7475
|
}
|
|
7476
|
+
/**
|
|
7477
|
+
* Append prose the ORCHESTRATOR wrote — the handoff cap, the no-specialist apology. Flagged
|
|
7478
|
+
* `excludeFromProvider` for the same reason the driver's own notices are: it is not something a
|
|
7479
|
+
* model said, and feeding it back invites the model to echo it. See
|
|
7480
|
+
* `ChatMessage.excludeFromProvider`.
|
|
7481
|
+
*/
|
|
7299
7482
|
appendInlineMessage(content) {
|
|
7300
7483
|
const history = this.chatDriver.getHistory();
|
|
7301
7484
|
this.chatDriver.loadHistory([
|
|
7302
7485
|
...history,
|
|
7303
|
-
{
|
|
7486
|
+
{
|
|
7487
|
+
role: "assistant",
|
|
7488
|
+
content,
|
|
7489
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7490
|
+
excludeFromProvider: true
|
|
7491
|
+
}
|
|
7304
7492
|
]);
|
|
7305
7493
|
this.dispatchEvent(
|
|
7306
7494
|
new CustomEvent("history-updated", { detail: this.chatDriver.getHistory() })
|
|
@@ -7516,7 +7704,11 @@ Politely let the user know what you can help with and invite them to rephrase th
|
|
|
7516
7704
|
var strictFallbackAgent = {
|
|
7517
7705
|
name: "Assistant",
|
|
7518
7706
|
fallback: true,
|
|
7519
|
-
systemPrompt: `You are a concise assistant. The user's request is outside your scope.
|
|
7707
|
+
systemPrompt: `You are a concise assistant. The user's request is outside your scope. The available specialists are: {{agents}}.
|
|
7708
|
+
|
|
7709
|
+
{{unroutable}}
|
|
7710
|
+
|
|
7711
|
+
Answer in a single short sentence. Do not elaborate.`
|
|
7520
7712
|
};
|
|
7521
7713
|
|
|
7522
7714
|
// src/utils/usage-rows.ts
|
|
@@ -7681,10 +7873,12 @@ export {
|
|
|
7681
7873
|
AnthropicTransport,
|
|
7682
7874
|
ChatDriver,
|
|
7683
7875
|
DEFAULT_PROVIDER_REFUSED_MESSAGE,
|
|
7876
|
+
EMPTY_RESPONSE_NUDGE,
|
|
7684
7877
|
GEMINI_CACHED_INPUT_MULTIPLIER,
|
|
7685
7878
|
GEMINI_LONG_CONTEXT_THRESHOLD,
|
|
7686
7879
|
GeminiProvider,
|
|
7687
7880
|
GeminiTransport,
|
|
7881
|
+
MALFORMED_CALL_NUDGE,
|
|
7688
7882
|
MutableAIProviderRegistry,
|
|
7689
7883
|
NOOP_ACTIVITY_BUS,
|
|
7690
7884
|
OrchestratingDriver,
|
|
@@ -7693,6 +7887,8 @@ export {
|
|
|
7693
7887
|
REQUEST_CONTINUATION_TOOL,
|
|
7694
7888
|
SUPPORTED_ANTHROPIC_MODEL_IDS,
|
|
7695
7889
|
SUPPORTED_GEMINI_MODEL_IDS,
|
|
7890
|
+
UNROUTABLE_INSTRUCTION,
|
|
7891
|
+
UNROUTABLE_PLACEHOLDER,
|
|
7696
7892
|
addUsage,
|
|
7697
7893
|
agenticActivityBus,
|
|
7698
7894
|
anthropicRatesFor,
|