@genesislcap/ai-assistant 14.451.3 → 14.451.4

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.
@@ -631,6 +631,21 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
631
631
  * hallucinated. Reset alongside `consecutiveUnknownToolCalls`.
632
632
  */
633
633
  private readonly recentUnknownToolNames;
634
+ /**
635
+ * Union of every tool name advertised at any point during the current agent
636
+ * activation. Lets the unknown-tool path tell a *stale* call (a real tool from
637
+ * an earlier state, now retired — or one an open exclusive fold is hiding)
638
+ * apart from a *hallucinated* one. Reset on agent swap in `applyAgent`.
639
+ */
640
+ private readonly everSeenToolNames;
641
+ /**
642
+ * Subset of the current unknown-tool streak that was stale (previously
643
+ * available) rather than hallucinated — surfaced separately on the
644
+ * `unknown-tool-limit` turn.error so triage can tell a state/prompt-design
645
+ * problem from a model that's inventing tools. Reset alongside
646
+ * `recentUnknownToolNames`.
647
+ */
648
+ private readonly recentStaleToolNames;
634
649
  private readonly maxFoldOperations;
635
650
  /** Sub-agents declared on the active agent config, keyed by name. */
636
651
  private subAgentsMap;
@@ -806,6 +821,17 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
806
821
  * to find which fold contains a given tool name. Returns the immediate parent fold name.
807
822
  */
808
823
  private findFoldContaining;
824
+ /**
825
+ * If an open fold is hiding a previously-available tool, return the name of
826
+ * the fold to close to start getting it back. Only exclusive folds hide tools
827
+ * (they replace the tool set on open rather than extending it), so a base tool
828
+ * that was visible before the fold opened now sits in a fold-stack frame's
829
+ * `previousHandlers` but not in the live handler map. Only the top fold's
830
+ * `close_` tool is active, so that's always the actionable next step — even
831
+ * when the tool lives further down the stack, closing repeatedly walks back to
832
+ * it. Returns null when no open fold accounts for the tool.
833
+ */
834
+ private foldHidingTool;
809
835
  /**
810
836
  * Install the fold's inner tool set, replacing (exclusive) or extending (non-exclusive)
811
837
  * the current tool set. Also injects the close tool. Does NOT touch the fold stack.
@@ -1579,7 +1605,7 @@ declare type MetaEventImportance = 'high' | 'normal' | 'low';
1579
1605
  * Catalogue of meta event names. This is the documented surface — extend it as
1580
1606
  * new events are wired in (Tier 2/3 lifecycle, interaction, provider events).
1581
1607
  */
1582
- declare type MetaEventType = 'assistant.connected' | 'assistant.disconnected' | 'assistant.popout' | 'assistant.popin' | 'driver.created' | 'driver.wired' | 'driver.unwired' | 'state.changed' | 'turn.start' | 'turn.end' | 'turn.retry' | 'turn.error' | 'tool.failed' | 'agent.handoff' | 'agent.pinned' | 'agent.unpinned' | 'provider.selected' | 'interaction.requested' | 'interaction.resolved' | 'context.updated' | 'context.threshold-crossed' | 'panel.toggled' | 'attachment.added' | 'file.read-failed' | 'suggestions.failed';
1608
+ declare type MetaEventType = 'assistant.connected' | 'assistant.disconnected' | 'assistant.popout' | 'assistant.popin' | 'driver.created' | 'driver.wired' | 'driver.unwired' | 'state.changed' | 'turn.start' | 'turn.end' | 'turn.retry' | 'turn.error' | 'tool.failed' | 'tool.unresolved' | 'agent.handoff' | 'agent.pinned' | 'agent.unpinned' | 'provider.selected' | 'interaction.requested' | 'interaction.resolved' | 'context.updated' | 'context.threshold-crossed' | 'panel.toggled' | 'attachment.added' | 'file.read-failed' | 'suggestions.failed';
1583
1609
 
1584
1610
  /**
1585
1611
  * Orchestrates multiple specialist agents. Sits between `FoundationAiAssistant`
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Test-only side effect: align the global `EventTarget` with jsdom's before any
3
+ * module that `extends EventTarget` is evaluated.
4
+ *
5
+ * The node test runner's jsdom setup installs `globalThis.CustomEvent` from
6
+ * jsdom but leaves `globalThis.EventTarget` as Node's native class. A class that
7
+ * `extends EventTarget` (e.g. {@link ChatDriver}) then inherits Node's native
8
+ * `dispatchEvent`, which rejects the jsdom `CustomEvent` instances it is handed
9
+ * ("The 'event' argument must be an instance of Event. Received an instance of
10
+ * CustomEvent"). Pointing `EventTarget` at jsdom's keeps the whole event family
11
+ * in one realm.
12
+ *
13
+ * No-op in a real browser, where `window.EventTarget === globalThis.EventTarget`
14
+ * already. Import this BEFORE importing anything that subclasses `EventTarget`.
15
+ */
16
+ declare const jsdomWindow: {
17
+ EventTarget?: typeof EventTarget;
18
+ };
19
+ //# sourceMappingURL=align-event-globals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"align-event-globals.d.ts","sourceRoot":"","sources":["../../../../src/components/chat-driver/align-event-globals.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,QAAA,MAAM,WAAW;kBAA6C,OAAO,WAAW;CAAY,CAAC"}
@@ -122,6 +122,21 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
122
122
  * hallucinated. Reset alongside `consecutiveUnknownToolCalls`.
123
123
  */
124
124
  private readonly recentUnknownToolNames;
125
+ /**
126
+ * Union of every tool name advertised at any point during the current agent
127
+ * activation. Lets the unknown-tool path tell a *stale* call (a real tool from
128
+ * an earlier state, now retired — or one an open exclusive fold is hiding)
129
+ * apart from a *hallucinated* one. Reset on agent swap in `applyAgent`.
130
+ */
131
+ private readonly everSeenToolNames;
132
+ /**
133
+ * Subset of the current unknown-tool streak that was stale (previously
134
+ * available) rather than hallucinated — surfaced separately on the
135
+ * `unknown-tool-limit` turn.error so triage can tell a state/prompt-design
136
+ * problem from a model that's inventing tools. Reset alongside
137
+ * `recentUnknownToolNames`.
138
+ */
139
+ private readonly recentStaleToolNames;
125
140
  private readonly maxFoldOperations;
126
141
  /** Sub-agents declared on the active agent config, keyed by name. */
127
142
  private subAgentsMap;
@@ -297,6 +312,17 @@ export declare class ChatDriver extends EventTarget implements AiDriver {
297
312
  * to find which fold contains a given tool name. Returns the immediate parent fold name.
298
313
  */
299
314
  private findFoldContaining;
315
+ /**
316
+ * If an open fold is hiding a previously-available tool, return the name of
317
+ * the fold to close to start getting it back. Only exclusive folds hide tools
318
+ * (they replace the tool set on open rather than extending it), so a base tool
319
+ * that was visible before the fold opened now sits in a fold-stack frame's
320
+ * `previousHandlers` but not in the live handler map. Only the top fold's
321
+ * `close_` tool is active, so that's always the actionable next step — even
322
+ * when the tool lives further down the stack, closing repeatedly walks back to
323
+ * it. Returns null when no open fold accounts for the tool.
324
+ */
325
+ private foldHidingTool;
300
326
  /**
301
327
  * Install the fold's inner tool set, replacing (exclusive) or extending (non-exclusive)
302
328
  * the current tool set. Also injects the close tool. Does NOT touch the fold stack.
@@ -1 +1 @@
1
- {"version":3,"file":"chat-driver.d.ts","sourceRoot":"","sources":["../../../../src/components/chat-driver/chat-driver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,kBAAkB,EAClB,cAAc,EACd,gBAAgB,EAChB,WAAW,EAKX,yBAAyB,EAE1B,MAAM,4BAA4B,CAAC;AAGpC,OAAO,KAAK,EACV,WAAW,EAGX,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,qBAAqB,CAAC;AAM7B,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAkBxE,wFAAwF;AACxF,eAAO,MAAM,yBAAyB,yBAAyB,CAAC;AAMhE;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG,WAAW,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC,CAAC;AAE9E;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,qFAAqF;IACrF,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qGAAqG;IACrG,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gGAAgG;IAChG,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAQD;;;;;;;;;GASG;AACH,qBAAa,UAAW,SAAQ,WAAY,YAAW,QAAQ;IAyI3D,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IAKjC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAGlC,oFAAoF;IACpF,OAAO,CAAC,QAAQ,CAAC,UAAU;IAjJ7B,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,IAAI,CAAS;IACrB,kFAAkF;IAClF,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,mBAAmB,CAQvB;IAEJ,OAAO,CAAC,YAAY,CAAC,CAAoB;IACzC;;;;OAIG;IACH,OAAO,CAAC,eAAe,CAAuB;IAC9C;;;;;OAKG;IACH,OAAO,CAAC,sBAAsB,CAAC,CAE2B;IAC1D;;;;;;;;OAQG;IACH,OAAO,CAAC,YAAY,CAAmB;IACvC;;;;OAIG;IACH,OAAO,CAAC,mBAAmB,CAAC,CAEsB;IAClD,OAAO,CAAC,aAAa,CAAC,CAAgB;IACtC,OAAO,CAAC,eAAe,CAAC,CAAS;IACjC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB,CAAC,CAAS;IAClC,OAAO,CAAC,WAAW,CAAC,CAAoB;IACxC;;;;;OAKG;IACH,OAAO,CAAC,wBAAwB,CAAC,CAIjB;IAChB;;;OAGG;IACH,OAAO,CAAC,wBAAwB,CAAC,CAA4C;IAE7E,8EAA8E;IAC9E,OAAO,CAAC,SAAS,CAAwB;IACzC,8FAA8F;IAC9F,OAAO,CAAC,kBAAkB,CAAK;IAC/B,6FAA6F;IAC7F,OAAO,CAAC,2BAA2B,CAAK;IACxC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAqB;IAC5D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAE3C,qEAAqE;IACrE,OAAO,CAAC,YAAY,CAAuC;IAC3D;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB,CAAkC;IAC5D;;;;;;;;OAQG;IACH,OAAO,CAAC,qBAAqB,CAAS;IACtC;;;;OAIG;IACH,OAAO,CAAC,aAAa,CAAsB;IAC3C,+FAA+F;IAC/F,OAAO,CAAC,eAAe,CAAK;IAC5B,4EAA4E;IAC5E,OAAO,CAAC,gBAAgB,CAAC,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAE1C;;;OAGG;IACH,OAAO,CAAC,mBAAmB,CAAC,CAAgB;IAC5C;;;;OAIG;IACH,OAAO,CAAC,qBAAqB,CAAiC;IAC9D,iFAAiF;IACjF,OAAO,CAAC,wBAAwB,CAAC,CAAS;IAC1C,wFAAwF;IACxF,OAAO,CAAC,0BAA0B,CAAC,CAAS;gBAGzB,gBAAgB,EAAE,kBAAkB,EACrD,YAAY,GAAE,iBAAsB,EACpC,eAAe,GAAE,oBAAyB,EAC1C,YAAY,CAAC,EAAE,iBAAiB,EAChC,aAAa,CAAC,EAAE,WAAW,EAAE,EACZ,iBAAiB,GAAE,MAAoC,EACxE,iBAAiB,GAAE,MAAoC,EACvD,gBAAgB,GAAE,MAAmC;IACrD,oFAAoF;IACnE,UAAU,GAAE,MAAW;IAuB1C;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IA4CrC;;;OAGG;IACH,qBAAqB,IAAI,MAAM;IAI/B;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;;;;OAKG;YACW,sBAAsB;IA4BpC;;;OAGG;IACH,qBAAqB,IAAI;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,SAAS;IAIxD;;;OAGG;IACH,wBAAwB,IAAI,OAAO;IAInC;;;;;;OAMG;IACH,gBAAgB,IAAI,aAAa,CAAC,YAAY,CAAC;IAI/C;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IA4B1B;;;OAGG;IACH,2BAA2B,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,GAAG,IAAI;IAIxF,UAAU,IAAI,aAAa,CAAC,WAAW,CAAC;IAIxC,aAAa,IAAI,SAAS,WAAW,EAAE;IAIvC,0DAA0D;IAC1D,kBAAkB,IAAI,MAAM,EAAE;IAIxB,cAAc,CAClB,OAAO,EAAE,WAAW,EAAE,EACtB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,eAAe,EAAE,GAC/B,OAAO,CAAC,MAAM,EAAE,CAAC;IAiHpB,MAAM,IAAI,OAAO;IAIjB;;;;;OAKG;IACI,2BAA2B,CAChC,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,yBAAyB,KAAK,OAAO,CAAC,CAAC,CAAC,GAC3F,IAAI;IAIP;;;;;;;;;;;;;;;;OAgBG;IACU,kBAAkB,CAAC,CAAC,EAC/B,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,GAAG,EACT,OAAO,CAAC,EAAE,yBAAyB,GAClC,OAAO,CAAC,CAAC,CAAC;IAuCb;;;OAGG;IACI,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,IAAI;IA0BnE;;;OAGG;IACI,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI;IAS3C,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAyC/F;;;;;;;;OAQG;IACH,OAAO,CAAC,mBAAmB;IAsC3B;;;;;OAKG;YACW,cAAc;IA8F5B;;;OAGG;IACG,mBAAmB,CAAC,eAAe,CAAC,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA0CrF,wFAAwF;IACxF,OAAO,CAAC,OAAO;IAKf;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAc1B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA+BxB,uFAAuF;IACvF,OAAO,CAAC,QAAQ;IAqChB,OAAO,CAAC,aAAa;IAQrB,8EAA8E;IAC9E,OAAO,CAAC,SAAS;IAWjB,mFAAmF;IACnF,OAAO,CAAC,2BAA2B;YAkCrB,WAAW;IAobzB,OAAO,CAAC,eAAe;CAoBxB"}
1
+ {"version":3,"file":"chat-driver.d.ts","sourceRoot":"","sources":["../../../../src/components/chat-driver/chat-driver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import './align-event-globals';
2
+ //# sourceMappingURL=chat-driver.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-driver.test.d.ts","sourceRoot":"","sources":["../../../../src/components/chat-driver/chat-driver.test.ts"],"names":[],"mappings":"AAiBA,OAAO,uBAAuB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"main.template.d.ts","sourceRoot":"","sources":["../../../src/main/main.template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AASH,OAAO,EAA2B,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAoIpD,gBAAgB;AAChB,eAAO,MAAM,6BAA6B,GACxC,oBAAoB,MAAM,KACzB,YAAY,CAAC,qBAAqB,CA8hBpC,CAAC"}
1
+ {"version":3,"file":"main.template.d.ts","sourceRoot":"","sources":["../../../src/main/main.template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AASH,OAAO,EAA2B,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAwIpD,gBAAgB;AAChB,eAAO,MAAM,6BAA6B,GACxC,oBAAoB,MAAM,KACzB,YAAY,CAAC,qBAAqB,CA8hBpC,CAAC"}
@@ -27,7 +27,7 @@
27
27
  * Catalogue of meta event names. This is the documented surface — extend it as
28
28
  * new events are wired in (Tier 2/3 lifecycle, interaction, provider events).
29
29
  */
30
- export type MetaEventType = 'assistant.connected' | 'assistant.disconnected' | 'assistant.popout' | 'assistant.popin' | 'driver.created' | 'driver.wired' | 'driver.unwired' | 'state.changed' | 'turn.start' | 'turn.end' | 'turn.retry' | 'turn.error' | 'tool.failed' | 'agent.handoff' | 'agent.pinned' | 'agent.unpinned' | 'provider.selected' | 'interaction.requested' | 'interaction.resolved' | 'context.updated' | 'context.threshold-crossed' | 'panel.toggled' | 'attachment.added' | 'file.read-failed' | 'suggestions.failed';
30
+ export type MetaEventType = 'assistant.connected' | 'assistant.disconnected' | 'assistant.popout' | 'assistant.popin' | 'driver.created' | 'driver.wired' | 'driver.unwired' | 'state.changed' | 'turn.start' | 'turn.end' | 'turn.retry' | 'turn.error' | 'tool.failed' | 'tool.unresolved' | 'agent.handoff' | 'agent.pinned' | 'agent.unpinned' | 'provider.selected' | 'interaction.requested' | 'interaction.resolved' | 'context.updated' | 'context.threshold-crossed' | 'panel.toggled' | 'attachment.added' | 'file.read-failed' | 'suggestions.failed';
31
31
  /**
32
32
  * How much a reader should care about an event — lets a consumer (or an AI
33
33
  * agent) filter the timeline: skip `low` UI/bookkeeping noise, skim `normal`
@@ -74,7 +74,8 @@ export declare function recordMetaEvent(key: string, type: MetaEventType, detail
74
74
  * - `exception` — an uncaught error escaped the tool loop (catch-all).
75
75
  * - `malformed-function-call`— the provider returned an unparseable tool call.
76
76
  * - `empty-response` — the model returned no content and no tool calls.
77
- * - `unknown-tool-limit` — the model repeatedly called tools that don't exist.
77
+ * - `unknown-tool-limit` — the model repeatedly called tools it couldn't dispatch,
78
+ * whether hallucinated or stale (real earlier, retired now).
78
79
  * - `max-iterations` — the tool loop hit its iteration cap.
79
80
  */
80
81
  export type TurnFailureReason = 'exception' | 'malformed-function-call' | 'empty-response' | 'unknown-tool-limit' | 'max-iterations';
@@ -1 +1 @@
1
- {"version":3,"file":"debug-event-log.d.ts","sourceRoot":"","sources":["../../../src/state/debug-event-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GAErB,qBAAqB,GACrB,wBAAwB,GACxB,kBAAkB,GAClB,iBAAiB,GAEjB,gBAAgB,GAChB,cAAc,GACd,gBAAgB,GAEhB,eAAe,GACf,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,aAAa,GAEb,eAAe,GACf,cAAc,GACd,gBAAgB,GAChB,mBAAmB,GAEnB,uBAAuB,GACvB,sBAAsB,GAEtB,iBAAiB,GACjB,2BAA2B,GAE3B,eAAe,GACf,kBAAkB,GAClB,kBAAkB,GAClB,oBAAoB,CAAC;AAEzB;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE5D;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,aAAa,EAAE,mBAAmB,CA4B5E,CAAC;AAEF,4CAA4C;AAC5C,MAAM,WAAW,SAAS;IACxB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,IAAI,EAAE,aAAa,CAAC;IACpB,6EAA6E;IAC7E,UAAU,EAAE,mBAAmB,CAAC;IAChC,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAmBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,aAAa,EACnB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,IAAI,CAyBN;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,yBAAyB,GACzB,gBAAgB,GAChB,oBAAoB,GACpB,gBAAgB,CAAC;AAErB;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,IAAI,CAEN;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,IAAI,CAEN;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,CAEnE;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAW7C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
1
+ {"version":3,"file":"debug-event-log.d.ts","sourceRoot":"","sources":["../../../src/state/debug-event-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;GAGG;AACH,MAAM,MAAM,aAAa,GAErB,qBAAqB,GACrB,wBAAwB,GACxB,kBAAkB,GAClB,iBAAiB,GAEjB,gBAAgB,GAChB,cAAc,GACd,gBAAgB,GAEhB,eAAe,GACf,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,aAAa,GACb,iBAAiB,GAEjB,eAAe,GACf,cAAc,GACd,gBAAgB,GAChB,mBAAmB,GAEnB,uBAAuB,GACvB,sBAAsB,GAEtB,iBAAiB,GACjB,2BAA2B,GAE3B,eAAe,GACf,kBAAkB,GAClB,kBAAkB,GAClB,oBAAoB,CAAC;AAEzB;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAE5D;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,aAAa,EAAE,mBAAmB,CA6B5E,CAAC;AAEF,4CAA4C;AAC5C,MAAM,WAAW,SAAS;IACxB,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,IAAI,EAAE,aAAa,CAAC;IACpB,6EAA6E;IAC7E,UAAU,EAAE,mBAAmB,CAAC;IAChC,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAmBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,aAAa,EACnB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,IAAI,CAyBN;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,yBAAyB,GACzB,gBAAgB,GAChB,oBAAoB,GACpB,gBAAgB,CAAC;AAErB;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,IAAI,CAEN;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,iBAAiB,EACzB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,IAAI,CAEN;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,CAEnE;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAW7C,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Test-only side effect: align the global `EventTarget` with jsdom's before any
3
+ * module that `extends EventTarget` is evaluated.
4
+ *
5
+ * The node test runner's jsdom setup installs `globalThis.CustomEvent` from
6
+ * jsdom but leaves `globalThis.EventTarget` as Node's native class. A class that
7
+ * `extends EventTarget` (e.g. {@link ChatDriver}) then inherits Node's native
8
+ * `dispatchEvent`, which rejects the jsdom `CustomEvent` instances it is handed
9
+ * ("The 'event' argument must be an instance of Event. Received an instance of
10
+ * CustomEvent"). Pointing `EventTarget` at jsdom's keeps the whole event family
11
+ * in one realm.
12
+ *
13
+ * No-op in a real browser, where `window.EventTarget === globalThis.EventTarget`
14
+ * already. Import this BEFORE importing anything that subclasses `EventTarget`.
15
+ */
16
+ const jsdomWindow = globalThis.window;
17
+ if ((jsdomWindow === null || jsdomWindow === void 0 ? void 0 : jsdomWindow.EventTarget) && globalThis.EventTarget !== jsdomWindow.EventTarget) {
18
+ Object.defineProperty(globalThis, 'EventTarget', {
19
+ value: jsdomWindow.EventTarget,
20
+ configurable: true,
21
+ writable: true,
22
+ });
23
+ }
@@ -60,6 +60,21 @@ export class ChatDriver extends EventTarget {
60
60
  * hallucinated. Reset alongside `consecutiveUnknownToolCalls`.
61
61
  */
62
62
  this.recentUnknownToolNames = new Set();
63
+ /**
64
+ * Union of every tool name advertised at any point during the current agent
65
+ * activation. Lets the unknown-tool path tell a *stale* call (a real tool from
66
+ * an earlier state, now retired — or one an open exclusive fold is hiding)
67
+ * apart from a *hallucinated* one. Reset on agent swap in `applyAgent`.
68
+ */
69
+ this.everSeenToolNames = new Set();
70
+ /**
71
+ * Subset of the current unknown-tool streak that was stale (previously
72
+ * available) rather than hallucinated — surfaced separately on the
73
+ * `unknown-tool-limit` turn.error so triage can tell a state/prompt-design
74
+ * problem from a model that's inventing tools. Reset alongside
75
+ * `recentUnknownToolNames`.
76
+ */
77
+ this.recentStaleToolNames = new Set();
63
78
  /** Sub-agents declared on the active agent config, keyed by name. */
64
79
  this.subAgentsMap = new Map();
65
80
  /**
@@ -156,6 +171,10 @@ export class ChatDriver extends EventTarget {
156
171
  // Reset fold state when agent changes — each specialist starts fresh
157
172
  this.foldStack = [];
158
173
  this.consecutiveFoldOps = 0;
174
+ // Forget the previous agent's tools — "previously available" is scoped to
175
+ // the current activation, so a stateful agent accumulates its tools across
176
+ // states while a swap to a different specialist starts clean.
177
+ this.everSeenToolNames.clear();
159
178
  }
160
179
  /**
161
180
  * Returns the most recently resolved provider name. Falls back to the
@@ -714,6 +733,22 @@ export class ChatDriver extends EventTarget {
714
733
  }
715
734
  return null;
716
735
  }
736
+ /**
737
+ * If an open fold is hiding a previously-available tool, return the name of
738
+ * the fold to close to start getting it back. Only exclusive folds hide tools
739
+ * (they replace the tool set on open rather than extending it), so a base tool
740
+ * that was visible before the fold opened now sits in a fold-stack frame's
741
+ * `previousHandlers` but not in the live handler map. Only the top fold's
742
+ * `close_` tool is active, so that's always the actionable next step — even
743
+ * when the tool lives further down the stack, closing repeatedly walks back to
744
+ * it. Returns null when no open fold accounts for the tool.
745
+ */
746
+ foldHidingTool(toolName) {
747
+ if (this.foldStack.length === 0)
748
+ return null;
749
+ const hidden = this.foldStack.some((f) => f.previousHandlers[toolName]);
750
+ return hidden ? this.foldStack[this.foldStack.length - 1].foldName : null;
751
+ }
717
752
  /**
718
753
  * Install the fold's inner tool set, replacing (exclusive) or extending (non-exclusive)
719
754
  * the current tool set. Also injects the close tool. Does NOT touch the fold stack.
@@ -820,7 +855,7 @@ export class ChatDriver extends EventTarget {
820
855
  // oxlint-disable-next-line complexity
821
856
  runToolLoop(userInput, attachments, transientPrimer) {
822
857
  return __awaiter(this, void 0, void 0, function* () {
823
- var _a, _b, _c, _d, _e, _f, _g, _h;
858
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
824
859
  if (!this.systemPrompt) {
825
860
  logger.warn('ChatDriver: no systemPrompt set. The assistant will have no instructions — provide a systemPrompt via agents config or the foundation-ai-assistant property.');
826
861
  }
@@ -860,6 +895,12 @@ export class ChatDriver extends EventTarget {
860
895
  // oxlint-disable-next-line no-await-in-loop
861
896
  this.toolHandlers = yield this.toolHandlersFactory(promptCtx);
862
897
  }
898
+ // Record everything advertised this turn so the unknown-tool path can tell
899
+ // a stale tool (real earlier, retired now) from a hallucinated one. Runs
900
+ // for both the static and factory cases; folds also flow through here as
901
+ // their inner tools become visible on the iteration after they open.
902
+ for (const def of this.toolDefinitions)
903
+ this.everSeenToolNames.add(def.name);
863
904
  const resolvedSystemPrompt = typeof this.systemPrompt === 'function'
864
905
  ? // oxlint-disable-next-line no-await-in-loop
865
906
  yield this.systemPrompt(promptCtx)
@@ -991,6 +1032,9 @@ export class ChatDriver extends EventTarget {
991
1032
  }, [[], []]);
992
1033
  const executedById = new Map();
993
1034
  const unknownToolIds = new Set();
1035
+ // Subset of unknownToolIds that were stale (previously available) rather
1036
+ // than hallucinated — drives the `stale` UI flag back-patched below.
1037
+ const staleToolIds = new Set();
994
1038
  let anyRealToolExecuted = false;
995
1039
  let hitUnknownToolLimit = false;
996
1040
  if (toolCalls.length > 0) {
@@ -1038,22 +1082,73 @@ export class ChatDriver extends EventTarget {
1038
1082
  const containingFold = this.findFoldContaining(tc.name);
1039
1083
  if (containingFold) {
1040
1084
  logger.debug(`ChatDriver: model called folded tool "${tc.name}" — guiding to open "${containingFold}"`);
1085
+ recordMetaEvent(this.sessionKey, 'tool.unresolved', {
1086
+ tool: tc.name,
1087
+ agent: this.activeAgentName,
1088
+ kind: 'folded',
1089
+ fold: containingFold,
1090
+ });
1041
1091
  executedById.set(tc.id, {
1042
1092
  toolCallId: tc.id,
1043
1093
  content: `"${tc.name}" is not directly available. It is inside the "${containingFold}" fold. Call ${containingFold} first to access it.`,
1044
1094
  });
1045
1095
  // Guidance does not count as a real iteration or fold op
1046
1096
  iterations -= 1;
1097
+ return;
1047
1098
  }
1048
- else {
1099
+ // Not in any registered fold. If the tool was advertised earlier
1100
+ // in this agent's lifetime it's *stale* (a stateful agent moved on,
1101
+ // or an exclusive fold is hiding it) rather than hallucinated — a
1102
+ // distinction worth making, because the model should stop retrying
1103
+ // a retired tool rather than treat the failure as a typo. Stale
1104
+ // calls still count toward the same unknown-tool limit (loop
1105
+ // protection); only the guidance and telemetry differ.
1106
+ if (this.everSeenToolNames.has(tc.name)) {
1049
1107
  this.consecutiveUnknownToolCalls += 1;
1050
- logger.warn(`ChatDriver: no handler registered for tool "${tc.name}" (${this.consecutiveUnknownToolCalls}/${DEFAULT_MAX_UNKNOWN_TOOL_CALLS}). Available tools: ${Object.keys(this.toolHandlers).join(', ') || '(none)'}`);
1051
- executedById.set(tc.id, { toolCallId: tc.id, content: `Unknown tool: ${tc.name}` });
1108
+ const hidingFold = this.foldHidingTool(tc.name);
1109
+ let content;
1110
+ if (hidingFold) {
1111
+ 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}.`;
1112
+ logger.warn(`ChatDriver: tool "${tc.name}" is hidden behind open fold "${hidingFold}" (${this.consecutiveUnknownToolCalls}/${DEFAULT_MAX_UNKNOWN_TOOL_CALLS})`);
1113
+ }
1114
+ else {
1115
+ 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)'}.`;
1116
+ logger.warn(`ChatDriver: stale tool "${tc.name}" — advertised earlier this activation but retired in the current state (${this.consecutiveUnknownToolCalls}/${DEFAULT_MAX_UNKNOWN_TOOL_CALLS})`);
1117
+ }
1118
+ recordMetaEvent(this.sessionKey, 'tool.unresolved', {
1119
+ tool: tc.name,
1120
+ agent: this.activeAgentName,
1121
+ kind: hidingFold ? 'fold-hidden' : 'stale',
1122
+ fold: hidingFold !== null && hidingFold !== void 0 ? hidingFold : undefined,
1123
+ consecutive: this.consecutiveUnknownToolCalls,
1124
+ max: DEFAULT_MAX_UNKNOWN_TOOL_CALLS,
1125
+ });
1126
+ executedById.set(tc.id, { toolCallId: tc.id, content });
1052
1127
  unknownToolIds.add(tc.id);
1128
+ staleToolIds.add(tc.id);
1053
1129
  this.recentUnknownToolNames.add(tc.name);
1130
+ this.recentStaleToolNames.add(tc.name);
1054
1131
  if (this.consecutiveUnknownToolCalls >= DEFAULT_MAX_UNKNOWN_TOOL_CALLS) {
1055
1132
  hitUnknownToolLimit = true;
1056
1133
  }
1134
+ return;
1135
+ }
1136
+ // Never advertised — a hallucinated tool name.
1137
+ this.consecutiveUnknownToolCalls += 1;
1138
+ logger.warn(`ChatDriver: no handler registered for tool "${tc.name}" (${this.consecutiveUnknownToolCalls}/${DEFAULT_MAX_UNKNOWN_TOOL_CALLS}). Available tools: ${Object.keys(this.toolHandlers).join(', ') || '(none)'}`);
1139
+ recordMetaEvent(this.sessionKey, 'tool.unresolved', {
1140
+ tool: tc.name,
1141
+ agent: this.activeAgentName,
1142
+ kind: 'unknown',
1143
+ consecutive: this.consecutiveUnknownToolCalls,
1144
+ max: DEFAULT_MAX_UNKNOWN_TOOL_CALLS,
1145
+ availableTools: Object.keys(this.toolHandlers),
1146
+ });
1147
+ executedById.set(tc.id, { toolCallId: tc.id, content: `Unknown tool: ${tc.name}` });
1148
+ unknownToolIds.add(tc.id);
1149
+ this.recentUnknownToolNames.add(tc.name);
1150
+ if (this.consecutiveUnknownToolCalls >= DEFAULT_MAX_UNKNOWN_TOOL_CALLS) {
1151
+ hitUnknownToolLimit = true;
1057
1152
  }
1058
1153
  return;
1059
1154
  }
@@ -1089,6 +1184,7 @@ export class ChatDriver extends EventTarget {
1089
1184
  this.consecutiveFoldOps = 0;
1090
1185
  this.consecutiveUnknownToolCalls = 0;
1091
1186
  this.recentUnknownToolNames.clear();
1187
+ this.recentStaleToolNames.clear();
1092
1188
  }
1093
1189
  // Tag tool calls with fold UI metadata before appending results
1094
1190
  const foldPath = this.foldStack.map((f) => f.foldName);
@@ -1134,7 +1230,10 @@ export class ChatDriver extends EventTarget {
1134
1230
  ? 'close'
1135
1231
  : undefined,
1136
1232
  // Use the fold path that was active at the START of this iteration (before any opens/closes)
1137
- foldPath: !isFoldOpen && !isFoldClose && foldPath.length > 0 ? foldPath : undefined, unknown: isUnknown || undefined, availableTools: isUnknown ? availableToolNames : undefined, subAgentTrace: (_e = executedById.get(tc.id)) === null || _e === void 0 ? void 0 : _e.subAgentTrace });
1233
+ foldPath: !isFoldOpen && !isFoldClose && foldPath.length > 0 ? foldPath : undefined, unknown: isUnknown || undefined, availableTools: isUnknown ? availableToolNames : undefined,
1234
+ // Distinguish a retired tool from a hallucinated one so the UI can
1235
+ // say "no longer available here" rather than "does not exist".
1236
+ stale: staleToolIds.has(tc.id) || undefined, subAgentTrace: (_e = executedById.get(tc.id)) === null || _e === void 0 ? void 0 : _e.subAgentTrace });
1138
1237
  });
1139
1238
  this.history[tcMsgIdx] = Object.assign(Object.assign({}, tcMsg), { toolCalls: annotatedCalls });
1140
1239
  this.dispatchEvent(new CustomEvent('history-updated', {
@@ -1151,10 +1250,25 @@ export class ChatDriver extends EventTarget {
1151
1250
  .map((tc) => tc.name),
1152
1251
  ]),
1153
1252
  ];
1253
+ // Stale tools were real earlier this activation; hallucinated tools
1254
+ // never existed. The hard stop counts both the same way, but the split
1255
+ // tells a triager whether the cause is a state/prompt-design problem
1256
+ // (stale) or a model inventing tool names (hallucinated).
1257
+ const staleTools = [
1258
+ ...new Set([
1259
+ ...this.recentStaleToolNames,
1260
+ ...((_j = response.toolCalls) !== null && _j !== void 0 ? _j : [])
1261
+ .filter((tc) => staleToolIds.has(tc.id))
1262
+ .map((tc) => tc.name),
1263
+ ]),
1264
+ ];
1265
+ const hallucinatedTools = unknownTools.filter((t) => !staleTools.includes(t));
1154
1266
  recordTurnError(this.sessionKey, 'unknown-tool-limit', {
1155
1267
  agent: this.activeAgentName,
1156
1268
  provider: this.lastResolvedProviderName,
1157
1269
  unknownTools,
1270
+ staleTools,
1271
+ hallucinatedTools,
1158
1272
  availableTools: Object.keys(this.toolHandlers),
1159
1273
  });
1160
1274
  this.appendToHistory({
@@ -0,0 +1,196 @@
1
+ import { __awaiter } from "tslib";
2
+ import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
3
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
4
+ import { agenticActivityBus } from '../../channel/ai-activity-bus';
5
+ import { clearMetaEventRegistry, getMetaEvents } from '../../state/debug-event-log';
6
+ import { createToolFold } from '../../utils/tool-fold';
7
+ // Side-effect import — MUST come before `./chat-driver` so the driver subclasses
8
+ // jsdom's EventTarget rather than Node's native one (see the file). None of the
9
+ // imports above pull in the driver, so its realm is still set before evaluation.
10
+ import './align-event-globals';
11
+ import { ChatDriver } from './chat-driver';
12
+ const scriptedProvider = (responses) => {
13
+ const queue = [...responses];
14
+ const advertisedPerCall = [];
15
+ return {
16
+ advertisedPerCall,
17
+ chat: (_history, _userMessage, options) => __awaiter(void 0, void 0, void 0, function* () {
18
+ var _a, _b;
19
+ advertisedPerCall.push(((_a = options === null || options === void 0 ? void 0 : options.tools) !== null && _a !== void 0 ? _a : []).map((t) => t.name));
20
+ // Once the script is exhausted, end the turn with a plain text reply.
21
+ return (_b = queue.shift()) !== null && _b !== void 0 ? _b : { role: 'assistant', content: 'done' };
22
+ }),
23
+ };
24
+ };
25
+ const makeRegistry = (provider) => ({
26
+ get: () => provider,
27
+ default: () => provider,
28
+ defaultName: () => 'test',
29
+ names: () => ['test'],
30
+ getStatus: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
31
+ listStatuses: () => __awaiter(void 0, void 0, void 0, function* () { return []; }),
32
+ });
33
+ const def = (name) => ({
34
+ name,
35
+ description: `${name} tool`,
36
+ parameters: { type: 'object', properties: {} },
37
+ });
38
+ /** An assistant turn that calls a single tool. `content` is empty so the driver
39
+ * does not treat it as a thinking step (which would split it into two messages). */
40
+ const callsTool = (name, id) => ({
41
+ role: 'assistant',
42
+ content: '',
43
+ toolCalls: [{ id, name, args: {} }],
44
+ });
45
+ const agent = (overrides) => (Object.assign({ description: 'test agent' }, overrides));
46
+ const makeDriver = (config, provider, sessionKey = '') => {
47
+ const driver = new ChatDriver(makeRegistry(provider), {}, [], undefined, undefined, 50, 5, undefined, sessionKey);
48
+ driver.applyAgent(config);
49
+ return driver;
50
+ };
51
+ /** All tool calls across the whole conversation, flattened. */
52
+ const allToolCalls = (driver) => driver.getHistory().flatMap((m) => { var _a; return (_a = m.toolCalls) !== null && _a !== void 0 ? _a : []; });
53
+ /** Tool-result message contents, in order. */
54
+ const toolResultContents = (driver) => driver
55
+ .getHistory()
56
+ .filter((m) => m.role === 'tool' && m.toolResult)
57
+ .map((m) => m.toolResult.content);
58
+ /** `tool.unresolved` meta-event details recorded for a session (download-log surface). */
59
+ const unresolvedEvents = (sessionKey) => getMetaEvents(sessionKey)
60
+ .filter((e) => e.type === 'tool.unresolved')
61
+ .map((e) => { var _a; return (_a = e.detail) !== null && _a !== void 0 ? _a : {}; });
62
+ // ---------------------------------------------------------------------------
63
+ // stale tool detection — stateful agent advances past a tool's state
64
+ // ---------------------------------------------------------------------------
65
+ const stale = createLogicSuite('ChatDriver stale-tool detection');
66
+ // The driver imports the `agenticActivityBus` singleton, which opens a
67
+ // BroadcastChannel at module load. An open channel keeps the test page alive
68
+ // and hangs the runner, so close it once the suite finishes.
69
+ stale.after(() => {
70
+ agenticActivityBus.close();
71
+ });
72
+ stale('guides the model when it calls a tool that an earlier state exposed', () => __awaiter(void 0, void 0, void 0, function* () {
73
+ // State A exposes tool_a; calling it advances to state B, which exposes only
74
+ // tool_b. A factory-form agent narrows the tool set per turn, mirroring how
75
+ // `defineStatefulAgent` works.
76
+ let state = 'A';
77
+ const config = agent({
78
+ name: 'Stateful',
79
+ toolDefinitions: () => (state === 'A' ? [def('tool_a')] : [def('tool_b')]),
80
+ toolHandlers: () => state === 'A'
81
+ ? {
82
+ tool_a: () => __awaiter(void 0, void 0, void 0, function* () {
83
+ state = 'B';
84
+ return 'advanced to B';
85
+ }),
86
+ }
87
+ : { tool_b: () => __awaiter(void 0, void 0, void 0, function* () { return 'b done'; }) },
88
+ });
89
+ const provider = scriptedProvider([
90
+ callsTool('tool_a', 't1'), // real — advances A -> B
91
+ callsTool('tool_a', 't2'), // stale — tool_a no longer in state B
92
+ callsTool('tool_b', 't3'), // real — valid in state B
93
+ ]);
94
+ const sessionKey = 'stale-meta-test';
95
+ const driver = makeDriver(config, provider, sessionKey);
96
+ const result = yield driver.sendMessage('go');
97
+ assert.is(result.reason, 'done');
98
+ // The per-state narrowing actually happened: tool_a advertised first, tool_b later.
99
+ assert.equal(provider.advertisedPerCall[0], ['tool_a']);
100
+ assert.ok(provider.advertisedPerCall.some((tools) => tools.includes('tool_b') && !tools.includes('tool_a')), 'a later turn should advertise tool_b without tool_a');
101
+ // The retried tool_a got stale guidance — not "Unknown tool".
102
+ const staleGuidance = toolResultContents(driver).find((c) => c.includes('was available earlier but is not part of the current step'));
103
+ assert.ok(staleGuidance, 'a previously-available tool should receive stale guidance');
104
+ assert.not.ok(toolResultContents(driver).some((c) => c.startsWith('Unknown tool:')), 'a previously-available tool must not be reported as a hallucination');
105
+ // The retried call is flagged unknown + stale for the UI.
106
+ const retried = allToolCalls(driver).filter((tc) => tc.name === 'tool_a' && isChatToolCallUnknown(tc));
107
+ assert.is(retried.length, 1, 'exactly one tool_a call should be flagged unknown');
108
+ assert.ok(isChatToolCallUnknown(retried[0]) && retried[0].stale === true, 'and marked stale');
109
+ // The occurrence is recorded to the meta-event log for the download log.
110
+ assert.ok(unresolvedEvents(sessionKey).some((d) => d.kind === 'stale' && d.tool === 'tool_a'), 'a stale tool.unresolved meta event should be recorded');
111
+ }));
112
+ stale('reports a never-seen tool as a hallucinated unknown tool', () => __awaiter(void 0, void 0, void 0, function* () {
113
+ const config = agent({
114
+ name: 'Static',
115
+ toolDefinitions: [def('real_tool')],
116
+ toolHandlers: { real_tool: () => __awaiter(void 0, void 0, void 0, function* () { return 'ok'; }) },
117
+ });
118
+ const provider = scriptedProvider([callsTool('made_up', 'm1')]);
119
+ const sessionKey = 'hallucination-meta-test';
120
+ const driver = makeDriver(config, provider, sessionKey);
121
+ yield driver.sendMessage('go');
122
+ assert.ok(toolResultContents(driver).includes('Unknown tool: made_up'), 'a tool never advertised should be reported as unknown');
123
+ const call = allToolCalls(driver).find((tc) => tc.name === 'made_up');
124
+ assert.ok(call && isChatToolCallUnknown(call), 'the call should be flagged unknown');
125
+ assert.not.ok(call.stale, 'a hallucinated tool must NOT be flagged stale');
126
+ assert.ok(unresolvedEvents(sessionKey).some((d) => d.kind === 'unknown' && d.tool === 'made_up'), 'an unknown tool.unresolved meta event should be recorded');
127
+ }));
128
+ stale('points the model at the close tool when an exclusive fold hides a base tool', () => __awaiter(void 0, void 0, void 0, function* () {
129
+ var _a, _b;
130
+ const fold = createToolFold({
131
+ name: 'my_fold',
132
+ tools: [def('inner_tool')],
133
+ handlers: { inner_tool: () => __awaiter(void 0, void 0, void 0, function* () { return 'inner done'; }) },
134
+ // exclusive defaults to true — opening it removes base_tool from the set.
135
+ });
136
+ const config = agent({
137
+ name: 'Folded',
138
+ toolDefinitions: [def('base_tool'), fold.definition],
139
+ toolHandlers: Object.assign({ base_tool: () => __awaiter(void 0, void 0, void 0, function* () { return 'base done'; }) }, fold.handler),
140
+ });
141
+ const provider = scriptedProvider([
142
+ callsTool('my_fold', 'f1'), // open the exclusive fold — base_tool now hidden
143
+ callsTool('base_tool', 'b1'), // hidden behind the open fold
144
+ ]);
145
+ const sessionKey = 'fold-meta-test';
146
+ const driver = makeDriver(config, provider, sessionKey);
147
+ yield driver.sendMessage('go');
148
+ // Target the base_tool result specifically — the fold-open result also
149
+ // mentions my_fold, so match on the tool call id rather than substring.
150
+ const guidance = (_b = (_a = driver
151
+ .getHistory()
152
+ .find((m) => { var _a; return m.role === 'tool' && ((_a = m.toolResult) === null || _a === void 0 ? void 0 : _a.toolCallId) === 'b1'; })) === null || _a === void 0 ? void 0 : _a.toolResult) === null || _b === void 0 ? void 0 : _b.content;
153
+ assert.ok(guidance, 'calling a fold-hidden tool should produce guidance');
154
+ assert.match(guidance, /not available while the "my_fold" fold is open/);
155
+ assert.match(guidance, /close_my_fold/);
156
+ const hidden = allToolCalls(driver).find((tc) => tc.name === 'base_tool' && isChatToolCallUnknown(tc));
157
+ assert.ok(hidden && isChatToolCallUnknown(hidden) && hidden.stale === true, 'the hidden call is stale');
158
+ assert.ok(unresolvedEvents(sessionKey).some((d) => d.kind === 'fold-hidden' && d.tool === 'base_tool' && d.fold === 'my_fold'), 'a fold-hidden tool.unresolved meta event should be recorded');
159
+ }));
160
+ stale('splits stale vs hallucinated tools on the unknown-tool-limit error', () => __awaiter(void 0, void 0, void 0, function* () {
161
+ const sessionKey = 'stale-limit-test';
162
+ clearMetaEventRegistry();
163
+ let state = 'A';
164
+ const config = agent({
165
+ name: 'Stateful',
166
+ toolDefinitions: () => (state === 'A' ? [def('tool_a')] : [def('tool_b')]),
167
+ toolHandlers: () => state === 'A'
168
+ ? {
169
+ tool_a: () => __awaiter(void 0, void 0, void 0, function* () {
170
+ state = 'B';
171
+ return 'advanced to B';
172
+ }),
173
+ }
174
+ : { tool_b: () => __awaiter(void 0, void 0, void 0, function* () { return 'b done'; }) },
175
+ });
176
+ // One real call to advance to B, then 5 consecutive stale calls — the 5th
177
+ // trips DEFAULT_MAX_UNKNOWN_TOOL_CALLS and ends the turn.
178
+ const provider = scriptedProvider([
179
+ callsTool('tool_a', 'real'),
180
+ ...Array.from({ length: 5 }, (_unused, i) => callsTool('tool_a', `stale-${i}`)),
181
+ ]);
182
+ const driver = makeDriver(config, provider, sessionKey);
183
+ const result = yield driver.sendMessage('go');
184
+ assert.is(result.reason, 'done');
185
+ const limitError = getMetaEvents(sessionKey).find((e) => { var _a; return e.type === 'turn.error' && ((_a = e.detail) === null || _a === void 0 ? void 0 : _a.reason) === 'unknown-tool-limit'; });
186
+ assert.ok(limitError, 'hitting the limit should record an unknown-tool-limit turn.error');
187
+ const detail = limitError.detail;
188
+ assert.equal(detail.staleTools, ['tool_a'], 'tool_a should be classified as stale');
189
+ assert.equal(detail.hallucinatedTools, [], 'nothing was hallucinated');
190
+ // Every stale attempt — not just the final limit error — is in the download log.
191
+ assert.is(unresolvedEvents(sessionKey).filter((d) => d.kind === 'stale').length, 5, 'each stale attempt should be recorded as its own tool.unresolved event');
192
+ // The user-facing turn ends with the apology, not a crash.
193
+ const last = driver.getHistory().at(-1);
194
+ assert.ok((last === null || last === void 0 ? void 0 : last.role) === 'assistant' && last.content.startsWith("I'm sorry"));
195
+ }));
196
+ stale.run();
@@ -20,7 +20,11 @@ import { ANIMATION_DEFS } from './main.types';
20
20
  function unknownToolPayload(tc) {
21
21
  if (!isChatToolCallUnknown(tc))
22
22
  return '';
23
- const lines = [`${tc.name} tool does not exist`];
23
+ // A stale tool was real earlier in this agent's lifetime but isn't available
24
+ // in the current state — distinct from a hallucinated name that never existed.
25
+ const lines = [
26
+ tc.stale ? `${tc.name} — no longer available at this step` : `${tc.name} — tool does not exist`,
27
+ ];
24
28
  if (tc.availableTools.length > 0) {
25
29
  lines.push('Available tools:');
26
30
  tc.availableTools.forEach((t) => lines.push(` • ${t}`));