@genesislcap/ai-assistant 14.452.0 → 14.452.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 +74 -3
- package/dist/ai-assistant.d.ts +62 -4
- package/dist/dts/components/chat-driver/chat-driver.d.ts +60 -3
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +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/esm/components/chat-driver/chat-driver.js +215 -43
- package/dist/esm/components/chat-driver/chat-driver.test.js +134 -4
- package/dist/esm/main/main.js +1 -1
- package/dist/esm/state/debug-event-log.js +2 -1
- package/docs/migration-GENC-1312.md +176 -0
- package/docs/sub_agent.md +35 -15
- package/package.json +16 -16
- package/src/components/chat-driver/chat-driver.test.ts +187 -4
- package/src/components/chat-driver/chat-driver.ts +247 -51
- package/src/main/main.ts +1 -1
- package/src/state/debug-event-log.ts +3 -1
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ChatToolDefinition,
|
|
10
10
|
ChatToolHandlers,
|
|
11
11
|
InteractionRequestOptions,
|
|
12
|
+
SubAgentFailureReason,
|
|
12
13
|
SubAgentRequestOptions,
|
|
13
14
|
} from '@genesislcap/foundation-ai';
|
|
14
15
|
import { MalformedFunctionCallError } from '@genesislcap/foundation-ai';
|
|
@@ -40,8 +41,19 @@ const DEFAULT_MAX_FOLD_OPERATIONS = 5;
|
|
|
40
41
|
// cap reach thousands for full-session capture without the memory blowup.
|
|
41
42
|
const DEFAULT_MAX_TURN_SNAPSHOTS = 400;
|
|
42
43
|
const DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
|
|
43
|
-
|
|
44
|
+
// Stale tools (advertised in an earlier state, retired now) and fold-hidden tools are
|
|
45
|
+
// self-correcting — the model drops them once guided — so they get a higher loop-protection
|
|
46
|
+
// ceiling than hallucinated names: a few legitimate stale calls across state transitions must
|
|
47
|
+
// not prematurely end the turn. Still bounded so a genuinely stuck loop terminates.
|
|
48
|
+
const MAX_STALE_TOOL_CALLS = DEFAULT_MAX_UNKNOWN_TOOL_CALLS * 2;
|
|
49
|
+
// Gemini in particular emits short bursts of MALFORMED_FUNCTION_CALL; allow more CONSECUTIVE
|
|
50
|
+
// retries. These counters reset on any productive response, so this is a consecutive-failure
|
|
51
|
+
// ceiling, not a per-turn total.
|
|
52
|
+
const MAX_MALFORMED_RETRIES = 5;
|
|
44
53
|
const MAX_EMPTY_RESPONSE_RETRIES = 3;
|
|
54
|
+
// Transient throws while building the per-turn tool surface or calling the provider retry the
|
|
55
|
+
// SAME iteration up to this many times before propagating, rather than tearing down the turn.
|
|
56
|
+
const MAX_SETUP_TRANSPORT_RETRIES = 3;
|
|
45
57
|
const SUGGESTIONS_HISTORY_WINDOW = 8;
|
|
46
58
|
|
|
47
59
|
/** Name reserved for the cross-agent handoff tool — injected by OrchestratingDriver. */
|
|
@@ -69,8 +81,15 @@ export type ChatHistoryUpdatedEvent = CustomEvent<ReadonlyArray<ChatMessage>>;
|
|
|
69
81
|
* @beta
|
|
70
82
|
*/
|
|
71
83
|
export interface TurnSnapshot {
|
|
72
|
-
/**
|
|
73
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Turn identifier, always a string. A driver's own turns are the bare counter
|
|
86
|
+
* (`"0"`, `"1"`, … — monotonic, does not reset on agent swap). Turns forwarded
|
|
87
|
+
* up from a sub-agent are re-labelled under the parent turn that activated them
|
|
88
|
+
* — a sub-agent invoked on parent turn 5 contributes `"5-1"`, `"5-2"`, …, and a
|
|
89
|
+
* nested sub-agent on `"5-2"` contributes `"5-2-1"`, …. See
|
|
90
|
+
* `forwardSubAgentSnapshots`.
|
|
91
|
+
*/
|
|
92
|
+
turnIndex: string;
|
|
74
93
|
/** ISO timestamp captured just before the LLM call. */
|
|
75
94
|
timestamp: string;
|
|
76
95
|
/** Name of the agent active when this LLM call ran. */
|
|
@@ -218,6 +237,20 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
218
237
|
* `undefined` means the loop has not been stopped early.
|
|
219
238
|
*/
|
|
220
239
|
private subAgentCompletion: { result: unknown } | undefined;
|
|
240
|
+
/**
|
|
241
|
+
* True when this driver runs as a child sub-agent (created by a parent
|
|
242
|
+
* driver's `invokeSubAgent`). Sub-agents force tool use every turn so a turn
|
|
243
|
+
* can only end via their completion tool, and on any non-completion exit they
|
|
244
|
+
* record a typed `SubAgentFailureReason` instead of appending a
|
|
245
|
+
* user-facing message — the parent decides how to surface the failure.
|
|
246
|
+
*/
|
|
247
|
+
private isSubAgent = false;
|
|
248
|
+
/**
|
|
249
|
+
* Set when a sub-agent's tool loop ends without `completeSubAgent` being
|
|
250
|
+
* called. Read by the parent's `invokeSubAgent` to build the `{ ok: false }`
|
|
251
|
+
* branch of `requestSubAgent`. Only ever set when `isSubAgent` is true.
|
|
252
|
+
*/
|
|
253
|
+
private subAgentFailure: { reason: SubAgentFailureReason } | undefined;
|
|
221
254
|
/**
|
|
222
255
|
* Set by `releaseAgent` inside a top-level tool handler — typically a stateful
|
|
223
256
|
* agent's terminal-state handler signalling that its flow is complete and the
|
|
@@ -405,6 +438,37 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
405
438
|
return this.subAgentCompletion;
|
|
406
439
|
}
|
|
407
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Mark this driver as running as a sub-agent. Called by a parent driver's
|
|
443
|
+
* `invokeSubAgent` immediately after construction, before the first turn.
|
|
444
|
+
* Enables forced tool use and typed failure reporting (see `isSubAgent`).
|
|
445
|
+
*/
|
|
446
|
+
markAsSubAgent(): void {
|
|
447
|
+
this.isSubAgent = true;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Returns the typed failure recorded when a sub-agent run ended without
|
|
452
|
+
* `completeSubAgent`, if any. Called by a parent `ChatDriver` after running
|
|
453
|
+
* this instance as a sub-agent.
|
|
454
|
+
*/
|
|
455
|
+
getSubAgentFailure(): { reason: SubAgentFailureReason } | undefined {
|
|
456
|
+
return this.subAgentFailure;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Record a sub-agent failure reason (first one wins). No-op for top-level
|
|
461
|
+
* agents, so loop-exit sites can call it unconditionally. The parent reads
|
|
462
|
+
* this via `getSubAgentFailure()` and emits the `subagent.failed` meta event
|
|
463
|
+
* under its *own* session — see `invokeSubAgent`. (A child sub-agent runs
|
|
464
|
+
* under a separate session key, so recording here would orphan the event off
|
|
465
|
+
* the user-visible debug-log timeline.)
|
|
466
|
+
*/
|
|
467
|
+
private failSubAgent(reason: SubAgentFailureReason): void {
|
|
468
|
+
if (!this.isSubAgent || this.subAgentFailure) return;
|
|
469
|
+
this.subAgentFailure = { reason };
|
|
470
|
+
}
|
|
471
|
+
|
|
408
472
|
/**
|
|
409
473
|
* Returns true if `releaseAgent` was called during the most recent turn.
|
|
410
474
|
* Consumed by the orchestrator to trigger the auto-pin release path.
|
|
@@ -424,6 +488,45 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
424
488
|
return this.turnSnapshots;
|
|
425
489
|
}
|
|
426
490
|
|
|
491
|
+
/**
|
|
492
|
+
* Merge a sub-agent's turn snapshots into this driver's buffer so they surface
|
|
493
|
+
* as `kind:'turn'` entries in the exported debug log. The child runs as a
|
|
494
|
+
* separate, discarded driver, so its snapshots would otherwise be lost. Each is
|
|
495
|
+
* re-labelled under the parent turn that activated the sub-agent: the child's
|
|
496
|
+
* own (numeric) turns become `"<parentTurn>-1"`, `"-2"`, … (1-based, in order);
|
|
497
|
+
* any already-forwarded grand-child labels (strings) have their leading segment
|
|
498
|
+
* remapped the same way, so nesting composes (`"5-2"` → `"5-2-1"`).
|
|
499
|
+
*
|
|
500
|
+
* Note: two sub-agents invoked in the *same* parent turn share the prefix, so
|
|
501
|
+
* their labels can repeat — `agentName` on each snapshot disambiguates them.
|
|
502
|
+
*/
|
|
503
|
+
private forwardSubAgentSnapshots(childSnapshots: ReadonlyArray<TurnSnapshot>): void {
|
|
504
|
+
if (childSnapshots.length === 0) return;
|
|
505
|
+
// The activating parent turn = the most recent snapshot this driver recorded
|
|
506
|
+
// before entering the tool handler that invoked the sub-agent.
|
|
507
|
+
const parentTurn = Math.max(0, this.globalTurnIndex - 1);
|
|
508
|
+
const ownTurnLabel = new Map<string, string>();
|
|
509
|
+
let ownPos = 0;
|
|
510
|
+
for (const snap of childSnapshots) {
|
|
511
|
+
let turnIndex: string;
|
|
512
|
+
if (!snap.turnIndex.includes('-')) {
|
|
513
|
+
// The child's own turn (a bare counter) → number it under the parent turn.
|
|
514
|
+
ownPos += 1;
|
|
515
|
+
turnIndex = `${parentTurn}-${ownPos}`;
|
|
516
|
+
ownTurnLabel.set(snap.turnIndex, turnIndex);
|
|
517
|
+
} else {
|
|
518
|
+
// An already-forwarded grand-child label — remap its leading segment.
|
|
519
|
+
const [lead, ...rest] = snap.turnIndex.split('-');
|
|
520
|
+
const leadLabel = ownTurnLabel.get(lead) ?? `${parentTurn}-${lead}`;
|
|
521
|
+
turnIndex = [leadLabel, ...rest].join('-');
|
|
522
|
+
}
|
|
523
|
+
this.turnSnapshots.push({ ...snap, turnIndex });
|
|
524
|
+
}
|
|
525
|
+
while (this.turnSnapshots.length > this.maxTurnSnapshots) {
|
|
526
|
+
this.turnSnapshots.shift();
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
427
530
|
/**
|
|
428
531
|
* Push one snapshot to the ring buffer. Called inside `runToolLoop` just
|
|
429
532
|
* before each LLM call — that's the latest point where the prompt, tool
|
|
@@ -441,7 +544,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
441
544
|
agentSnapshot = `<getDebugSnapshot threw: ${e instanceof Error ? e.message : String(e)}>`;
|
|
442
545
|
}
|
|
443
546
|
}
|
|
444
|
-
const turnIndex = this.globalTurnIndex;
|
|
547
|
+
const turnIndex = String(this.globalTurnIndex);
|
|
445
548
|
this.globalTurnIndex += 1;
|
|
446
549
|
this.turnSnapshots.push({
|
|
447
550
|
turnIndex,
|
|
@@ -720,6 +823,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
720
823
|
|
|
721
824
|
this.busy = true;
|
|
722
825
|
this.subAgentCompletion = undefined;
|
|
826
|
+
this.subAgentFailure = undefined;
|
|
723
827
|
this.agentReleaseRequested = false;
|
|
724
828
|
this.appendToHistory({ role: 'user', content: userInput, attachments });
|
|
725
829
|
this.turnStartedAt = Date.now();
|
|
@@ -776,10 +880,13 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
776
880
|
requestSubAgent: <T = never>(
|
|
777
881
|
name: string,
|
|
778
882
|
options?: SubAgentRequestOptions,
|
|
779
|
-
): Promise<
|
|
780
|
-
|
|
883
|
+
): Promise<
|
|
884
|
+
| { ok: true; result: T; reason?: never }
|
|
885
|
+
| { ok: false; result?: never; reason: SubAgentFailureReason }
|
|
886
|
+
> =>
|
|
887
|
+
this.invokeSubAgent<T>(name, options).then(({ outcome, trace }) => {
|
|
781
888
|
if (traceCapture) traceCapture.trace = trace;
|
|
782
|
-
return
|
|
889
|
+
return outcome;
|
|
783
890
|
}),
|
|
784
891
|
}),
|
|
785
892
|
completeSubAgent: (result: unknown): void => {
|
|
@@ -812,7 +919,14 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
812
919
|
private async invokeSubAgent<T = never>(
|
|
813
920
|
name: string,
|
|
814
921
|
options?: SubAgentRequestOptions,
|
|
815
|
-
): Promise<{
|
|
922
|
+
): Promise<{
|
|
923
|
+
// Closed union (see ChatToolHandlers.requestSubAgent) — keeps `result`/`reason`
|
|
924
|
+
// accessible for consumers compiled without strictNullChecks.
|
|
925
|
+
outcome:
|
|
926
|
+
| { ok: true; result: T; reason?: never }
|
|
927
|
+
| { ok: false; result?: never; reason: SubAgentFailureReason };
|
|
928
|
+
trace: ChatMessage[];
|
|
929
|
+
}> {
|
|
816
930
|
const subConfig = this.subAgentsMap.get(name);
|
|
817
931
|
if (!subConfig) {
|
|
818
932
|
const available = [...this.subAgentsMap.keys()].join(', ') || '(none)';
|
|
@@ -846,6 +960,9 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
846
960
|
];
|
|
847
961
|
|
|
848
962
|
const child = new ChatDriver(this.providerRegistry);
|
|
963
|
+
// Mark before the first turn so the child forces tool use and reports a
|
|
964
|
+
// typed failure (rather than user-facing text) if it never completes.
|
|
965
|
+
child.markAsSubAgent();
|
|
849
966
|
child.applyAgent({ ...subConfig, primerHistory: effectivePrimer });
|
|
850
967
|
// Route interactions back through this driver so widgets render in the
|
|
851
968
|
// parent's (ultimately the root's) history and resolve via the same
|
|
@@ -891,16 +1008,30 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
891
1008
|
}
|
|
892
1009
|
|
|
893
1010
|
const trace = child.getHistory() as ChatMessage[];
|
|
1011
|
+
// Forward the child's per-LLM-call snapshots onto this (parent) driver's
|
|
1012
|
+
// buffer so they show as `kind:'turn'` entries in the exported debug log,
|
|
1013
|
+
// re-numbered under the activating parent turn. Runs for both success and
|
|
1014
|
+
// failure so the sub-agent's turns are always visible.
|
|
1015
|
+
this.forwardSubAgentSnapshots(child.getTurnSnapshots());
|
|
894
1016
|
const completion = child.getSubAgentCompletion();
|
|
895
1017
|
|
|
896
1018
|
if (completion) {
|
|
897
|
-
return { result: completion.result as T, trace };
|
|
1019
|
+
return { outcome: { ok: true, result: completion.result as T }, trace };
|
|
898
1020
|
}
|
|
899
1021
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1022
|
+
// No completion → the sub-agent's loop ended without calling its completion
|
|
1023
|
+
// tool. Surface the typed reason it recorded; default to 'max_iterations'
|
|
1024
|
+
// for the defensive case where the loop ended with no reason set (e.g. a
|
|
1025
|
+
// provider ignored forced tool use and returned text). The previous
|
|
1026
|
+
// final-text fallback is intentionally gone — sub-agents return a
|
|
1027
|
+
// structured outcome only, and the parent handler decides how to recover.
|
|
1028
|
+
const reason = child.getSubAgentFailure()?.reason ?? 'max_iterations';
|
|
1029
|
+
// Record under THIS (parent) driver's session so the failure lands on the
|
|
1030
|
+
// user-visible debug-log timeline — the child ran under its own session key.
|
|
1031
|
+
// This is also the only telemetry for the defensive default above, where the
|
|
1032
|
+
// child's loop ended without recording an explicit failure reason.
|
|
1033
|
+
recordMetaEvent(this.sessionKey, 'subagent.failed', { agent: name, reason });
|
|
1034
|
+
return { outcome: { ok: false, reason }, trace };
|
|
904
1035
|
}
|
|
905
1036
|
|
|
906
1037
|
/**
|
|
@@ -912,6 +1043,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
912
1043
|
|
|
913
1044
|
this.busy = true;
|
|
914
1045
|
this.subAgentCompletion = undefined;
|
|
1046
|
+
this.subAgentFailure = undefined;
|
|
915
1047
|
this.turnStartedAt = Date.now();
|
|
916
1048
|
recordMetaEvent(this.sessionKey, 'turn.start', {
|
|
917
1049
|
phase: 'continueFromHistory',
|
|
@@ -1133,6 +1265,10 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1133
1265
|
let iterations = 0;
|
|
1134
1266
|
let malformedAttempts = 0;
|
|
1135
1267
|
let emptyResponseAttempts = 0;
|
|
1268
|
+
// Bounded retries for transient throws while resolving the per-turn tool surface or
|
|
1269
|
+
// calling the provider. Without this, a single transient throw tears down the whole turn
|
|
1270
|
+
// and strands the agent's unflushed work behind an opaque error.
|
|
1271
|
+
let setupTransportAttempts = 0;
|
|
1136
1272
|
// True only for the very first LLM call. Used to exclude the pending user message
|
|
1137
1273
|
// from history (it is passed separately as currentInput). Must not be derived from
|
|
1138
1274
|
// `iterations` because fold operations decrement iterations, which would incorrectly
|
|
@@ -1155,17 +1291,31 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1155
1291
|
// forbidden when a factory is set, so the array form is always valid.
|
|
1156
1292
|
// Sequential await is required — each iteration must see fresh values
|
|
1157
1293
|
// before constructing the LLM request.
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
//
|
|
1163
|
-
//
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1294
|
+
// A transient throw while building the tool surface should retry the iteration, not
|
|
1295
|
+
// tear down the whole turn and strand the agent's unflushed buffer behind an opaque
|
|
1296
|
+
// error. The handler-map factory re-resolves in lockstep so dispatch sees only the
|
|
1297
|
+
// handlers valid for the current state, in step with the tool definitions exposed
|
|
1298
|
+
// above. Folds are forbidden when either factory is set, so the fold-mutation paths
|
|
1299
|
+
// on `this.toolDefinitions` / `this.toolHandlers` are unreachable.
|
|
1300
|
+
try {
|
|
1301
|
+
if (this.toolDefinitionsFactory) {
|
|
1302
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
1303
|
+
this.toolDefinitions = await this.toolDefinitionsFactory(promptCtx);
|
|
1304
|
+
}
|
|
1305
|
+
if (this.toolHandlersFactory) {
|
|
1306
|
+
// oxlint-disable-next-line no-await-in-loop
|
|
1307
|
+
this.toolHandlers = await this.toolHandlersFactory(promptCtx);
|
|
1308
|
+
}
|
|
1309
|
+
} catch (e) {
|
|
1310
|
+
setupTransportAttempts += 1;
|
|
1311
|
+
if (setupTransportAttempts < MAX_SETUP_TRANSPORT_RETRIES) {
|
|
1312
|
+
logger.warn(
|
|
1313
|
+
`ChatDriver: tool-surface resolution failed, retrying (${setupTransportAttempts}/${MAX_SETUP_TRANSPORT_RETRIES})`,
|
|
1314
|
+
);
|
|
1315
|
+
iterations -= 1;
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
throw e;
|
|
1169
1319
|
}
|
|
1170
1320
|
|
|
1171
1321
|
// Record everything advertised this turn so the unknown-tool path can tell
|
|
@@ -1227,6 +1377,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1227
1377
|
// Strip fold-only properties (foldEvent, foldPath) before sending to provider
|
|
1228
1378
|
tools: this.toolDefinitions.length ? this.toolDefinitions : undefined,
|
|
1229
1379
|
attachments: attachmentsForCall,
|
|
1380
|
+
// Sub-agents must finish by calling a tool (their completion tool), never
|
|
1381
|
+
// by emitting a free-text turn — force tool use so the provider can't
|
|
1382
|
+
// return a bare text answer. Top-level agents stay on the default 'auto'.
|
|
1383
|
+
// (Transports no-op the force when no tools are advertised.)
|
|
1384
|
+
toolChoice: this.isSubAgent ? 'required' : undefined,
|
|
1230
1385
|
};
|
|
1231
1386
|
|
|
1232
1387
|
// Resolve the active provider for this turn. Static names were validated
|
|
@@ -1262,14 +1417,31 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1262
1417
|
provider: this.lastResolvedProviderName,
|
|
1263
1418
|
attempts: malformedAttempts,
|
|
1264
1419
|
finishMessage: e.finishMessage,
|
|
1420
|
+
isSubAgent: this.isSubAgent,
|
|
1265
1421
|
});
|
|
1266
|
-
this.
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1422
|
+
if (this.isSubAgent) {
|
|
1423
|
+
// Bubble a typed failure to the parent instead of speaking to the user.
|
|
1424
|
+
this.failSubAgent('malformed_tool_call');
|
|
1425
|
+
} else {
|
|
1426
|
+
this.appendToHistory({
|
|
1427
|
+
role: 'assistant',
|
|
1428
|
+
content:
|
|
1429
|
+
'While working on your request, I repeatedly called my tools incorrectly. This often works on a second try — would you like me to try again? If it happens again, try breaking your request into smaller steps.',
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1271
1432
|
return { reason: 'done' };
|
|
1272
1433
|
}
|
|
1434
|
+
// A transient provider/transport error should retry the SAME iteration a bounded
|
|
1435
|
+
// number of times rather than tearing down the whole turn (which strands the
|
|
1436
|
+
// agent's unflushed buffer behind an opaque error message).
|
|
1437
|
+
setupTransportAttempts += 1;
|
|
1438
|
+
if (setupTransportAttempts < MAX_SETUP_TRANSPORT_RETRIES) {
|
|
1439
|
+
logger.warn(
|
|
1440
|
+
`ChatDriver: provider/transport error, retrying (${setupTransportAttempts}/${MAX_SETUP_TRANSPORT_RETRIES})`,
|
|
1441
|
+
);
|
|
1442
|
+
iterations -= 1;
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1273
1445
|
throw e;
|
|
1274
1446
|
}
|
|
1275
1447
|
|
|
@@ -1296,12 +1468,17 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1296
1468
|
agent: this.activeAgentName,
|
|
1297
1469
|
provider: this.lastResolvedProviderName,
|
|
1298
1470
|
attempts: emptyResponseAttempts,
|
|
1471
|
+
isSubAgent: this.isSubAgent,
|
|
1299
1472
|
});
|
|
1300
|
-
this.
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1473
|
+
if (this.isSubAgent) {
|
|
1474
|
+
this.failSubAgent('empty_response');
|
|
1475
|
+
} else {
|
|
1476
|
+
this.appendToHistory({
|
|
1477
|
+
role: 'assistant',
|
|
1478
|
+
content:
|
|
1479
|
+
'While working on your request, I repeatedly generated a blank response. This often works on a second try — would you like me to try again? If it happens again, try breaking your request into smaller steps.',
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1305
1482
|
return { reason: 'done' };
|
|
1306
1483
|
} else if (isThinkingStep) {
|
|
1307
1484
|
this.appendToHistory({ ...response, toolCalls: undefined, thinking: true });
|
|
@@ -1310,6 +1487,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1310
1487
|
this.appendToHistory(response);
|
|
1311
1488
|
}
|
|
1312
1489
|
|
|
1490
|
+
// Reset retry budgets on any productive (non-empty) response, so the caps mean
|
|
1491
|
+
// "N CONSECUTIVE failures" not "N total per turn".
|
|
1492
|
+
emptyResponseAttempts = 0;
|
|
1493
|
+
malformedAttempts = 0;
|
|
1494
|
+
setupTransportAttempts = 0;
|
|
1495
|
+
|
|
1313
1496
|
if (!response.toolCalls?.length) {
|
|
1314
1497
|
break;
|
|
1315
1498
|
}
|
|
@@ -1406,8 +1589,9 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1406
1589
|
// or an exclusive fold is hiding it) rather than hallucinated — a
|
|
1407
1590
|
// distinction worth making, because the model should stop retrying
|
|
1408
1591
|
// a retired tool rather than treat the failure as a typo. Stale
|
|
1409
|
-
// calls still
|
|
1410
|
-
//
|
|
1592
|
+
// calls still trip loop protection, but at a higher ceiling than
|
|
1593
|
+
// hallucinated tools (see below) — they are self-correcting, so the
|
|
1594
|
+
// guidance, telemetry, and limit differ.
|
|
1411
1595
|
if (this.everSeenToolNames.has(tc.name)) {
|
|
1412
1596
|
this.consecutiveUnknownToolCalls += 1;
|
|
1413
1597
|
const hidingFold = this.foldHidingTool(tc.name);
|
|
@@ -1415,12 +1599,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1415
1599
|
if (hidingFold) {
|
|
1416
1600
|
content = `"${tc.name}" is not available while the "${hidingFold}" fold is open. Call close_${hidingFold} to return to the previous set of tools, then call ${tc.name}.`;
|
|
1417
1601
|
logger.warn(
|
|
1418
|
-
`ChatDriver: tool "${tc.name}" is hidden behind open fold "${hidingFold}" (${this.consecutiveUnknownToolCalls}/${
|
|
1602
|
+
`ChatDriver: tool "${tc.name}" is hidden behind open fold "${hidingFold}" (${this.consecutiveUnknownToolCalls}/${MAX_STALE_TOOL_CALLS})`,
|
|
1419
1603
|
);
|
|
1420
1604
|
} else {
|
|
1421
1605
|
content = `"${tc.name}" was available earlier but is not part of the current step — that step is complete, so do not call it again. Continue with the tools available now: ${Object.keys(this.toolHandlers).join(', ') || '(none)'}.`;
|
|
1422
1606
|
logger.warn(
|
|
1423
|
-
`ChatDriver: stale tool "${tc.name}" — advertised earlier this activation but retired in the current state (${this.consecutiveUnknownToolCalls}/${
|
|
1607
|
+
`ChatDriver: stale tool "${tc.name}" — advertised earlier this activation but retired in the current state (${this.consecutiveUnknownToolCalls}/${MAX_STALE_TOOL_CALLS})`,
|
|
1424
1608
|
);
|
|
1425
1609
|
}
|
|
1426
1610
|
recordMetaEvent(this.sessionKey, 'tool.unresolved', {
|
|
@@ -1429,14 +1613,14 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1429
1613
|
kind: hidingFold ? 'fold-hidden' : 'stale',
|
|
1430
1614
|
fold: hidingFold ?? undefined,
|
|
1431
1615
|
consecutive: this.consecutiveUnknownToolCalls,
|
|
1432
|
-
max:
|
|
1616
|
+
max: MAX_STALE_TOOL_CALLS,
|
|
1433
1617
|
});
|
|
1434
1618
|
executedById.set(tc.id, { toolCallId: tc.id, content });
|
|
1435
1619
|
unknownToolIds.add(tc.id);
|
|
1436
1620
|
staleToolIds.add(tc.id);
|
|
1437
1621
|
this.recentUnknownToolNames.add(tc.name);
|
|
1438
1622
|
this.recentStaleToolNames.add(tc.name);
|
|
1439
|
-
if (this.consecutiveUnknownToolCalls >=
|
|
1623
|
+
if (this.consecutiveUnknownToolCalls >= MAX_STALE_TOOL_CALLS) {
|
|
1440
1624
|
hitUnknownToolLimit = true;
|
|
1441
1625
|
}
|
|
1442
1626
|
return;
|
|
@@ -1484,7 +1668,9 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1484
1668
|
});
|
|
1485
1669
|
executedById.set(tc.id, {
|
|
1486
1670
|
toolCallId: tc.id,
|
|
1487
|
-
|
|
1671
|
+
// Structured recovery hint so the model retries or routes around a tool
|
|
1672
|
+
// failure instead of apologising and giving up.
|
|
1673
|
+
content: `Tool error: ${(e as Error).message}\nRECOVERY: this tool failed once — you may retry it, or take a different valid action to make progress. Do NOT abandon the task, ask the user to rephrase, or claim you cannot make changes. If a planning tool failed, retry it or proceed with the information you already have.`,
|
|
1488
1674
|
});
|
|
1489
1675
|
anyRealToolExecuted = true; // treat errors as real work for fold op counting
|
|
1490
1676
|
}
|
|
@@ -1595,12 +1781,17 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1595
1781
|
staleTools,
|
|
1596
1782
|
hallucinatedTools,
|
|
1597
1783
|
availableTools: Object.keys(this.toolHandlers),
|
|
1784
|
+
isSubAgent: this.isSubAgent,
|
|
1598
1785
|
});
|
|
1599
|
-
this.
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1786
|
+
if (this.isSubAgent) {
|
|
1787
|
+
this.failSubAgent('unknown_tool_limit');
|
|
1788
|
+
} else {
|
|
1789
|
+
this.appendToHistory({
|
|
1790
|
+
role: 'assistant',
|
|
1791
|
+
content:
|
|
1792
|
+
"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.",
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1604
1795
|
return { reason: 'done' };
|
|
1605
1796
|
}
|
|
1606
1797
|
|
|
@@ -1627,12 +1818,17 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1627
1818
|
provider: this.lastResolvedProviderName,
|
|
1628
1819
|
iterations,
|
|
1629
1820
|
limit: this.maxToolIterations,
|
|
1821
|
+
isSubAgent: this.isSubAgent,
|
|
1630
1822
|
});
|
|
1631
|
-
this.
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1823
|
+
if (this.isSubAgent) {
|
|
1824
|
+
this.failSubAgent('max_iterations');
|
|
1825
|
+
} else {
|
|
1826
|
+
this.appendToHistory({
|
|
1827
|
+
role: 'assistant',
|
|
1828
|
+
content:
|
|
1829
|
+
"I've reached my limit for this response. You can ask me to continue and I'll pick up where I left off.",
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1636
1832
|
}
|
|
1637
1833
|
|
|
1638
1834
|
return { reason: 'done' };
|
package/src/main/main.ts
CHANGED
|
@@ -1434,7 +1434,7 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1434
1434
|
// prompt is still shown in full whenever it changes, so prompt evolution
|
|
1435
1435
|
// stays visible.
|
|
1436
1436
|
let lastFullPrompt: string | undefined;
|
|
1437
|
-
let lastFullIndex =
|
|
1437
|
+
let lastFullIndex = '';
|
|
1438
1438
|
const turns = (this.driver?.getTurnSnapshots?.() ?? []).map((t) => {
|
|
1439
1439
|
let { systemPrompt } = t;
|
|
1440
1440
|
if (systemPrompt != null && systemPrompt === lastFullPrompt) {
|
|
@@ -46,6 +46,7 @@ export type MetaEventType =
|
|
|
46
46
|
| 'turn.error'
|
|
47
47
|
| 'tool.failed'
|
|
48
48
|
| 'tool.unresolved'
|
|
49
|
+
| 'subagent.failed'
|
|
49
50
|
// Routing / providers
|
|
50
51
|
| 'agent.handoff'
|
|
51
52
|
| 'agent.pinned'
|
|
@@ -83,6 +84,7 @@ export type MetaEventImportance = 'high' | 'normal' | 'low';
|
|
|
83
84
|
export const META_EVENT_IMPORTANCE: Record<MetaEventType, MetaEventImportance> = {
|
|
84
85
|
'turn.error': 'high',
|
|
85
86
|
'tool.failed': 'high',
|
|
87
|
+
'subagent.failed': 'high',
|
|
86
88
|
'file.read-failed': 'high',
|
|
87
89
|
'suggestions.failed': 'high',
|
|
88
90
|
'context.threshold-crossed': 'high',
|
|
@@ -241,7 +243,7 @@ export const DEBUG_LOG_README: readonly string[] = [
|
|
|
241
243
|
'This is an exported debug log for the Genesis AI assistant. Read it top-to-bottom.',
|
|
242
244
|
'`timeline` is the entire session as one array, already sorted chronologically by `timestamp` (ISO 8601). Every entry has a `kind`.',
|
|
243
245
|
"kind:'message' — the conversation. `role` is user/assistant/tool/system-event; `agentName` says which agent produced it; `toolCalls`/`toolResult`/`interaction` carry tool and widget activity; `inputTokens`/`outputTokens`/`cost` are per-message usage.",
|
|
244
|
-
"kind:'turn' — one LLM call. `systemPrompt` and `toolNames` are what the model saw. A systemPrompt of '<repeated — identical to turn N>' was byte-identical to turn N and de-duplicated; the full prompt is shown whenever it changes (often because a stateful agent advanced), so prompt evolution is visible.",
|
|
246
|
+
"kind:'turn' — one LLM call. `turnIndex` is a string: a top-level turn is the bare counter ('0', '1', …); a sub-agent's turns are numbered under the parent turn that activated them ('3-1', '3-2', …, and a nested sub-agent contributes '3-2-1', …), and `agentName` names the agent that ran the turn. `systemPrompt` and `toolNames` are what the model saw. A systemPrompt of '<repeated — identical to turn N>' was byte-identical to turn N and de-duplicated; the full prompt is shown whenever it changes (often because a stateful agent advanced), so prompt evolution is visible.",
|
|
245
247
|
"kind:'turn'.`agentSnapshot` — the active agent's own view of its internal state, captured at that turn. An agent opts into this by exposing a `getDebugSnapshot()` that returns JSON-serializable per-state info; stateful/flow agents wire it automatically, so you can watch a flow advance turn-by-turn (e.g. current step, cursor, collected fields, pending changes). Absent for agents that don't expose one.",
|
|
246
248
|
"kind:'event' — a meta/lifecycle event. `type` names it (see below); `detail` carries structured data. `detail.placement` is the emitting UI instance: 'bubble' (collapsed), 'panel' (popped-out), or 'standalone'.",
|
|
247
249
|
"Each 'event' also has an `importance`: 'high' (failures/limits — turn.error, tool.failed, file.read-failed, suggestions.failed, context.threshold-crossed), 'normal' (session flow — connects, turns, retries, handoffs, agent/provider changes, interactions), or 'low' (skippable UI/bookkeeping noise — panel.toggled, attachment.added, driver.wired/unwired, context.updated). To skim, ignore importance:'low'; to triage a failure, filter to importance:'high' then read the nearby messages and turns. A 'high' turn.error is often preceded by one or more 'normal' turn.retry events for the same reason — read them together to see how many attempts were made before bailing. 'message' and 'turn' entries carry no importance — they are the substance, always read them.",
|