@genesislcap/ai-assistant 14.436.0 → 14.437.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 +315 -66
- package/dist/ai-assistant.d.ts +130 -41
- package/dist/dts/components/chat-driver/chat-driver.d.ts +36 -5
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +30 -2
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +38 -14
- package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +8 -6
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +12 -10
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +2 -2
- package/dist/esm/components/chat-driver/chat-driver.js +66 -8
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +6 -0
- package/dist/esm/config/define-stateful-agent.js +32 -45
- package/dist/esm/main/main.js +43 -12
- package/dist/esm/main/main.template.js +8 -5
- package/dist/esm/state/ai-assistant-slice.js +5 -5
- package/package.json +16 -16
- package/src/components/chat-driver/chat-driver.ts +124 -14
- package/src/components/orchestrating-driver/orchestrating-driver.ts +10 -0
- package/src/config/config.ts +33 -2
- package/src/config/define-stateful-agent.ts +72 -57
- package/src/main/main.template.ts +1 -1
- package/src/main/main.ts +44 -12
- package/src/state/ai-assistant-slice.ts +15 -15
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ChatToolCall,
|
|
8
8
|
ChatToolDefinition,
|
|
9
9
|
ChatToolHandlers,
|
|
10
|
+
InteractionRequestOptions,
|
|
10
11
|
SubAgentRequestOptions,
|
|
11
12
|
} from '@genesislcap/foundation-ai';
|
|
12
13
|
import { MalformedFunctionCallError } from '@genesislcap/foundation-ai';
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
SystemPromptContext,
|
|
16
17
|
SystemPromptInput,
|
|
17
18
|
ToolDefinitionsInput,
|
|
19
|
+
ToolHandlersInput,
|
|
18
20
|
} from '../../config/config';
|
|
19
21
|
import { applyHistoryCap } from '../../utils/history-transform';
|
|
20
22
|
import { logger } from '../../utils/logger';
|
|
@@ -64,6 +66,12 @@ export interface TurnSnapshot {
|
|
|
64
66
|
systemPrompt?: string;
|
|
65
67
|
/** Tool names sent to the LLM, in order — definitions are static per name so names alone suffice. */
|
|
66
68
|
toolNames: string[];
|
|
69
|
+
/**
|
|
70
|
+
* Per-turn display label resolved from the agent's `displayName`, e.g.
|
|
71
|
+
* "Guided Booking (Counterparties)". `agentName` stays as the canonical
|
|
72
|
+
* identity used for routing/filtering.
|
|
73
|
+
*/
|
|
74
|
+
agentLabel?: string;
|
|
67
75
|
/** Agent-supplied snapshot — machine state/context for stateful agents, undefined otherwise. */
|
|
68
76
|
agentSnapshot?: unknown;
|
|
69
77
|
}
|
|
@@ -89,7 +97,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
89
97
|
private busy = false;
|
|
90
98
|
private pendingInteractions = new Map<
|
|
91
99
|
string,
|
|
92
|
-
{
|
|
100
|
+
{
|
|
101
|
+
resolve: (value: any) => void;
|
|
102
|
+
reject: (reason?: any) => void;
|
|
103
|
+
/** Present when the call requested a chat-input override. */
|
|
104
|
+
overrideId?: string;
|
|
105
|
+
}
|
|
93
106
|
>();
|
|
94
107
|
|
|
95
108
|
private systemPrompt?: SystemPromptInput;
|
|
@@ -108,16 +121,44 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
108
121
|
private toolDefinitionsFactory?: (
|
|
109
122
|
ctx: SystemPromptContext,
|
|
110
123
|
) => ChatToolDefinition[] | Promise<ChatToolDefinition[]>;
|
|
124
|
+
/**
|
|
125
|
+
* Resolved tool handler map used for dispatch. When `toolHandlersFactory` is
|
|
126
|
+
* set, this is overwritten each tool-loop iteration with the factory's output
|
|
127
|
+
* — keeping it in lockstep with `toolDefinitions` so handlers don't have to
|
|
128
|
+
* defend themselves against being dispatched in states where their tool
|
|
129
|
+
* isn't advertised. Folds mutate this in place; `defineStatefulAgent`
|
|
130
|
+
* forbids folds when a factory is set, so the fold-mutation path is
|
|
131
|
+
* unreachable in that case.
|
|
132
|
+
*/
|
|
111
133
|
private toolHandlers: ChatToolHandlers;
|
|
134
|
+
/**
|
|
135
|
+
* Optional per-turn handler-map source. Mirrors `toolDefinitionsFactory` so
|
|
136
|
+
* the LLM-visible tools and the dispatchable handlers can be narrowed in
|
|
137
|
+
* lockstep. Resolved each tool-loop iteration before the LLM call.
|
|
138
|
+
*/
|
|
139
|
+
private toolHandlersFactory?: (
|
|
140
|
+
ctx: SystemPromptContext,
|
|
141
|
+
) => ChatToolHandlers | Promise<ChatToolHandlers>;
|
|
112
142
|
private primerHistory?: ChatMessage[];
|
|
113
143
|
private activeAgentName?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Per-turn display label resolved from the agent's `displayName`. Stamped
|
|
146
|
+
* onto outgoing messages and turn snapshots for UX; `activeAgentName` stays
|
|
147
|
+
* stable for routing/history-transform identity matching.
|
|
148
|
+
*/
|
|
149
|
+
private activeAgentLabel?: string;
|
|
150
|
+
private displayName?: SystemPromptInput;
|
|
114
151
|
/**
|
|
115
152
|
* When set, `requestInteraction` delegates to this callback instead of using
|
|
116
153
|
* this driver's own pending map. Wired by `invokeSubAgent` so a sub-agent's
|
|
117
154
|
* widget renders in — and resolves through — the parent (ultimately the
|
|
118
155
|
* root) driver, where the main UI is listening.
|
|
119
156
|
*/
|
|
120
|
-
private hostInteractionRequester?: <T>(
|
|
157
|
+
private hostInteractionRequester?: <T>(
|
|
158
|
+
componentName: string,
|
|
159
|
+
data: any,
|
|
160
|
+
options?: InteractionRequestOptions,
|
|
161
|
+
) => Promise<T>;
|
|
121
162
|
/**
|
|
122
163
|
* When set (e.g. by OrchestratingDriver), applied only to the conversation slice
|
|
123
164
|
* sent to the model — stored `history` stays unchanged for UI and logging.
|
|
@@ -165,7 +206,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
165
206
|
|
|
166
207
|
constructor(
|
|
167
208
|
private readonly aiProvider: AIProvider,
|
|
168
|
-
toolHandlers:
|
|
209
|
+
toolHandlers: ToolHandlersInput = {},
|
|
169
210
|
toolDefinitions: ToolDefinitionsInput = [],
|
|
170
211
|
systemPrompt?: SystemPromptInput,
|
|
171
212
|
primerHistory?: ChatMessage[],
|
|
@@ -174,7 +215,13 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
174
215
|
maxTurnSnapshots: number = DEFAULT_MAX_TURN_SNAPSHOTS,
|
|
175
216
|
) {
|
|
176
217
|
super();
|
|
177
|
-
|
|
218
|
+
if (typeof toolHandlers === 'function') {
|
|
219
|
+
this.toolHandlersFactory = toolHandlers;
|
|
220
|
+
this.toolHandlers = {};
|
|
221
|
+
} else {
|
|
222
|
+
this.toolHandlersFactory = undefined;
|
|
223
|
+
this.toolHandlers = toolHandlers;
|
|
224
|
+
}
|
|
178
225
|
if (typeof toolDefinitions === 'function') {
|
|
179
226
|
this.toolDefinitionsFactory = toolDefinitions;
|
|
180
227
|
this.toolDefinitions = [];
|
|
@@ -203,9 +250,23 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
203
250
|
this.toolDefinitionsFactory = undefined;
|
|
204
251
|
this.toolDefinitions = config.toolDefinitions ?? [];
|
|
205
252
|
}
|
|
206
|
-
|
|
253
|
+
if (typeof config.toolHandlers === 'function') {
|
|
254
|
+
this.toolHandlersFactory = config.toolHandlers;
|
|
255
|
+
// Cleared each turn by the factory in runToolLoop; empty is safe in the
|
|
256
|
+
// meantime (no LLM call happens before resolution).
|
|
257
|
+
this.toolHandlers = {};
|
|
258
|
+
} else {
|
|
259
|
+
this.toolHandlersFactory = undefined;
|
|
260
|
+
this.toolHandlers = config.toolHandlers ?? {};
|
|
261
|
+
}
|
|
207
262
|
this.primerHistory = config.primerHistory;
|
|
208
263
|
this.activeAgentName = config.name;
|
|
264
|
+
this.displayName = config.displayName;
|
|
265
|
+
// Static string form resolves to a stable label up-front; the function
|
|
266
|
+
// form gets re-resolved each tool-loop iteration. Falls back to the
|
|
267
|
+
// canonical name when displayName is unset.
|
|
268
|
+
this.activeAgentLabel =
|
|
269
|
+
typeof config.displayName === 'string' ? config.displayName : config.name;
|
|
209
270
|
this.debugSnapshotter = config.getDebugSnapshot;
|
|
210
271
|
this.subAgentsMap = new Map((config.subAgents ?? []).map((s) => [s.name, s]));
|
|
211
272
|
// Reset fold state when agent changes — each specialist starts fresh
|
|
@@ -263,6 +324,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
263
324
|
turnIndex,
|
|
264
325
|
timestamp: new Date().toISOString(),
|
|
265
326
|
agentName: this.activeAgentName,
|
|
327
|
+
agentLabel: this.activeAgentLabel,
|
|
266
328
|
systemPrompt: resolvedSystemPrompt,
|
|
267
329
|
toolNames: this.toolDefinitions.map((t) => t.name),
|
|
268
330
|
agentSnapshot,
|
|
@@ -385,7 +447,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
385
447
|
* naturally: a grandchild → child → root.
|
|
386
448
|
*/
|
|
387
449
|
public setHostInteractionRequester(
|
|
388
|
-
fn: <T>(componentName: string, data: any) => Promise<T>,
|
|
450
|
+
fn: <T>(componentName: string, data: any, options?: InteractionRequestOptions) => Promise<T>,
|
|
389
451
|
): void {
|
|
390
452
|
this.hostInteractionRequester = fn;
|
|
391
453
|
}
|
|
@@ -403,10 +465,17 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
403
465
|
*
|
|
404
466
|
* @param componentName - The custom element name to render.
|
|
405
467
|
* @param data - Data to pass to the component.
|
|
468
|
+
* @param options - Optional per-call overrides, including
|
|
469
|
+
* `chatInputDuringExecution` to hide or disable the main chat input while
|
|
470
|
+
* the widget is awaiting user input. Reverts when the interaction resolves.
|
|
406
471
|
*/
|
|
407
|
-
public async requestInteraction<T>(
|
|
472
|
+
public async requestInteraction<T>(
|
|
473
|
+
componentName: string,
|
|
474
|
+
data: any,
|
|
475
|
+
options?: InteractionRequestOptions,
|
|
476
|
+
): Promise<T> {
|
|
408
477
|
if (this.hostInteractionRequester) {
|
|
409
|
-
return this.hostInteractionRequester<T>(componentName, data);
|
|
478
|
+
return this.hostInteractionRequester<T>(componentName, data, options);
|
|
410
479
|
}
|
|
411
480
|
if (this.pendingInteractions.size > 0) {
|
|
412
481
|
throw new Error(
|
|
@@ -416,8 +485,20 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
416
485
|
);
|
|
417
486
|
}
|
|
418
487
|
const interactionId = crypto.randomUUID();
|
|
488
|
+
const chatInputDuringExecution = options?.chatInputDuringExecution;
|
|
419
489
|
return new Promise((resolve, reject) => {
|
|
420
|
-
this.pendingInteractions.set(interactionId, {
|
|
490
|
+
this.pendingInteractions.set(interactionId, {
|
|
491
|
+
resolve,
|
|
492
|
+
reject,
|
|
493
|
+
overrideId: chatInputDuringExecution ? interactionId : undefined,
|
|
494
|
+
});
|
|
495
|
+
if (chatInputDuringExecution) {
|
|
496
|
+
this.dispatchEvent(
|
|
497
|
+
new CustomEvent('interaction-start', {
|
|
498
|
+
detail: { interactionId, chatInputDuringExecution },
|
|
499
|
+
}),
|
|
500
|
+
);
|
|
501
|
+
}
|
|
421
502
|
this.appendToHistory({
|
|
422
503
|
role: 'assistant',
|
|
423
504
|
content: '',
|
|
@@ -445,6 +526,9 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
445
526
|
}),
|
|
446
527
|
);
|
|
447
528
|
}
|
|
529
|
+
if (interaction.overrideId) {
|
|
530
|
+
this.dispatchEvent(new CustomEvent('interaction-stop', { detail: { interactionId } }));
|
|
531
|
+
}
|
|
448
532
|
interaction.resolve(result);
|
|
449
533
|
this.pendingInteractions.delete(interactionId);
|
|
450
534
|
} else {
|
|
@@ -499,8 +583,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
499
583
|
*/
|
|
500
584
|
private buildHandlerContext(traceCapture?: { trace?: ChatMessage[] }) {
|
|
501
585
|
return {
|
|
502
|
-
requestInteraction: <T>(
|
|
503
|
-
|
|
586
|
+
requestInteraction: <T>(
|
|
587
|
+
componentName: string,
|
|
588
|
+
data: any,
|
|
589
|
+
options?: InteractionRequestOptions,
|
|
590
|
+
): Promise<T> => this.requestInteraction(componentName, data, options),
|
|
504
591
|
...(this.subAgentsMap.size > 0 && {
|
|
505
592
|
requestSubAgent: <T = never>(
|
|
506
593
|
name: string,
|
|
@@ -581,8 +668,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
581
668
|
// pending map the main UI is wired to. Recurses naturally for nested
|
|
582
669
|
// sub-agents.
|
|
583
670
|
child.setHostInteractionRequester(
|
|
584
|
-
<R>(componentName: string, data: any): Promise<R> =>
|
|
585
|
-
this.requestInteraction<R>(componentName, data),
|
|
671
|
+
<R>(componentName: string, data: any, opts?: InteractionRequestOptions): Promise<R> =>
|
|
672
|
+
this.requestInteraction<R>(componentName, data, opts),
|
|
586
673
|
);
|
|
587
674
|
|
|
588
675
|
const forwardTrace = (e: Event) => {
|
|
@@ -800,6 +887,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
800
887
|
// Tool loop
|
|
801
888
|
// ---------------------------------------------------------------------------
|
|
802
889
|
|
|
890
|
+
// eslint-disable-next-line complexity
|
|
803
891
|
private async runToolLoop(
|
|
804
892
|
userInput: string,
|
|
805
893
|
attachments?: ChatAttachment[],
|
|
@@ -842,6 +930,14 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
842
930
|
// eslint-disable-next-line no-await-in-loop
|
|
843
931
|
this.toolDefinitions = await this.toolDefinitionsFactory(promptCtx);
|
|
844
932
|
}
|
|
933
|
+
// Same story for the handler-map factory: re-resolve so dispatch sees
|
|
934
|
+
// only the handlers valid for the current state, in lockstep with the
|
|
935
|
+
// tool definitions exposed above. Folds are forbidden when this is set,
|
|
936
|
+
// so the fold-mutation paths on `this.toolHandlers` are unreachable.
|
|
937
|
+
if (this.toolHandlersFactory) {
|
|
938
|
+
// eslint-disable-next-line no-await-in-loop
|
|
939
|
+
this.toolHandlers = await this.toolHandlersFactory(promptCtx);
|
|
940
|
+
}
|
|
845
941
|
|
|
846
942
|
const resolvedSystemPrompt =
|
|
847
943
|
typeof this.systemPrompt === 'function'
|
|
@@ -849,6 +945,15 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
849
945
|
await this.systemPrompt(promptCtx)
|
|
850
946
|
: this.systemPrompt;
|
|
851
947
|
|
|
948
|
+
// Re-resolve the per-turn display label. Falls back to the canonical
|
|
949
|
+
// agent name when displayName is unset. Stamped onto outgoing messages
|
|
950
|
+
// and turn snapshots for UX only; routing/history-transform continues
|
|
951
|
+
// to read `activeAgentName`.
|
|
952
|
+
if (typeof this.displayName === 'function') {
|
|
953
|
+
// eslint-disable-next-line no-await-in-loop
|
|
954
|
+
this.activeAgentLabel = await this.displayName(promptCtx);
|
|
955
|
+
}
|
|
956
|
+
|
|
852
957
|
const foldSuffix = this.buildFoldSystemPromptSuffix();
|
|
853
958
|
const baseSystemPrompt = resolvedSystemPrompt
|
|
854
959
|
? `${resolvedSystemPrompt}${foldSuffix}`
|
|
@@ -1163,7 +1268,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1163
1268
|
|
|
1164
1269
|
private appendToHistory(message: ChatMessage): void {
|
|
1165
1270
|
const tagged: ChatMessage = this.activeAgentName
|
|
1166
|
-
? {
|
|
1271
|
+
? {
|
|
1272
|
+
...message,
|
|
1273
|
+
agentName: this.activeAgentName,
|
|
1274
|
+
// Display-only — falls back to agentName in renderers when unset.
|
|
1275
|
+
agentLabel: this.activeAgentLabel,
|
|
1276
|
+
}
|
|
1167
1277
|
: message;
|
|
1168
1278
|
this.history = [...this.history, tagged];
|
|
1169
1279
|
this.dispatchEvent(
|
|
@@ -156,6 +156,16 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
156
156
|
this.chatDriver.addEventListener('sub-agent-stop', (e: Event) => {
|
|
157
157
|
this.dispatchEvent(new CustomEvent('sub-agent-stop', { detail: (e as CustomEvent).detail }));
|
|
158
158
|
});
|
|
159
|
+
this.chatDriver.addEventListener('interaction-start', (e: Event) => {
|
|
160
|
+
this.dispatchEvent(
|
|
161
|
+
new CustomEvent('interaction-start', { detail: (e as CustomEvent).detail }),
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
this.chatDriver.addEventListener('interaction-stop', (e: Event) => {
|
|
165
|
+
this.dispatchEvent(
|
|
166
|
+
new CustomEvent('interaction-stop', { detail: (e as CustomEvent).detail }),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
resolveInteraction(interactionId: string, result: unknown): void {
|
package/src/config/config.ts
CHANGED
|
@@ -61,6 +61,20 @@ export type ToolDefinitionsInput =
|
|
|
61
61
|
| ChatToolDefinition[]
|
|
62
62
|
| ((ctx: SystemPromptContext) => ChatToolDefinition[] | Promise<ChatToolDefinition[]>);
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Tool handlers for an agent. Either a static map (the conventional shape) or a
|
|
66
|
+
* function resolved each tool-loop iteration. The function form lets the agent
|
|
67
|
+
* narrow the dispatchable handler set per turn — pair it with the function form
|
|
68
|
+
* of `toolDefinitions` so the LLM-visible tools and the dispatchable handlers
|
|
69
|
+
* stay in lockstep, and handlers don't have to defend themselves against being
|
|
70
|
+
* dispatched in states where their tool isn't advertised.
|
|
71
|
+
*
|
|
72
|
+
* @beta
|
|
73
|
+
*/
|
|
74
|
+
export type ToolHandlersInput =
|
|
75
|
+
| ChatToolHandlers
|
|
76
|
+
| ((ctx: SystemPromptContext) => ChatToolHandlers | Promise<ChatToolHandlers>);
|
|
77
|
+
|
|
64
78
|
/**
|
|
65
79
|
* Opts an agent in to manual selection from the assistant's agent picker.
|
|
66
80
|
*
|
|
@@ -83,9 +97,21 @@ export interface ManualSelectionConfig {
|
|
|
83
97
|
|
|
84
98
|
interface BaseAgentConfig {
|
|
85
99
|
/**
|
|
86
|
-
*
|
|
100
|
+
* Stable identity for this agent. Used for classifier routing, manual
|
|
101
|
+
* pinning, and history filtering — must not vary per turn. For a per-turn
|
|
102
|
+
* display label (e.g. "Guided Booking (Counterparties)"), supply
|
|
103
|
+
* {@link BaseAgentConfig.displayName}.
|
|
87
104
|
*/
|
|
88
105
|
name: string;
|
|
106
|
+
/**
|
|
107
|
+
* Optional per-turn display label. Resolved each tool-loop iteration and
|
|
108
|
+
* stamped onto outgoing messages (`agentLabel`) and the debug-log timeline.
|
|
109
|
+
* Renderers fall back to `name` when this is unset. Use the function form
|
|
110
|
+
* to vary the label by current state (e.g. a state machine's step).
|
|
111
|
+
*
|
|
112
|
+
* Identity stays on `name` — this is for UX only.
|
|
113
|
+
*/
|
|
114
|
+
displayName?: SystemPromptInput;
|
|
89
115
|
/**
|
|
90
116
|
* System prompt injected into every conversation turn for this agent.
|
|
91
117
|
*
|
|
@@ -105,8 +131,13 @@ interface BaseAgentConfig {
|
|
|
105
131
|
toolDefinitions?: ToolDefinitionsInput;
|
|
106
132
|
/**
|
|
107
133
|
* Tool handler implementations for this agent.
|
|
134
|
+
*
|
|
135
|
+
* Either a static map or a function resolved each tool-loop iteration —
|
|
136
|
+
* pick the function form to narrow the dispatchable handler set per turn,
|
|
137
|
+
* matching the function form of `toolDefinitions`.
|
|
138
|
+
* See {@link ToolHandlersInput}.
|
|
108
139
|
*/
|
|
109
|
-
toolHandlers?:
|
|
140
|
+
toolHandlers?: ToolHandlersInput;
|
|
110
141
|
/**
|
|
111
142
|
* Optional primer history prepended to every call (not visible to the user).
|
|
112
143
|
* Used to establish agent identity and behavioural rules.
|
|
@@ -6,9 +6,23 @@ import type {
|
|
|
6
6
|
ChatInputDuringExecutionMode,
|
|
7
7
|
ManualSelectionConfig,
|
|
8
8
|
SystemPromptContext,
|
|
9
|
+
SystemPromptInput,
|
|
9
10
|
ToolDefinitionsInput,
|
|
11
|
+
ToolHandlersInput,
|
|
10
12
|
} from './config';
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Context passed to per-turn resolvers on a stateful agent — the standard
|
|
16
|
+
* {@link SystemPromptContext} plus the live `state` value. Used by
|
|
17
|
+
* `systemPrompt`, `displayName`, and the function form of `toolDefinitions`.
|
|
18
|
+
*
|
|
19
|
+
* Exported so consumers can lift resolvers into separate files without
|
|
20
|
+
* re-deriving the shape.
|
|
21
|
+
*
|
|
22
|
+
* @beta
|
|
23
|
+
*/
|
|
24
|
+
export type StatefulAgentContext<S> = SystemPromptContext & { state: S };
|
|
25
|
+
|
|
12
26
|
/**
|
|
13
27
|
* Init options for {@link defineStatefulAgent}. Generic over the state shape `S`
|
|
14
28
|
* the agent owns (a state machine, an observable controller, anything).
|
|
@@ -57,7 +71,15 @@ export interface StatefulAgentInit<S> {
|
|
|
57
71
|
* iteration. Use this to feed the LLM whatever the current state implies
|
|
58
72
|
* (e.g. a state machine's `meta.systemPrompt` plus captured context).
|
|
59
73
|
*/
|
|
60
|
-
systemPrompt?: (ctx:
|
|
74
|
+
systemPrompt?: (ctx: StatefulAgentContext<S>) => string | Promise<string>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Per-turn display label, e.g. "Guided Booking (Counterparties)". Resolved
|
|
78
|
+
* each tool-loop iteration and stamped onto outgoing messages and the
|
|
79
|
+
* debug-log timeline — display only. The agent's `name` stays as the
|
|
80
|
+
* canonical identity used for routing/history filtering.
|
|
81
|
+
*/
|
|
82
|
+
displayName?: (ctx: StatefulAgentContext<S>) => string | Promise<string>;
|
|
61
83
|
|
|
62
84
|
/**
|
|
63
85
|
* Tool definitions the LLM sees. Either a static array (resolved once) or a
|
|
@@ -72,21 +94,27 @@ export interface StatefulAgentInit<S> {
|
|
|
72
94
|
*/
|
|
73
95
|
toolDefinitions?:
|
|
74
96
|
| ChatToolDefinition[]
|
|
75
|
-
| ((
|
|
76
|
-
ctx: SystemPromptContext & { state: S },
|
|
77
|
-
) => ChatToolDefinition[] | Promise<ChatToolDefinition[]>);
|
|
97
|
+
| ((ctx: StatefulAgentContext<S>) => ChatToolDefinition[] | Promise<ChatToolDefinition[]>);
|
|
78
98
|
|
|
79
99
|
/**
|
|
80
|
-
* Factory returning the
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
100
|
+
* Factory returning the handler map for the **current state**. Called each
|
|
101
|
+
* tool-loop iteration with the live `state` value, so the handler set the
|
|
102
|
+
* driver dispatches against matches what `toolDefinitions` exposes to the
|
|
103
|
+
* LLM that turn. Return only the handlers valid right now — no need to
|
|
104
|
+
* advertise every handler the agent might ever expose, and no defensive
|
|
105
|
+
* `if (!machine.matches(...))` guards inside each handler.
|
|
106
|
+
*
|
|
107
|
+
* Pair with the function form of `toolDefinitions` so the visible tools and
|
|
108
|
+
* the dispatchable handlers stay in lockstep.
|
|
84
109
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
110
|
+
* **Constraint:** resolved handlers must not include fold facades. Folds and
|
|
111
|
+
* state-machine-driven tool filtering both try to control the LLM's tool
|
|
112
|
+
* view — pick one. Helper samples once on activation (init state) and
|
|
113
|
+
* throws if a fold-tagged handler is detected; subsequent resolves also
|
|
114
|
+
* validate, so misuse on a non-init state surfaces when that state is
|
|
115
|
+
* reached.
|
|
88
116
|
*/
|
|
89
|
-
toolHandlers?: (state: S) => ChatToolHandlers
|
|
117
|
+
toolHandlers?: (state: S) => ChatToolHandlers | Promise<ChatToolHandlers>;
|
|
90
118
|
|
|
91
119
|
/**
|
|
92
120
|
* Optional getter for the debug-log snapshot. Defaults to auto-snapshotting
|
|
@@ -163,7 +191,6 @@ function defaultStatefulDebugSnapshot(state: unknown): unknown {
|
|
|
163
191
|
*/
|
|
164
192
|
export function defineStatefulAgent<S>(opts: StatefulAgentInit<S>): AgentConfig {
|
|
165
193
|
let state: S | undefined;
|
|
166
|
-
let cachedHandlers: ChatToolHandlers | undefined;
|
|
167
194
|
|
|
168
195
|
const assertNoFolds = (handlers: ChatToolHandlers): void => {
|
|
169
196
|
for (const [name, handler] of Object.entries(handlers)) {
|
|
@@ -194,62 +221,51 @@ export function defineStatefulAgent<S>(opts: StatefulAgentInit<S>): AgentConfig
|
|
|
194
221
|
return td;
|
|
195
222
|
})();
|
|
196
223
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if (!state || !cachedHandlers) {
|
|
207
|
-
throw new Error(`Stateful agent "${opts.name}" handler called before init`);
|
|
208
|
-
}
|
|
209
|
-
const handler = cachedHandlers[name];
|
|
210
|
-
if (!handler) {
|
|
211
|
-
throw new Error(`Tool "${name}" has no handler on stateful agent "${opts.name}"`);
|
|
224
|
+
// Wrap the handler factory into the ctx-shaped form the driver expects, and
|
|
225
|
+
// validate folds on each resolve. The driver re-resolves this per tool-loop
|
|
226
|
+
// iteration, so the handler set the driver dispatches against always
|
|
227
|
+
// matches the LLM-visible tools for the current state — handlers don't need
|
|
228
|
+
// defensive `if (!machine.matches(...))` guards.
|
|
229
|
+
const wrappedHandlers: ToolHandlersInput | undefined = opts.toolHandlers
|
|
230
|
+
? async () => {
|
|
231
|
+
if (!state) {
|
|
232
|
+
throw new Error(`Stateful agent "${opts.name}" handlers called before init`);
|
|
212
233
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// Static-tools case: handler names are knowable from the definitions array.
|
|
220
|
-
// Dynamic-tools case: we discover names from a sample of `toolHandlers(state)`
|
|
221
|
-
// taken inside onActivate. Until then, the handler dict is empty — fine
|
|
222
|
-
// because no tool call can happen before activation completes.
|
|
223
|
-
const staticHandlerProxy =
|
|
224
|
-
Array.isArray(opts.toolDefinitions) && opts.toolHandlers
|
|
225
|
-
? buildHandlerProxy(opts.toolDefinitions.map((d) => d.name))
|
|
226
|
-
: undefined;
|
|
234
|
+
const handlers = await opts.toolHandlers!(state);
|
|
235
|
+
assertNoFolds(handlers);
|
|
236
|
+
return handlers;
|
|
237
|
+
}
|
|
238
|
+
: undefined;
|
|
227
239
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
240
|
+
const wrappedDisplayName: SystemPromptInput | undefined = opts.displayName
|
|
241
|
+
? async (ctx: SystemPromptContext) => {
|
|
242
|
+
if (!state) {
|
|
243
|
+
throw new Error(`Stateful agent "${opts.name}" displayName called before init`);
|
|
244
|
+
}
|
|
245
|
+
return opts.displayName!({ ...ctx, state });
|
|
246
|
+
}
|
|
247
|
+
: undefined;
|
|
231
248
|
|
|
232
249
|
const base = {
|
|
233
250
|
name: opts.name,
|
|
251
|
+
displayName: wrappedDisplayName,
|
|
234
252
|
primerHistory: opts.primerHistory,
|
|
235
253
|
subAgents: opts.subAgents,
|
|
236
254
|
manualSelection: opts.manualSelection,
|
|
237
255
|
chatInputDuringExecution: opts.chatInputDuringExecution,
|
|
238
256
|
toolDefinitions: wrappedTools,
|
|
239
|
-
toolHandlers:
|
|
257
|
+
toolHandlers: wrappedHandlers,
|
|
240
258
|
|
|
241
259
|
onActivate: async (ctx: AgentLifecycleContext) => {
|
|
242
260
|
state = await opts.init(ctx);
|
|
243
|
-
// Sample
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for (const key of Object.keys(resolvedHandlers)) delete resolvedHandlers[key];
|
|
252
|
-
Object.assign(resolvedHandlers, dynamicProxy);
|
|
261
|
+
// Sample once with the init state to fail loud on fold misuse at the
|
|
262
|
+
// agent-switch boundary instead of waiting for the first tool call.
|
|
263
|
+
// The driver re-resolves the factory each iteration, so misuse on
|
|
264
|
+
// handlers gated behind a non-init state surfaces when that state is
|
|
265
|
+
// reached — same loud failure mode, narrower coverage.
|
|
266
|
+
if (opts.toolHandlers) {
|
|
267
|
+
const sampled = await opts.toolHandlers(state);
|
|
268
|
+
assertNoFolds(sampled);
|
|
253
269
|
}
|
|
254
270
|
},
|
|
255
271
|
|
|
@@ -261,7 +277,6 @@ export function defineStatefulAgent<S>(opts: StatefulAgentInit<S>): AgentConfig
|
|
|
261
277
|
// cannot interleave the read-then-write of `state`.
|
|
262
278
|
// eslint-disable-next-line require-atomic-updates
|
|
263
279
|
state = undefined;
|
|
264
|
-
cachedHandlers = undefined;
|
|
265
280
|
},
|
|
266
281
|
|
|
267
282
|
systemPrompt: opts.systemPrompt
|
|
@@ -233,7 +233,7 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
|
|
|
233
233
|
messageType(m) === 'ai-function' &&
|
|
234
234
|
m.agentName &&
|
|
235
235
|
(c.parent as FoundationAiAssistant).showAgentSwitchIndicator
|
|
236
|
-
? `Tool Call · ${m.agentName}`
|
|
236
|
+
? `Tool Call · ${m.agentLabel ?? m.agentName}`
|
|
237
237
|
: senderLabel[messageType(m)]}
|
|
238
238
|
</div>
|
|
239
239
|
<div class="content">
|