@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.
- package/dist/ai-assistant.d.ts +27 -1
- package/dist/dts/components/chat-driver/align-event-globals.d.ts +19 -0
- package/dist/dts/components/chat-driver/align-event-globals.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts +26 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts +2 -0
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -0
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/state/debug-event-log.d.ts +3 -2
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/align-event-globals.js +23 -0
- package/dist/esm/components/chat-driver/chat-driver.js +119 -5
- package/dist/esm/components/chat-driver/chat-driver.test.js +196 -0
- package/dist/esm/main/main.template.js +5 -1
- package/dist/esm/state/debug-event-log.js +3 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/components/chat-driver/align-event-globals.ts +23 -0
- package/src/components/chat-driver/chat-driver.test.ts +315 -0
- package/src/components/chat-driver/chat-driver.ts +125 -5
- package/src/main/main.template.ts +5 -1
- package/src/state/debug-event-log.ts +6 -3
package/dist/ai-assistant.d.ts
CHANGED
|
@@ -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;
|
|
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 @@
|
|
|
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;
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1051
|
-
|
|
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,
|
|
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
|
-
|
|
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}`));
|