@genesislcap/ai-assistant 15.24.1 → 15.25.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 +1065 -33
- package/dist/ai-assistant.d.ts +288 -102
- package/dist/chat-driver.cjs +630 -378
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +630 -378
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +1198 -388
- package/dist/dts/components/agent-session/agent-session.d.ts +134 -0
- package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -0
- package/dist/dts/components/agent-session/index.d.ts +2 -0
- package/dist/dts/components/agent-session/index.d.ts.map +1 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts +31 -4
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.constants.d.ts +52 -0
- package/dist/dts/components/chat-driver/chat-driver.constants.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts +100 -82
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/fold-controller.d.ts +82 -0
- package/dist/dts/components/chat-driver/fold-controller.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/turn-failures.d.ts +20 -1
- package/dist/dts/components/chat-driver/turn-failures.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -24
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +6 -16
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/suggestions/generate-suggestions.d.ts +35 -0
- package/dist/dts/suggestions/generate-suggestions.d.ts.map +1 -0
- package/dist/dts/utils/agents-key.d.ts +10 -0
- package/dist/dts/utils/agents-key.d.ts.map +1 -0
- package/dist/dts/utils/resolve-turn-input.d.ts +9 -0
- package/dist/dts/utils/resolve-turn-input.d.ts.map +1 -0
- package/dist/esm/components/agent-session/agent-session.js +208 -0
- package/dist/esm/components/agent-session/index.js +1 -0
- package/dist/esm/components/chat-driver/chat-driver.constants.js +72 -0
- package/dist/esm/components/chat-driver/chat-driver.js +190 -395
- package/dist/esm/components/chat-driver/fold-controller.js +184 -0
- package/dist/esm/components/chat-driver/turn-failures.js +14 -0
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +48 -131
- package/dist/esm/index.js +1 -0
- package/dist/esm/main/main.js +11 -38
- package/dist/esm/suggestions/generate-suggestions.js +123 -0
- package/dist/esm/utils/agents-key.js +24 -0
- package/dist/esm/utils/resolve-turn-input.js +16 -0
- package/docs/migration-GENC-1580.md +44 -36
- package/package.json +16 -16
|
@@ -4,13 +4,19 @@ import { NOOP_ACTIVITY_BUS } from '../../channel/ai-activity-bus';
|
|
|
4
4
|
import { resolveChatProvider } from '../../config/validate-providers';
|
|
5
5
|
import { clearSession, getMetaEvents, mergeMetaEvents, recordMetaEvent, recordTurnError, recordTurnRetry, } from '../../state/debug-event-log';
|
|
6
6
|
import { createInteractionContext, } from '../../state/interaction-context';
|
|
7
|
+
import { generateSuggestions } from '../../suggestions/generate-suggestions';
|
|
7
8
|
import { applyCondensation, condenseForPersistence, } from '../../utils/condense-history';
|
|
8
9
|
import { estimatePreambleTokens, estimateRequestTokens, } from '../../utils/context-tokens';
|
|
9
10
|
import { applyHistoryCap, buildCompactionSummaryPrompt, planCompaction, normalizeForProvider, } from '../../utils/history-transform';
|
|
10
11
|
import { logger } from '../../utils/logger';
|
|
12
|
+
import { resolveTurnInput } from '../../utils/resolve-turn-input';
|
|
11
13
|
import { messageUsage, sumUsage } from '../../utils/sum-usage';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
+
import { AgentSession } from '../agent-session/agent-session';
|
|
15
|
+
import { DEFAULT_MAX_FOLD_OPERATIONS, DEFAULT_MAX_TOOL_ITERATIONS, DEFAULT_MAX_TURN_SNAPSHOTS, DEFAULT_MAX_UNKNOWN_TOOL_CALLS, EMPTY_RESPONSE_NUDGE, HANDOFF_TOOL_RESULT_PLACEHOLDER, HARVESTED_SUBAGENT_EVENTS, MALFORMED_CALL_NUDGE, MAX_EMPTY_RESPONSE_RETRIES, MAX_MALFORMED_RETRIES, MAX_SETUP_TRANSPORT_RETRIES, MAX_STALE_TOOL_CALLS, REQUEST_CONTINUATION_TOOL, } from './chat-driver.constants';
|
|
16
|
+
import { FoldController } from './fold-controller';
|
|
17
|
+
import { TURN_FAILURE_EXITS, turnDone } from './turn-failures';
|
|
18
|
+
/** Re-exported so `from './chat-driver'` keeps resolving these. */
|
|
19
|
+
export { EMPTY_RESPONSE_NUDGE, MALFORMED_CALL_NUDGE, REQUEST_CONTINUATION_TOOL, } from './chat-driver.constants';
|
|
14
20
|
/**
|
|
15
21
|
* Lift the reportable facts off a {@link BudgetExhaustedError}, or `undefined`
|
|
16
22
|
* when it carries none.
|
|
@@ -71,74 +77,6 @@ const budgetDetailOf = (e) => {
|
|
|
71
77
|
return undefined;
|
|
72
78
|
return Object.assign(Object.assign({ budgetUsd: e.budgetUsd, spentUsd: e.spentUsd, vendorLabel: e.vendorLabel }, (vendor ? { vendor } : {})), (e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}));
|
|
73
79
|
};
|
|
74
|
-
const DEFAULT_MAX_TOOL_ITERATIONS = 50;
|
|
75
|
-
const DEFAULT_MAX_FOLD_OPERATIONS = 5;
|
|
76
|
-
// TODO: dedup system prompts in-memory to allow raising this cap much higher.
|
|
77
|
-
// Each TurnSnapshot stores the full resolved `systemPrompt`, so the buffer's
|
|
78
|
-
// memory cost scales with cap × prompt size — a stable agent repeats a multi-KB
|
|
79
|
-
// prompt every turn. `getDebugLog` already dedups identical consecutive prompts
|
|
80
|
-
// at export time (`<repeated — identical to turn N>`); applying the same dedup
|
|
81
|
-
// at capture time (store the prompt only when it changes, back-reference
|
|
82
|
-
// otherwise) would make snapshots nearly as cheap as meta events and let this
|
|
83
|
-
// cap reach thousands for full-session capture without the memory blowup.
|
|
84
|
-
const DEFAULT_MAX_TURN_SNAPSHOTS = 400;
|
|
85
|
-
const DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
|
|
86
|
-
// Stale tools (advertised in an earlier state, retired now) and fold-hidden tools are
|
|
87
|
-
// self-correcting — the model drops them once guided — so they get a higher loop-protection
|
|
88
|
-
// ceiling than hallucinated names: a few legitimate stale calls across state transitions must
|
|
89
|
-
// not prematurely end the turn. Still bounded so a genuinely stuck loop terminates.
|
|
90
|
-
const MAX_STALE_TOOL_CALLS = DEFAULT_MAX_UNKNOWN_TOOL_CALLS * 2;
|
|
91
|
-
// Gemini in particular emits short bursts of MALFORMED_FUNCTION_CALL; allow more CONSECUTIVE
|
|
92
|
-
// retries. These counters reset on any productive response, so this is a consecutive-failure
|
|
93
|
-
// ceiling, not a per-turn total.
|
|
94
|
-
const MAX_MALFORMED_RETRIES = 5;
|
|
95
|
-
const MAX_EMPTY_RESPONSE_RETRIES = 3;
|
|
96
|
-
/**
|
|
97
|
-
* Framed-tail nudge added to a retry after a MALFORMED tool call.
|
|
98
|
-
*
|
|
99
|
-
* Exported so tests can assert placement (framed tail, never the system prompt) without pinning
|
|
100
|
-
* the wording — the copy is a prompt, and a prompt should be free to improve.
|
|
101
|
-
*
|
|
102
|
-
* @beta
|
|
103
|
-
*/
|
|
104
|
-
export const MALFORMED_CALL_NUDGE = 'IMPORTANT: Use only the structured function-call API to invoke tools. Do not write Python code or use Python-style syntax to call tools.';
|
|
105
|
-
/**
|
|
106
|
-
* Framed-tail nudge added to a retry after an EMPTY response.
|
|
107
|
-
*
|
|
108
|
-
* Rewritten in GENC-1573. The previous copy ("You must respond to the user's message. Call the
|
|
109
|
-
* appropriate tool or provide a text response") told a model that had just returned nothing to
|
|
110
|
-
* respond, without naming the one exit available to an agent that genuinely cannot act: saying so.
|
|
111
|
-
* Across nine failures in the measured session it changed nothing — all 29 attempts came back at
|
|
112
|
-
* 8–9 output tokens — and most of those were on a tool-less agent, for which "call the appropriate
|
|
113
|
-
* tool" is not an option at all. This version names the honest answer explicitly.
|
|
114
|
-
*
|
|
115
|
-
* @beta
|
|
116
|
-
*/
|
|
117
|
-
export const EMPTY_RESPONSE_NUDGE = "IMPORTANT: Your previous attempt returned nothing at all, which is never a valid response. Answer the user's last message now, in prose — even if all you can honestly say is that you cannot help with it and why. If a tool is the right way to answer, call it. Do not return an empty response.";
|
|
118
|
-
// Transient throws while building the per-turn tool surface or calling the provider retry the
|
|
119
|
-
// SAME iteration up to this many times before propagating, rather than tearing down the turn.
|
|
120
|
-
const MAX_SETUP_TRANSPORT_RETRIES = 3;
|
|
121
|
-
const SUGGESTIONS_HISTORY_WINDOW = 8;
|
|
122
|
-
/**
|
|
123
|
-
* Sub-agent meta events worth folding into the parent's debug timeline: the
|
|
124
|
-
* per-attempt and per-failure signals that do NOT otherwise surface in the
|
|
125
|
-
* sub-agent's (now hoisted) messages — a retried-away malformed/empty attempt
|
|
126
|
-
* produces no message, and the stale-vs-hallucinated/streak diagnostics live only
|
|
127
|
-
* on the event. High-volume, message-derivable events (turn.start/turn.end,
|
|
128
|
-
* provider.selected, context.updated) are intentionally excluded: read the
|
|
129
|
-
* sub-agent's hoisted messages for model/tokens/cost and turn-by-turn activity.
|
|
130
|
-
* See `ChatDriver.invokeSubAgent`.
|
|
131
|
-
*/
|
|
132
|
-
const HARVESTED_SUBAGENT_EVENTS = new Set([
|
|
133
|
-
'turn.retry',
|
|
134
|
-
'turn.error',
|
|
135
|
-
'tool.failed',
|
|
136
|
-
'tool.unresolved',
|
|
137
|
-
]);
|
|
138
|
-
/** Name reserved for the cross-agent handoff tool — injected by OrchestratingDriver. */
|
|
139
|
-
export const REQUEST_CONTINUATION_TOOL = 'request_continuation';
|
|
140
|
-
/** Paired in history for each `request_continuation` so tool_calls stay balanced for the provider. */
|
|
141
|
-
const HANDOFF_TOOL_RESULT_PLACEHOLDER = 'Handoff to another specialist — routing continues on the next turn.';
|
|
142
80
|
export class ChatDriver extends EventTarget {
|
|
143
81
|
constructor(providerRegistry, config = {}) {
|
|
144
82
|
super();
|
|
@@ -201,10 +139,18 @@ export class ChatDriver extends EventTarget {
|
|
|
201
139
|
* (`callPhaseEpoch < currentPhaseEpoch`), with an `agentEnd` backstop.
|
|
202
140
|
*/
|
|
203
141
|
this.currentPhaseEpoch = 0;
|
|
204
|
-
/**
|
|
205
|
-
this.
|
|
206
|
-
|
|
207
|
-
|
|
142
|
+
/** Tool folds: the open-fold stack, the no-progress counter, and the tool-surface swap. */
|
|
143
|
+
this.folds = new FoldController({
|
|
144
|
+
getDefinitions: () => this.toolDefinitions,
|
|
145
|
+
setDefinitions: (defs) => {
|
|
146
|
+
this.toolDefinitions = defs;
|
|
147
|
+
},
|
|
148
|
+
getHandlers: () => this.toolHandlers,
|
|
149
|
+
setHandlers: (handlers) => {
|
|
150
|
+
this.toolHandlers = handlers;
|
|
151
|
+
},
|
|
152
|
+
handlerContext: (activeToolCallId) => this.buildHandlerContext(activeToolCallId),
|
|
153
|
+
});
|
|
208
154
|
/** Consecutive unknown-tool calls without a real tool call. Reset on real tool execution. */
|
|
209
155
|
this.consecutiveUnknownToolCalls = 0;
|
|
210
156
|
/**
|
|
@@ -304,6 +250,19 @@ export class ChatDriver extends EventTarget {
|
|
|
304
250
|
* picked up on the next turn.
|
|
305
251
|
*/
|
|
306
252
|
this.resolvedStatusCache = new Map();
|
|
253
|
+
/**
|
|
254
|
+
* The stateful-agent contract for this driver: active agent, user pin, flow lock, restored
|
|
255
|
+
* state, and the `onActivate` / `onDeactivate` hooks (§7.1).
|
|
256
|
+
*
|
|
257
|
+
* **Public because `OrchestratingDriver` reaches through to it.** The orchestrator decorates a
|
|
258
|
+
* config before installing it (the cross-agent continuation tool), so it cannot route through
|
|
259
|
+
* {@link ChatDriver.activateAgent} and must drive the session itself — and one shared session is
|
|
260
|
+
* the point: two would let `getActiveAgent()` disagree between the outer and inner driver.
|
|
261
|
+
* `@internal` rather than private so that reach is legal without six delegating one-liners.
|
|
262
|
+
*
|
|
263
|
+
* @internal
|
|
264
|
+
*/
|
|
265
|
+
this.agentSession = new AgentSession({ sessionKey: () => this.sessionKey });
|
|
307
266
|
/**
|
|
308
267
|
* A provider refusal was observed this turn, so the tool loop must not call the model again
|
|
309
268
|
* (GENC-1506).
|
|
@@ -330,10 +289,11 @@ export class ChatDriver extends EventTarget {
|
|
|
330
289
|
* Reset per turn alongside `budgetWallDetail`.
|
|
331
290
|
*/
|
|
332
291
|
this.budgetWallViaSubAgent = false;
|
|
333
|
-
const { toolHandlers = {}, toolDefinitions = [], systemPrompt, primerHistory, maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS, condenseBatchCalls = 1, maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS, sessionKey = '', activityBus = NOOP_ACTIVITY_BUS, budgetExhaustedMessage, providerRefusedMessage, } = config;
|
|
292
|
+
const { toolHandlers = {}, toolDefinitions = [], systemPrompt, primerHistory, maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS, condenseBatchCalls = 1, maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS, sessionKey = '', deferAgentRelease = false, activityBus = NOOP_ACTIVITY_BUS, budgetExhaustedMessage, providerRefusedMessage, } = config;
|
|
334
293
|
this.maxToolIterations = maxToolIterations;
|
|
335
294
|
this.condenseBatchCalls = condenseBatchCalls;
|
|
336
295
|
this.sessionKey = sessionKey;
|
|
296
|
+
this.deferAgentRelease = deferAgentRelease;
|
|
337
297
|
this.activityBus = activityBus;
|
|
338
298
|
this.budgetExhaustedMessageOverride = budgetExhaustedMessage;
|
|
339
299
|
this.providerRefusedMessageOverride = providerRefusedMessage;
|
|
@@ -393,20 +353,32 @@ export class ChatDriver extends EventTarget {
|
|
|
393
353
|
* timeout), which also stamps history and reverts the chat-input override.
|
|
394
354
|
*/
|
|
395
355
|
dispose() {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
356
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
357
|
+
var _a;
|
|
358
|
+
// Everything up to the `await` below runs SYNCHRONOUSLY on the call, and that is the
|
|
359
|
+
// contract: callers rely on `dispose()` having detached listeners by the time it returns —
|
|
360
|
+
// `chat-driver.test.ts`'s sub-agent listener-leak case asserts it directly, and the host
|
|
361
|
+
// fires this and forgets (`main.ts`, `void old.dispose()`). Awaiting anything first opened a
|
|
362
|
+
// window in which a disposed driver was still subscribed to the provider registry.
|
|
363
|
+
this.lifecycleController.abort(new DOMException('AI assistant driver disposed', 'AbortError'));
|
|
364
|
+
// Drop any live interaction contexts (GENC-1390). Normally empty at teardown
|
|
365
|
+
// (a context is created and destroyed with its pending interaction, and dispose
|
|
366
|
+
// is not reached mid-interaction); disposed defensively so a stray context can
|
|
367
|
+
// never leak an open socket/timer.
|
|
368
|
+
for (const handle of this.interactionContexts.values())
|
|
369
|
+
handle.dispose();
|
|
370
|
+
this.interactionContexts.clear();
|
|
371
|
+
// Detach from the provider registry so the long-lived registry doesn't pin
|
|
372
|
+
// this driver (and its closure) after teardown. Guard-cleared so a second
|
|
373
|
+
// dispose is a no-op, matching this method's idempotent contract.
|
|
374
|
+
(_a = this.unsubscribeRegistry) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
375
|
+
this.unsubscribeRegistry = undefined;
|
|
376
|
+
// Lifecycle hooks last, and awaited so a caller can know they finished. Idempotent, so the
|
|
377
|
+
// orchestrated path — which fires them through the shared session BEFORE getting here, to
|
|
378
|
+
// keep the pre-GENC-1580 order where `onDeactivate` saw a live driver — costs a no-op.
|
|
379
|
+
// Returning a promise is within `AiDriver.dispose`'s declared `void | Promise<void>`.
|
|
380
|
+
yield this.agentSession.dispose();
|
|
381
|
+
});
|
|
410
382
|
}
|
|
411
383
|
/**
|
|
412
384
|
* Stop the current turn (user "stop" button). Aborts the in-flight provider
|
|
@@ -458,21 +430,6 @@ export class ChatDriver extends EventTarget {
|
|
|
458
430
|
}
|
|
459
431
|
return { reason: 'done' };
|
|
460
432
|
}
|
|
461
|
-
/**
|
|
462
|
-
* Build the `done` loop result, carrying the typed failure reason when the turn
|
|
463
|
-
* bailed (PTC-0). The discriminant stays `'done'` either way — the same value a
|
|
464
|
-
* clean turn returns — so consumers matching on `reason === 'done'` are unchanged;
|
|
465
|
-
* `failureReason` is simply present on a failure and absent on success. Omitted
|
|
466
|
-
* (rather than set to `undefined`) so a happy-path result stays byte-identical to
|
|
467
|
-
* the historical `{ reason: 'done' }`.
|
|
468
|
-
*/
|
|
469
|
-
turnDone(failureReason, budget, providerRefused) {
|
|
470
|
-
if (!failureReason)
|
|
471
|
-
return { reason: 'done' };
|
|
472
|
-
// Both payloads are omitted rather than set to `undefined`, so a failure that carries neither has
|
|
473
|
-
// a shape unchanged for a consumer that structurally compares it.
|
|
474
|
-
return Object.assign(Object.assign({ reason: 'done', failureReason }, (budget ? { budget } : {})), (providerRefused ? { providerRefused } : {}));
|
|
475
|
-
}
|
|
476
433
|
/**
|
|
477
434
|
* End the turn on a terminal failure: fail upward when this driver is a sub-agent, speak
|
|
478
435
|
* to the user when it is not, and hand the reason to the caller either way.
|
|
@@ -500,7 +457,7 @@ export class ChatDriver extends EventTarget {
|
|
|
500
457
|
else {
|
|
501
458
|
this.appendDriverNotice((_a = overrides === null || overrides === void 0 ? void 0 : overrides.copy) !== null && _a !== void 0 ? _a : exit.copy);
|
|
502
459
|
}
|
|
503
|
-
return
|
|
460
|
+
return turnDone(exit.resultReason, overrides === null || overrides === void 0 ? void 0 : overrides.budget, overrides === null || overrides === void 0 ? void 0 : overrides.providerRefused);
|
|
504
461
|
}
|
|
505
462
|
/**
|
|
506
463
|
* Terminal budget outcome for a wall hit **outside** the tool loop — today,
|
|
@@ -573,7 +530,7 @@ export class ChatDriver extends EventTarget {
|
|
|
573
530
|
isSubAgent: this.isSubAgent,
|
|
574
531
|
});
|
|
575
532
|
this.appendDriverNotice(this.budgetExhaustedBubble(e));
|
|
576
|
-
return
|
|
533
|
+
return turnDone('budget-exhausted', budgetDetailOf(e));
|
|
577
534
|
}
|
|
578
535
|
/**
|
|
579
536
|
* The sentence shown when the upstream provider refuses (GENC-1506).
|
|
@@ -630,8 +587,68 @@ export class ChatDriver extends EventTarget {
|
|
|
630
587
|
return Object.assign(Object.assign({ failureReason }, (vendor ? { vendor } : {})), (budget ? { budget } : {}));
|
|
631
588
|
}
|
|
632
589
|
/**
|
|
633
|
-
*
|
|
634
|
-
*
|
|
590
|
+
* Make `config` the active agent, firing its lifecycle hooks — `onDeactivate` on the outgoing
|
|
591
|
+
* agent, then `onActivate` on this one — before installing it.
|
|
592
|
+
*
|
|
593
|
+
* This is the entry point a host should use. {@link ChatDriver.applyAgent} installs a config
|
|
594
|
+
* *without* firing hooks, which is right for the two callers that need it (the orchestrator,
|
|
595
|
+
* which fires them through the shared session, and `invokeSubAgent`, where a child's config is
|
|
596
|
+
* not an activation) and wrong for everyone else.
|
|
597
|
+
*
|
|
598
|
+
* @beta
|
|
599
|
+
*/
|
|
600
|
+
activateAgent(config) {
|
|
601
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
602
|
+
yield this.agentSession.activate(config);
|
|
603
|
+
this.applyAgent(config);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* {@inheritDoc AiDriver.getActiveAgent}
|
|
608
|
+
*
|
|
609
|
+
* @beta
|
|
610
|
+
*/
|
|
611
|
+
getActiveAgent() {
|
|
612
|
+
return this.agentSession.getActive();
|
|
613
|
+
}
|
|
614
|
+
/** {@inheritDoc AiDriver.primeRestoredAgentState} */
|
|
615
|
+
primeRestoredAgentState(snapshots) {
|
|
616
|
+
this.agentSession.primeRestoredStates(snapshots);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Record the sticky agent pin. See {@link AiDriver.setPinnedAgent} for the contract.
|
|
620
|
+
*
|
|
621
|
+
* Recorded, not acted on: this driver has one agent and nothing to route between, so a pin
|
|
622
|
+
* changes no behaviour here. It is still honoured as state, because the session outlives the
|
|
623
|
+
* question of which driver is holding it — a host that pins before an orchestrator is built
|
|
624
|
+
* would otherwise lose the pick.
|
|
625
|
+
*
|
|
626
|
+
* @beta
|
|
627
|
+
*/
|
|
628
|
+
setPinnedAgent(name) {
|
|
629
|
+
this.agentSession.setPinned(name);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Record the transient flow lock. See {@link AiDriver.setFlowOwner} for the contract.
|
|
633
|
+
*
|
|
634
|
+
* Recorded, not acted on, for the same reason as {@link ChatDriver.setPinnedAgent}.
|
|
635
|
+
*
|
|
636
|
+
* @beta
|
|
637
|
+
*/
|
|
638
|
+
setFlowOwner(name) {
|
|
639
|
+
this.agentSession.setFlowOwner(name);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Install `config` as the active agent **without firing lifecycle hooks**.
|
|
643
|
+
*
|
|
644
|
+
* Prefer {@link ChatDriver.activateAgent} unless you are deliberately skipping the hooks. Two
|
|
645
|
+
* callers are: `OrchestratingDriver`, which fires them through the shared
|
|
646
|
+
* {@link ChatDriver.agentSession} and then installs a decorated config, and `invokeSubAgent`,
|
|
647
|
+
* where the child driver's config is a tool surface rather than an activation.
|
|
648
|
+
*
|
|
649
|
+
* Renamed to `installAgent` in a follow-up (GENC-1580) — see `docs/migration-GENC-1580.md`.
|
|
650
|
+
* Until then the name says less than it should, so do not add a third caller to this path
|
|
651
|
+
* without checking which of the two above it resembles.
|
|
635
652
|
*/
|
|
636
653
|
applyAgent(config) {
|
|
637
654
|
var _a, _b, _c;
|
|
@@ -689,9 +706,10 @@ export class ChatDriver extends EventTarget {
|
|
|
689
706
|
if (typeof config.provider === 'string') {
|
|
690
707
|
this.resolveProviderByName(config.provider, config.name);
|
|
691
708
|
}
|
|
692
|
-
// Reset fold state when agent changes — each specialist starts fresh
|
|
693
|
-
|
|
694
|
-
|
|
709
|
+
// Reset fold state when agent changes — each specialist starts fresh, and an unclosed fold
|
|
710
|
+
// must not survive into the next agent: its frame holds the PREVIOUS agent's tool surface,
|
|
711
|
+
// which `close()` would restore over the incoming one.
|
|
712
|
+
this.folds.reset();
|
|
695
713
|
// Forget the previous agent's tools — "previously available" is scoped to
|
|
696
714
|
// the current activation, so a stateful agent accumulates its tools across
|
|
697
715
|
// states while a swap to a different specialist starts clean.
|
|
@@ -827,20 +845,6 @@ export class ChatDriver extends EventTarget {
|
|
|
827
845
|
return status;
|
|
828
846
|
});
|
|
829
847
|
}
|
|
830
|
-
/**
|
|
831
|
-
* Resolve a per-turn config input that is either a static value or a function
|
|
832
|
-
* of the turn context — the value-or-resolver shape shared by `provider`,
|
|
833
|
-
* `temperature`, and `toolChoice`. Returns undefined when the input is unset.
|
|
834
|
-
*/
|
|
835
|
-
resolveTurnInput(input, ctx) {
|
|
836
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
837
|
-
if (input === undefined)
|
|
838
|
-
return undefined;
|
|
839
|
-
return typeof input === 'function'
|
|
840
|
-
? input(ctx)
|
|
841
|
-
: input;
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
848
|
/**
|
|
845
849
|
* Returns the early-stop result set by `completeSubAgent`, if any.
|
|
846
850
|
* Called by a parent `ChatDriver` after running this instance as a sub-agent.
|
|
@@ -886,6 +890,22 @@ export class ChatDriver extends EventTarget {
|
|
|
886
890
|
getAgentReleaseRequested() {
|
|
887
891
|
return this.agentReleaseRequested;
|
|
888
892
|
}
|
|
893
|
+
/**
|
|
894
|
+
* Act on a `releaseAgent` call from a tool handler: fire `onDeactivate` and drop the flow lock,
|
|
895
|
+
* so the next turn starts clean and the persister does not serialize a flow that is over.
|
|
896
|
+
*
|
|
897
|
+
* No-op when the owner handles release ({@link ChatDriverConfig.deferAgentRelease}). The flag is
|
|
898
|
+
* cleared here rather than only at the next `sendMessage`, so the signal cannot be acted on twice
|
|
899
|
+
* and release a later agent that never asked.
|
|
900
|
+
*/
|
|
901
|
+
releaseIfRequested() {
|
|
902
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
903
|
+
if (this.deferAgentRelease || !this.agentReleaseRequested)
|
|
904
|
+
return;
|
|
905
|
+
this.agentReleaseRequested = false;
|
|
906
|
+
yield this.agentSession.release();
|
|
907
|
+
});
|
|
908
|
+
}
|
|
889
909
|
/**
|
|
890
910
|
* Return the per-turn snapshots captured so far. Used by the host's debug
|
|
891
911
|
* log exporter to show what the LLM saw on each turn — system prompt, tool
|
|
@@ -1065,7 +1085,7 @@ export class ChatDriver extends EventTarget {
|
|
|
1065
1085
|
}
|
|
1066
1086
|
/** Returns the current fold stack names for debugging. */
|
|
1067
1087
|
getActiveFoldNames() {
|
|
1068
|
-
return this.
|
|
1088
|
+
return this.folds.activeNames();
|
|
1069
1089
|
}
|
|
1070
1090
|
/**
|
|
1071
1091
|
* The transcript as it should be **stored**: every tool payload the model has already stopped
|
|
@@ -1087,106 +1107,18 @@ export class ChatDriver extends EventTarget {
|
|
|
1087
1107
|
getHistoryForPersistence() {
|
|
1088
1108
|
return condenseForPersistence([...this.history], this.condensePolicies);
|
|
1089
1109
|
}
|
|
1110
|
+
/**
|
|
1111
|
+
* {@inheritDoc AiDriver.getSuggestions}
|
|
1112
|
+
*
|
|
1113
|
+
* @beta
|
|
1114
|
+
*/
|
|
1090
1115
|
getSuggestions(history, prompt, count, allAgentInfo) {
|
|
1091
1116
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
logger.warn('ChatDriver: default AI provider does not implement prompt()');
|
|
1098
|
-
return [];
|
|
1099
|
-
}
|
|
1100
|
-
let agentContext = '';
|
|
1101
|
-
let toolContext = '';
|
|
1102
|
-
if (allAgentInfo === null || allAgentInfo === void 0 ? void 0 : allAgentInfo.length) {
|
|
1103
|
-
const agentDescriptions = allAgentInfo
|
|
1104
|
-
.map((agent) => {
|
|
1105
|
-
const tools = agent.tools.map((t) => t.name).join(', ');
|
|
1106
|
-
return `- ${agent.name} (${agent.description}): ${tools ? `(Tools: ${tools})` : 'No tools'}`;
|
|
1107
|
-
})
|
|
1108
|
-
.join('\n');
|
|
1109
|
-
agentContext = `The assistant has the following capabilities:\n${agentDescriptions}`;
|
|
1110
|
-
const allToolNames = allAgentInfo
|
|
1111
|
-
.flatMap((agent) => agent.tools.map((t) => t.name))
|
|
1112
|
-
.filter((value, index, self) => self.indexOf(value) === index)
|
|
1113
|
-
.join(', ');
|
|
1114
|
-
toolContext = allToolNames
|
|
1115
|
-
? `You have access to the following tools across all agents: ${allToolNames}.`
|
|
1116
|
-
: '';
|
|
1117
|
-
}
|
|
1118
|
-
else if (this.activeAgentName) {
|
|
1119
|
-
const toolNames = this.toolDefinitions.map((tool) => tool.name).join(', ');
|
|
1120
|
-
agentContext = `You are currently acting as the "${this.activeAgentName}" agent.`;
|
|
1121
|
-
toolContext = toolNames ? `You have access to the following tools: ${toolNames}.` : '';
|
|
1122
|
-
}
|
|
1123
|
-
// System prompt is intentionally short: it sets the role only. Concrete
|
|
1124
|
-
// instructions live in the user message *after* the transcript so the
|
|
1125
|
-
// immediately-preceding context for the model is "here is data, now do X"
|
|
1126
|
-
// rather than "here is a dialogue, continue it". This matters for Anthropic
|
|
1127
|
-
// — without it, Claude reads the transcript as an in-progress conversation
|
|
1128
|
-
// and produces a normal assistant turn instead of a list of suggestions.
|
|
1129
|
-
const systemPrompt = 'You generate suggested prompts that a user could send to an AI assistant. ' +
|
|
1130
|
-
'You never continue the conversation or roleplay as the assistant.';
|
|
1131
|
-
const capabilitiesBlock = agentContext || toolContext
|
|
1132
|
-
? `\n\n<capabilities>\n${[agentContext, toolContext].filter(Boolean).join('\n')}\n</capabilities>\nSuggestions must only cover what the agent is capable of. Do not suggest anything outside these capabilities.`
|
|
1133
|
-
: '';
|
|
1134
|
-
const guidanceBlock = prompt ? `\n\nAdditional guidance: "${prompt}"` : '';
|
|
1135
|
-
let userMessage;
|
|
1136
|
-
if (history.length === 0) {
|
|
1137
|
-
userMessage =
|
|
1138
|
-
`Generate exactly ${count} brief starter prompts to show a user what an AI assistant can do. ` +
|
|
1139
|
-
`Phrase each one as the user would write it. Keep them short and generic — do not invent specific names, IDs, or data (e.g. prefer "Search for a trade" over "Find all trades with Client A").` +
|
|
1140
|
-
capabilitiesBlock +
|
|
1141
|
-
guidanceBlock +
|
|
1142
|
-
`\n\nOutput format (strict):\n` +
|
|
1143
|
-
`- Exactly ${count} lines.\n` +
|
|
1144
|
-
`- One suggestion per line.\n` +
|
|
1145
|
-
`- No numbering, bullets, markdown, emojis, code blocks, or quotes around the suggestion.\n` +
|
|
1146
|
-
`- No preamble, headings, summary, or commentary before or after the list.`;
|
|
1147
|
-
}
|
|
1148
|
-
else {
|
|
1149
|
-
const conversationContext = history
|
|
1150
|
-
.filter((m) => {
|
|
1151
|
-
var _a, _b;
|
|
1152
|
-
return (m.role === 'user' || m.role === 'assistant') &&
|
|
1153
|
-
!((_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.length) &&
|
|
1154
|
-
!m.thinking &&
|
|
1155
|
-
!!((_b = m.content) === null || _b === void 0 ? void 0 : _b.trim());
|
|
1156
|
-
})
|
|
1157
|
-
.slice(-SUGGESTIONS_HISTORY_WINDOW)
|
|
1158
|
-
.map((m) => `${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`)
|
|
1159
|
-
.join('\n');
|
|
1160
|
-
userMessage =
|
|
1161
|
-
`<conversation_history>\n${conversationContext}\n</conversation_history>\n\n` +
|
|
1162
|
-
`The conversation above has ended. Do not continue it or respond as the assistant. ` +
|
|
1163
|
-
`Generate exactly ${count} follow-up prompts the *user* might send next, phrased in first person as the user would write them. ` +
|
|
1164
|
-
`The first ${Math.max(0, count - 1)} should be natural follow-ups to the last turn. ` +
|
|
1165
|
-
`Do not invent specific names, IDs, or data values that do not appear in <conversation_history>.` +
|
|
1166
|
-
capabilitiesBlock +
|
|
1167
|
-
guidanceBlock +
|
|
1168
|
-
`\n\nOutput format (strict):\n` +
|
|
1169
|
-
`- Exactly ${count} lines.\n` +
|
|
1170
|
-
`- One suggestion per line.\n` +
|
|
1171
|
-
`- No numbering, bullets, markdown, emojis, code blocks, or quotes around the suggestion.\n` +
|
|
1172
|
-
`- No preamble, headings, summary, or commentary before or after the list.`;
|
|
1173
|
-
}
|
|
1174
|
-
const text = yield defaultProvider.prompt(userMessage, { systemPrompt });
|
|
1175
|
-
// Lenient parsing as a defensive backstop: even with the strict prompt,
|
|
1176
|
-
// models occasionally slip in numbering, bullets, or surrounding markdown.
|
|
1177
|
-
return (text
|
|
1178
|
-
.split('\n')
|
|
1179
|
-
.map((s) => s.trim())
|
|
1180
|
-
// strip leading bullets/numbers ("- ", "* ", "• ", "1. ", "1) ")
|
|
1181
|
-
.map((s) => s.replace(/^([-*•]|\d+[.)])\s+/, ''))
|
|
1182
|
-
// strip surrounding bold/italic markers
|
|
1183
|
-
.map((s) => s.replace(/^[*_]{1,3}|[*_]{1,3}$/g, '').trim())
|
|
1184
|
-
// drop empty lines, separators, and obvious preamble lines
|
|
1185
|
-
.filter((s) => s.length > 0)
|
|
1186
|
-
.filter((s) => !/^[-=]{2,}$/.test(s))
|
|
1187
|
-
.filter((s) => !/^here (are|is) /i.test(s))
|
|
1188
|
-
.filter((s) => !s.endsWith(':'))
|
|
1189
|
-
.slice(0, count));
|
|
1117
|
+
return generateSuggestions({
|
|
1118
|
+
registry: this.providerRegistry,
|
|
1119
|
+
activeAgentName: this.activeAgentName,
|
|
1120
|
+
toolDefinitions: this.toolDefinitions,
|
|
1121
|
+
}, history, prompt, count, allAgentInfo);
|
|
1190
1122
|
});
|
|
1191
1123
|
}
|
|
1192
1124
|
/**
|
|
@@ -1668,7 +1600,7 @@ export class ChatDriver extends EventTarget {
|
|
|
1668
1600
|
message: e instanceof Error ? e.message : String(e),
|
|
1669
1601
|
});
|
|
1670
1602
|
this.appendDriverNotice('Sorry, something went wrong on my end. Please try again in a moment.');
|
|
1671
|
-
result =
|
|
1603
|
+
result = turnDone('exception');
|
|
1672
1604
|
return result;
|
|
1673
1605
|
}
|
|
1674
1606
|
finally {
|
|
@@ -1680,6 +1612,9 @@ export class ChatDriver extends EventTarget {
|
|
|
1680
1612
|
this.busy = false;
|
|
1681
1613
|
this.endTurn();
|
|
1682
1614
|
this.activityBus.publish('tool-loop-end', this.loopEndDetail(result));
|
|
1615
|
+
// In `finally` rather than on the success path: the agent asked to be released, and a turn
|
|
1616
|
+
// that later threw does not make its flow any less finished.
|
|
1617
|
+
yield this.releaseIfRequested();
|
|
1683
1618
|
}
|
|
1684
1619
|
});
|
|
1685
1620
|
}
|
|
@@ -2046,7 +1981,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2046
1981
|
message: e instanceof Error ? e.message : String(e),
|
|
2047
1982
|
});
|
|
2048
1983
|
this.appendDriverNotice('Sorry, something went wrong on my end. Please try again in a moment.');
|
|
2049
|
-
result =
|
|
1984
|
+
result = turnDone('exception');
|
|
2050
1985
|
return result;
|
|
2051
1986
|
}
|
|
2052
1987
|
finally {
|
|
@@ -2058,153 +1993,13 @@ export class ChatDriver extends EventTarget {
|
|
|
2058
1993
|
this.busy = false;
|
|
2059
1994
|
this.endTurn();
|
|
2060
1995
|
this.activityBus.publish('tool-loop-end', this.loopEndDetail(result));
|
|
1996
|
+
// In `finally` rather than on the success path: the agent asked to be released, and a turn
|
|
1997
|
+
// that later threw does not make its flow any less finished.
|
|
1998
|
+
yield this.releaseIfRequested();
|
|
2061
1999
|
}
|
|
2062
2000
|
});
|
|
2063
2001
|
}
|
|
2064
2002
|
// ---------------------------------------------------------------------------
|
|
2065
|
-
// Fold mechanics
|
|
2066
|
-
// ---------------------------------------------------------------------------
|
|
2067
|
-
/** Extract ToolFold metadata from a handler, or undefined if it isn't a fold facade. */
|
|
2068
|
-
getFold(toolName) {
|
|
2069
|
-
const handler = this.toolHandlers[toolName];
|
|
2070
|
-
return handler ? handler[TOOL_FOLD_SYMBOL] : undefined;
|
|
2071
|
-
}
|
|
2072
|
-
/**
|
|
2073
|
-
* Search all currently registered fold facades (and their nested folds recursively)
|
|
2074
|
-
* to find which fold contains a given tool name. Returns the immediate parent fold name.
|
|
2075
|
-
*/
|
|
2076
|
-
findFoldContaining(toolName, handlers) {
|
|
2077
|
-
const source = handlers !== null && handlers !== void 0 ? handlers : this.toolHandlers;
|
|
2078
|
-
for (const [, handler] of Object.entries(source)) {
|
|
2079
|
-
const fold = handler[TOOL_FOLD_SYMBOL];
|
|
2080
|
-
if (!fold)
|
|
2081
|
-
continue;
|
|
2082
|
-
// Direct inner tool match
|
|
2083
|
-
if (fold.handlers[toolName])
|
|
2084
|
-
return fold.name;
|
|
2085
|
-
// Recurse into nested folds
|
|
2086
|
-
const nested = this.findFoldContaining(toolName, fold.handlers);
|
|
2087
|
-
if (nested)
|
|
2088
|
-
return fold.name;
|
|
2089
|
-
}
|
|
2090
|
-
return null;
|
|
2091
|
-
}
|
|
2092
|
-
/**
|
|
2093
|
-
* If an open fold is hiding a previously-available tool, return the name of
|
|
2094
|
-
* the fold to close to start getting it back. Only exclusive folds hide tools
|
|
2095
|
-
* (they replace the tool set on open rather than extending it), so a base tool
|
|
2096
|
-
* that was visible before the fold opened now sits in a fold-stack frame's
|
|
2097
|
-
* `previousHandlers` but not in the live handler map. Only the top fold's
|
|
2098
|
-
* `close_` tool is active, so that's always the actionable next step — even
|
|
2099
|
-
* when the tool lives further down the stack, closing repeatedly walks back to
|
|
2100
|
-
* it. Returns null when no open fold accounts for the tool.
|
|
2101
|
-
*/
|
|
2102
|
-
foldHidingTool(toolName) {
|
|
2103
|
-
if (this.foldStack.length === 0)
|
|
2104
|
-
return null;
|
|
2105
|
-
const hidden = this.foldStack.some((f) => f.previousHandlers[toolName]);
|
|
2106
|
-
return hidden ? this.foldStack[this.foldStack.length - 1].foldName : null;
|
|
2107
|
-
}
|
|
2108
|
-
/**
|
|
2109
|
-
* Install the fold's inner tool set, replacing (exclusive) or extending (non-exclusive)
|
|
2110
|
-
* the current tool set. Also injects the close tool. Does NOT touch the fold stack.
|
|
2111
|
-
*/
|
|
2112
|
-
applyFoldToolSet(fold, foldName) {
|
|
2113
|
-
const closeToolName = `close_${foldName}`;
|
|
2114
|
-
const newDefs = [];
|
|
2115
|
-
const newHandlers = {};
|
|
2116
|
-
if (!fold.exclusive) {
|
|
2117
|
-
// Non-exclusive: keep existing tools minus the facade we just opened
|
|
2118
|
-
for (const def of this.toolDefinitions) {
|
|
2119
|
-
if (def.name !== foldName)
|
|
2120
|
-
newDefs.push(def);
|
|
2121
|
-
}
|
|
2122
|
-
for (const [name, handler] of Object.entries(this.toolHandlers)) {
|
|
2123
|
-
if (name !== foldName)
|
|
2124
|
-
newHandlers[name] = handler;
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
// Install inner tools from fold metadata
|
|
2128
|
-
newDefs.push(...fold.tools);
|
|
2129
|
-
Object.assign(newHandlers, fold.handlers);
|
|
2130
|
-
// Inject the close tool
|
|
2131
|
-
newDefs.push({
|
|
2132
|
-
name: closeToolName,
|
|
2133
|
-
description: `Close the ${foldName} fold and return to the previous set of tools.`,
|
|
2134
|
-
parameters: { type: 'object', properties: {} },
|
|
2135
|
-
});
|
|
2136
|
-
newHandlers[closeToolName] = () => __awaiter(this, void 0, void 0, function* () { return this.closeFold(); });
|
|
2137
|
-
this.toolDefinitions = newDefs;
|
|
2138
|
-
this.toolHandlers = newHandlers;
|
|
2139
|
-
}
|
|
2140
|
-
/** Open a fold: push a stack frame, swap the tool set, return the response message. */
|
|
2141
|
-
openFold(foldName, fold, args, activeToolCallId) {
|
|
2142
|
-
// Shortcut dispatch: model passed inner tool args directly, e.g.
|
|
2143
|
-
// trading_tools({ search_trades: { side: "BUY" } })
|
|
2144
|
-
for (const key of Object.keys(args)) {
|
|
2145
|
-
const innerHandler = fold.handlers[key];
|
|
2146
|
-
if (innerHandler) {
|
|
2147
|
-
logger.debug(`ChatDriver: fold shortcut dispatch "${foldName}" → "${key}"`);
|
|
2148
|
-
// Open the fold first so the tool set is correct for subsequent calls
|
|
2149
|
-
this.pushFoldFrame(foldName);
|
|
2150
|
-
this.applyFoldToolSet(fold, foldName);
|
|
2151
|
-
this.consecutiveFoldOps = 0; // shortcut dispatch counts as real work
|
|
2152
|
-
const innerArgs = typeof args[key] === 'object' && args[key] !== null
|
|
2153
|
-
? args[key]
|
|
2154
|
-
: {};
|
|
2155
|
-
return innerHandler(innerArgs, this.buildHandlerContext(activeToolCallId)).then((r) => typeof r === 'string' ? r : JSON.stringify(r));
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
// Normal two-step open
|
|
2159
|
-
this.pushFoldFrame(foldName);
|
|
2160
|
-
this.applyFoldToolSet(fold, foldName);
|
|
2161
|
-
const innerToolNames = fold.tools.map((t) => t.name);
|
|
2162
|
-
const closeToolName = `close_${foldName}`;
|
|
2163
|
-
let message = `Fold opened: ${foldName}. Tools now available: ${[...innerToolNames, closeToolName].join(', ')}.`;
|
|
2164
|
-
if (fold.usageNotes)
|
|
2165
|
-
message += ` Notes: ${fold.usageNotes}`;
|
|
2166
|
-
message += ` Call ${closeToolName} when done to return to the previous tools.`;
|
|
2167
|
-
return Promise.resolve(message);
|
|
2168
|
-
}
|
|
2169
|
-
pushFoldFrame(foldName) {
|
|
2170
|
-
this.foldStack.push({
|
|
2171
|
-
foldName,
|
|
2172
|
-
previousDefinitions: [...this.toolDefinitions],
|
|
2173
|
-
previousHandlers: Object.assign({}, this.toolHandlers),
|
|
2174
|
-
});
|
|
2175
|
-
}
|
|
2176
|
-
/** Close the top fold: pop the stack frame, restore the previous tool set. */
|
|
2177
|
-
closeFold() {
|
|
2178
|
-
const frame = this.foldStack.pop();
|
|
2179
|
-
if (!frame)
|
|
2180
|
-
return 'No fold is currently open.';
|
|
2181
|
-
this.toolDefinitions = frame.previousDefinitions;
|
|
2182
|
-
this.toolHandlers = frame.previousHandlers;
|
|
2183
|
-
const toolNames = this.toolDefinitions.map((t) => t.name);
|
|
2184
|
-
return `Fold closed: ${frame.foldName}. Tools now available: ${toolNames.join(', ')}.`;
|
|
2185
|
-
}
|
|
2186
|
-
/** Build the fold-awareness suffix appended to the system prompt each LLM call. */
|
|
2187
|
-
buildFoldSystemPromptSuffix() {
|
|
2188
|
-
// Collect fold facades from the current handler map
|
|
2189
|
-
const activeFolds = [];
|
|
2190
|
-
for (const handler of Object.values(this.toolHandlers)) {
|
|
2191
|
-
const fold = handler[TOOL_FOLD_SYMBOL];
|
|
2192
|
-
if (fold)
|
|
2193
|
-
activeFolds.push(fold);
|
|
2194
|
-
}
|
|
2195
|
-
if (activeFolds.length === 0 && this.foldStack.length === 0)
|
|
2196
|
-
return '';
|
|
2197
|
-
const parts = ['\n\n--- Tool Folds ---'];
|
|
2198
|
-
parts.push('Some tools are grouped into folds. You may see tool calls in the conversation history for tools that are not currently available — they are inside a fold. To access them, invoke the fold tool first.');
|
|
2199
|
-
if (this.foldStack.length > 0) {
|
|
2200
|
-
const current = this.foldStack[this.foldStack.length - 1];
|
|
2201
|
-
parts.push(`You are currently inside the "${current.foldName}" fold. Call close_${current.foldName} when you are done with these tools.`);
|
|
2202
|
-
}
|
|
2203
|
-
for (const fold of activeFolds) {
|
|
2204
|
-
parts.push(`• ${fold.name}: ${fold.description}`);
|
|
2205
|
-
}
|
|
2206
|
-
return parts.join('\n');
|
|
2207
|
-
}
|
|
2208
2003
|
// ---------------------------------------------------------------------------
|
|
2209
2004
|
// Tool loop
|
|
2210
2005
|
// ---------------------------------------------------------------------------
|
|
@@ -2375,7 +2170,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2375
2170
|
// retry nudge — that would otherwise destabilize a cached system prompt. They are routed to
|
|
2376
2171
|
// the framed tail (below), never appended to the system string, so the system prompt stays
|
|
2377
2172
|
// byte-stable for every agent.
|
|
2378
|
-
const foldSuffix = this.
|
|
2173
|
+
const foldSuffix = this.folds.systemPromptSuffix();
|
|
2379
2174
|
const retryNudge = malformedAttempts > 0
|
|
2380
2175
|
? `\n\n${MALFORMED_CALL_NUDGE}`
|
|
2381
2176
|
: emptyResponseAttempts > 0
|
|
@@ -2423,16 +2218,16 @@ export class ChatDriver extends EventTarget {
|
|
|
2423
2218
|
const [resolvedTemperature, resolvedToolChoice, resolvedCachePolicy, resolvedTailContext, resolvedResponseSchema, firstResolvedThinkingPolicy,] =
|
|
2424
2219
|
// oxlint-disable-next-line no-await-in-loop
|
|
2425
2220
|
yield Promise.all([
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2221
|
+
resolveTurnInput(this.activeTemperatureInput, promptCtx),
|
|
2222
|
+
resolveTurnInput(this.activeToolChoiceInput, promptCtx),
|
|
2223
|
+
resolveTurnInput(this.activeCachePolicyInput, promptCtx),
|
|
2224
|
+
resolveTurnInput(this.activeTailContextInput, promptCtx),
|
|
2225
|
+
resolveTurnInput(this.activeResponseSchemaInput, promptCtx),
|
|
2431
2226
|
// Only consulted on the first iteration (see `pinnedThinkingPolicy`); resolved
|
|
2432
2227
|
// alongside the others so a resolver still sees the same turn context.
|
|
2433
2228
|
thinkingPolicyPinned
|
|
2434
2229
|
? Promise.resolve(undefined)
|
|
2435
|
-
:
|
|
2230
|
+
: resolveTurnInput(this.activeThinkingPolicyInput, promptCtx),
|
|
2436
2231
|
]);
|
|
2437
2232
|
if (!thinkingPolicyPinned) {
|
|
2438
2233
|
pinnedThinkingPolicy = firstResolvedThinkingPolicy;
|
|
@@ -2696,7 +2491,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2696
2491
|
// user, carrying the attribution so the parent walls the vendor that
|
|
2697
2492
|
// actually refused rather than its own.
|
|
2698
2493
|
this.failSubAgent('budget_exhausted', this.budgetWallDetail);
|
|
2699
|
-
return
|
|
2494
|
+
return turnDone('budget-exhausted', this.budgetWallDetail);
|
|
2700
2495
|
}
|
|
2701
2496
|
return this.reportBudgetExhausted(e);
|
|
2702
2497
|
}
|
|
@@ -2876,36 +2671,36 @@ export class ChatDriver extends EventTarget {
|
|
|
2876
2671
|
// oxlint-disable-next-line no-await-in-loop -- Promise.all is correct parallelism, not sequential loop awaiting
|
|
2877
2672
|
yield Promise.all(toolCalls.map((tc) => __awaiter(this, void 0, void 0, function* () {
|
|
2878
2673
|
// Check for fold facade
|
|
2879
|
-
const fold = this.
|
|
2674
|
+
const fold = this.folds.foldFor(tc.name);
|
|
2880
2675
|
if (fold) {
|
|
2881
|
-
this.
|
|
2882
|
-
if (
|
|
2676
|
+
const foldOps = this.folds.noteOp();
|
|
2677
|
+
if (foldOps > this.maxFoldOperations) {
|
|
2883
2678
|
logger.warn(`ChatDriver: fold operation limit (${this.maxFoldOperations}) reached — injecting guidance`);
|
|
2884
2679
|
executedById.set(tc.id, {
|
|
2885
2680
|
toolCallId: tc.id,
|
|
2886
|
-
content: `You have opened and closed folds ${
|
|
2681
|
+
content: `You have opened and closed folds ${foldOps} times without calling any tools. Please call a specific tool to make progress, or respond to the user.`,
|
|
2887
2682
|
});
|
|
2888
2683
|
return;
|
|
2889
2684
|
}
|
|
2890
|
-
const content = yield this.
|
|
2685
|
+
const content = yield this.folds.open(tc.name, fold, tc.args, tc.id);
|
|
2891
2686
|
executedById.set(tc.id, { toolCallId: tc.id, content });
|
|
2892
2687
|
// Fold open/close does NOT count as a real iteration — decrement to compensate
|
|
2893
2688
|
iterations -= 1;
|
|
2894
2689
|
return;
|
|
2895
2690
|
}
|
|
2896
2691
|
// Check for close-fold tool
|
|
2897
|
-
if (tc.name.startsWith('close_') && this.
|
|
2898
|
-
const topFoldName = this.
|
|
2692
|
+
if (tc.name.startsWith('close_') && this.folds.depth > 0) {
|
|
2693
|
+
const topFoldName = this.folds.topName;
|
|
2899
2694
|
if (tc.name === `close_${topFoldName}`) {
|
|
2900
|
-
this.
|
|
2901
|
-
if (
|
|
2695
|
+
const foldOps = this.folds.noteOp();
|
|
2696
|
+
if (foldOps > this.maxFoldOperations) {
|
|
2902
2697
|
executedById.set(tc.id, {
|
|
2903
2698
|
toolCallId: tc.id,
|
|
2904
|
-
content: `You have opened and closed folds ${
|
|
2699
|
+
content: `You have opened and closed folds ${foldOps} times without calling any tools. Please call a specific tool to make progress, or respond to the user.`,
|
|
2905
2700
|
});
|
|
2906
2701
|
return;
|
|
2907
2702
|
}
|
|
2908
|
-
const content = this.
|
|
2703
|
+
const content = this.folds.close();
|
|
2909
2704
|
executedById.set(tc.id, { toolCallId: tc.id, content });
|
|
2910
2705
|
iterations -= 1;
|
|
2911
2706
|
return;
|
|
@@ -2914,7 +2709,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2914
2709
|
// Regular tool — check if it's inside a fold and guide the model
|
|
2915
2710
|
const handler = this.toolHandlers[tc.name];
|
|
2916
2711
|
if (!handler) {
|
|
2917
|
-
const containingFold = this.
|
|
2712
|
+
const containingFold = this.folds.findContaining(tc.name);
|
|
2918
2713
|
if (containingFold) {
|
|
2919
2714
|
logger.debug(`ChatDriver: model called folded tool "${tc.name}" — guiding to open "${containingFold}"`);
|
|
2920
2715
|
recordMetaEvent(this.sessionKey, 'tool.unresolved', {
|
|
@@ -2941,7 +2736,7 @@ export class ChatDriver extends EventTarget {
|
|
|
2941
2736
|
// guidance, telemetry, and limit differ.
|
|
2942
2737
|
if (this.everSeenToolNames.has(tc.name)) {
|
|
2943
2738
|
this.consecutiveUnknownToolCalls += 1;
|
|
2944
|
-
const hidingFold = this.
|
|
2739
|
+
const hidingFold = this.folds.hidingTool(tc.name);
|
|
2945
2740
|
let content;
|
|
2946
2741
|
if (hidingFold) {
|
|
2947
2742
|
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}.`;
|
|
@@ -3050,13 +2845,13 @@ export class ChatDriver extends EventTarget {
|
|
|
3050
2845
|
}
|
|
3051
2846
|
// Reset counters whenever a real tool executes
|
|
3052
2847
|
if (anyRealToolExecuted) {
|
|
3053
|
-
this.
|
|
2848
|
+
this.folds.clearOps();
|
|
3054
2849
|
this.consecutiveUnknownToolCalls = 0;
|
|
3055
2850
|
this.recentUnknownToolNames.clear();
|
|
3056
2851
|
this.recentStaleToolNames.clear();
|
|
3057
2852
|
}
|
|
3058
2853
|
// Tag tool calls with fold UI metadata before appending results
|
|
3059
|
-
const foldPath = this.
|
|
2854
|
+
const foldPath = this.folds.activeNames();
|
|
3060
2855
|
for (const tc of response.toolCalls) {
|
|
3061
2856
|
if (tc.name === REQUEST_CONTINUATION_TOOL) {
|
|
3062
2857
|
this.appendToHistory({
|
|
@@ -3095,7 +2890,7 @@ export class ChatDriver extends EventTarget {
|
|
|
3095
2890
|
const availableToolNames = Object.keys(this.toolHandlers);
|
|
3096
2891
|
const annotatedCalls = tcMsg.toolCalls.map((tc) => {
|
|
3097
2892
|
var _a, _b, _c, _d, _e;
|
|
3098
|
-
const isFoldOpen = !!this.
|
|
2893
|
+
const isFoldOpen = !!this.folds.foldFor(tc.name) ||
|
|
3099
2894
|
(
|
|
3100
2895
|
// Was a fold facade at time of the call (now the tool set has changed)
|
|
3101
2896
|
// — detect by checking if the result message indicated a fold open
|
|
@@ -3160,7 +2955,7 @@ export class ChatDriver extends EventTarget {
|
|
|
3160
2955
|
// Sub-agent early exit — checked here so the exit point mirrors the
|
|
3161
2956
|
// system-call pattern above. Set by completeSubAgent() in a tool handler.
|
|
3162
2957
|
if (this.subAgentCompletion) {
|
|
3163
|
-
return
|
|
2958
|
+
return turnDone();
|
|
3164
2959
|
}
|
|
3165
2960
|
}
|
|
3166
2961
|
// The loop fell through: either it hit the iteration cap (a failure) or it
|
|
@@ -3176,7 +2971,7 @@ export class ChatDriver extends EventTarget {
|
|
|
3176
2971
|
});
|
|
3177
2972
|
return this.endTurnWithFailure(TURN_FAILURE_EXITS.maxIterations);
|
|
3178
2973
|
}
|
|
3179
|
-
return
|
|
2974
|
+
return turnDone();
|
|
3180
2975
|
});
|
|
3181
2976
|
}
|
|
3182
2977
|
/**
|