@genesislcap/ai-assistant 14.434.0 → 14.436.0

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.
Files changed (33) hide show
  1. package/dist/ai-assistant.api.json +1513 -70
  2. package/dist/ai-assistant.d.ts +367 -7
  3. package/dist/dts/components/ai-driver/ai-driver.d.ts +8 -0
  4. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  5. package/dist/dts/components/chat-driver/chat-driver.d.ts +79 -3
  6. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  7. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +23 -0
  8. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  9. package/dist/dts/config/config.d.ts +106 -2
  10. package/dist/dts/config/config.d.ts.map +1 -1
  11. package/dist/dts/config/define-stateful-agent.d.ts +115 -0
  12. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -0
  13. package/dist/dts/index.d.ts +1 -0
  14. package/dist/dts/index.d.ts.map +1 -1
  15. package/dist/dts/main/main.d.ts +36 -4
  16. package/dist/dts/main/main.d.ts.map +1 -1
  17. package/dist/dts/main/main.template.d.ts.map +1 -1
  18. package/dist/esm/components/chat-driver/chat-driver.js +126 -11
  19. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +192 -33
  20. package/dist/esm/config/define-stateful-agent.js +174 -0
  21. package/dist/esm/index.js +1 -0
  22. package/dist/esm/main/main.js +164 -21
  23. package/dist/esm/main/main.template.js +2 -11
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +16 -16
  26. package/src/components/ai-driver/ai-driver.ts +9 -0
  27. package/src/components/chat-driver/chat-driver.ts +178 -8
  28. package/src/components/orchestrating-driver/orchestrating-driver.ts +191 -17
  29. package/src/config/config.ts +112 -2
  30. package/src/config/define-stateful-agent.ts +293 -0
  31. package/src/index.ts +1 -0
  32. package/src/main/main.template.ts +2 -9
  33. package/src/main/main.ts +167 -14
@@ -7,6 +7,60 @@ import type {
7
7
 
8
8
  export type { ChatInputDuringExecutionMode };
9
9
 
10
+ /**
11
+ * Context passed to `onActivate` / `onDeactivate` lifecycle hooks on an agent.
12
+ *
13
+ * @beta
14
+ */
15
+ export interface AgentLifecycleContext {
16
+ /** The agent the hook is firing for. */
17
+ agentName: string;
18
+ /** The assistant session key — stable across reloads, unique per assistant instance. */
19
+ sessionKey: string;
20
+ /** The agent that was active immediately before this one, if any. Only set on `onActivate`. */
21
+ previousAgentName?: string;
22
+ /** Aborted if the session disconnects mid-activation. */
23
+ signal: AbortSignal;
24
+ }
25
+
26
+ /**
27
+ * Context passed to the function form of `systemPrompt` / `toolDefinitions`.
28
+ * Resolved each tool-loop iteration so the agent can vary what the LLM sees per turn.
29
+ *
30
+ * @beta
31
+ */
32
+ export interface SystemPromptContext {
33
+ /** The active agent's name. */
34
+ agentName: string;
35
+ /** Full conversation history up to (but not including) the message being processed. */
36
+ history: ReadonlyArray<ChatMessage>;
37
+ /** 0 = first LLM call this turn; > 0 = retry or subsequent tool-loop iteration. */
38
+ turnIndex: number;
39
+ /** Aborted if the turn is cancelled. */
40
+ signal: AbortSignal;
41
+ }
42
+
43
+ /**
44
+ * System prompt for an agent. Either a static string (resolved once) or a function
45
+ * resolved each tool-loop iteration. The function form lets the agent compute the
46
+ * prompt from external state — e.g. a state machine's current step.
47
+ *
48
+ * @beta
49
+ */
50
+ export type SystemPromptInput = string | ((ctx: SystemPromptContext) => string | Promise<string>);
51
+
52
+ /**
53
+ * Tool definitions for an agent. Either a static array (the conventional shape) or
54
+ * a function resolved each tool-loop iteration. The function form lets the agent
55
+ * narrow the tool surface per turn — e.g. expose only the tools valid in the
56
+ * current state of a state machine.
57
+ *
58
+ * @beta
59
+ */
60
+ export type ToolDefinitionsInput =
61
+ | ChatToolDefinition[]
62
+ | ((ctx: SystemPromptContext) => ChatToolDefinition[] | Promise<ChatToolDefinition[]>);
63
+
10
64
  /**
11
65
  * Opts an agent in to manual selection from the assistant's agent picker.
12
66
  *
@@ -34,12 +88,21 @@ interface BaseAgentConfig {
34
88
  name: string;
35
89
  /**
36
90
  * System prompt injected into every conversation turn for this agent.
91
+ *
92
+ * Either a string (resolved once) or a function resolved each tool-loop
93
+ * iteration — pick the function form when the prompt depends on per-turn state
94
+ * (e.g. a state machine's current step). See {@link SystemPromptInput}.
37
95
  */
38
- systemPrompt?: string;
96
+ systemPrompt?: SystemPromptInput;
39
97
  /**
40
98
  * Tool definitions (JSON Schema) passed to the AI provider for this agent.
99
+ *
100
+ * Either a static array or a function resolved each tool-loop iteration —
101
+ * pick the function form to narrow the tool surface per turn (e.g. expose
102
+ * only the tools valid in the current state of a state machine).
103
+ * See {@link ToolDefinitionsInput}.
41
104
  */
42
- toolDefinitions?: ChatToolDefinition[];
105
+ toolDefinitions?: ToolDefinitionsInput;
43
106
  /**
44
107
  * Tool handler implementations for this agent.
45
108
  */
@@ -64,6 +127,40 @@ interface BaseAgentConfig {
64
127
  * disabled. See {@link ManualSelectionConfig}.
65
128
  */
66
129
  manualSelection?: ManualSelectionConfig;
130
+ /**
131
+ * Fires when this agent becomes the active specialist (including being pinned).
132
+ * Use to instantiate per-agent state that should outlive a single turn — e.g.
133
+ * a state machine the agent owns across the conversation. Awaited before the
134
+ * agent's first turn runs.
135
+ *
136
+ * Capture dependencies (services, redux store refs, etc.) via closure on the
137
+ * host element rather than expecting them on the context.
138
+ *
139
+ * @beta
140
+ */
141
+ onActivate?: (ctx: AgentLifecycleContext) => void | Promise<void>;
142
+ /**
143
+ * Fires when this agent is being deactivated (the user switches to another
144
+ * agent, the session is torn down, or the agent is unpinned and replaced).
145
+ * Use to dispose per-agent state created in `onActivate`. Awaited before the
146
+ * incoming agent's `onActivate` runs.
147
+ *
148
+ * @beta
149
+ */
150
+ onDeactivate?: (ctx: AgentLifecycleContext) => void | Promise<void>;
151
+ /**
152
+ * Returns an agent-supplied debug payload for the export log. Called once per
153
+ * LLM call by the driver (alongside the resolved prompt and tool list) and
154
+ * once at log-export time for the latest snapshot. Stateful agents should
155
+ * return their machine state and captured context so the exported timeline
156
+ * captures what drove each turn.
157
+ *
158
+ * Return value must be JSON-serializable. {@link defineStatefulAgent} wires a
159
+ * sensible default that snapshots any machine-shaped state automatically.
160
+ *
161
+ * @beta
162
+ */
163
+ getDebugSnapshot?: () => unknown;
67
164
  }
68
165
 
69
166
  /**
@@ -82,6 +179,19 @@ export interface SpecialistAgentConfig extends BaseAgentConfig {
82
179
  */
83
180
  description: string;
84
181
  fallback?: never;
182
+ /**
183
+ * When `true`, the classifier never auto-routes to this agent. The user can
184
+ * still select it manually via the picker, provided both prerequisites are
185
+ * met: this agent has `manualSelection.enabled` set to `true`, *and* the
186
+ * assistant has the picker enabled via `chatConfig.picker.mode`.
187
+ *
188
+ * Use this for agents that overlap heavily with a sibling (e.g. a guided
189
+ * wizard sitting next to a free-form agent in the same domain) and should
190
+ * only be reached intentionally rather than via classifier routing.
191
+ *
192
+ * @beta
193
+ */
194
+ excludeFromClassifier?: boolean;
85
195
  }
86
196
 
87
197
  /**
@@ -0,0 +1,293 @@
1
+ import type { ChatMessage, ChatToolDefinition, ChatToolHandlers } from '@genesislcap/foundation-ai';
2
+ import { TOOL_FOLD_SYMBOL } from '../utils/tool-fold';
3
+ import type {
4
+ AgentConfig,
5
+ AgentLifecycleContext,
6
+ ChatInputDuringExecutionMode,
7
+ ManualSelectionConfig,
8
+ SystemPromptContext,
9
+ ToolDefinitionsInput,
10
+ } from './config';
11
+
12
+ /**
13
+ * Init options for {@link defineStatefulAgent}. Generic over the state shape `S`
14
+ * the agent owns (a state machine, an observable controller, anything).
15
+ *
16
+ * The helper threads `state` through `systemPrompt`, `toolDefinitions`, and the
17
+ * `toolHandlers` factory so consumer code never has to reach for a closure or
18
+ * a module-level mutable.
19
+ *
20
+ * @beta
21
+ */
22
+ export interface StatefulAgentInit<S> {
23
+ /** Display name — must be unique within the agents array. */
24
+ name: string;
25
+ /** Plain-language description used by the classifier. Required: stateful agents are always specialists. */
26
+ description: string;
27
+ /**
28
+ * Hide this agent from the classifier — only reachable via manual pinning.
29
+ * Stateful agents are always specialists; they cannot be the fallback (a
30
+ * fallback is a leaf invoked when no specialist matches, with no flow to
31
+ * own state for).
32
+ */
33
+ excludeFromClassifier?: boolean;
34
+ /** Static primer history prepended to every call (not visible to the user). */
35
+ primerHistory?: ChatMessage[];
36
+ /** Sub-agents available to this agent's tool handlers via `requestSubAgent`. */
37
+ subAgents?: AgentConfig[];
38
+ /** Opt this agent in to manual picker selection. */
39
+ manualSelection?: ManualSelectionConfig;
40
+ /** How the main chat input behaves while this agent is executing. */
41
+ chatInputDuringExecution?: ChatInputDuringExecutionMode;
42
+
43
+ /**
44
+ * Construct the agent-scoped state. Called by the framework when this agent
45
+ * becomes active (`onActivate`). Awaited before the agent's first turn runs.
46
+ */
47
+ init: (ctx: AgentLifecycleContext) => S | Promise<S>;
48
+
49
+ /**
50
+ * Tear down the agent-scoped state. Called when the agent is being replaced.
51
+ * Awaited before the incoming agent's `init` runs.
52
+ */
53
+ dispose?: (ctx: AgentLifecycleContext & { state: S }) => void | Promise<void>;
54
+
55
+ /**
56
+ * System prompt composed from current state. Resolved each tool-loop
57
+ * iteration. Use this to feed the LLM whatever the current state implies
58
+ * (e.g. a state machine's `meta.systemPrompt` plus captured context).
59
+ */
60
+ systemPrompt?: (ctx: SystemPromptContext & { state: S }) => string | Promise<string>;
61
+
62
+ /**
63
+ * Tool definitions the LLM sees. Either a static array (resolved once) or a
64
+ * function resolved each tool-loop iteration. The function form is how a
65
+ * machine-driven agent narrows the surface per state.
66
+ *
67
+ * **Constraint:** the resolved handlers (returned from {@link StatefulAgentInit.toolHandlers})
68
+ * must not include fold facades. Folds are an LLM-driven UX optimisation
69
+ * that competes with the machine for control of the tool view; one of them
70
+ * has to be in charge. Helper throws at init time if a fold-tagged handler
71
+ * is detected.
72
+ */
73
+ toolDefinitions?:
74
+ | ChatToolDefinition[]
75
+ | ((
76
+ ctx: SystemPromptContext & { state: S },
77
+ ) => ChatToolDefinition[] | Promise<ChatToolDefinition[]>);
78
+
79
+ /**
80
+ * Factory returning the tool handler map. Called with the live `state` at
81
+ * `onActivate` time; the handlers it returns are cached for the lifetime of
82
+ * this activation. Each handler closes over `state` and any other deps you
83
+ * captured.
84
+ *
85
+ * The handler set returned must be the **union** of every tool name your
86
+ * agent might advertise across all states — `toolDefinitions` filters which
87
+ * are visible to the LLM per turn, but every name still needs an entry here.
88
+ */
89
+ toolHandlers?: (state: S) => ChatToolHandlers;
90
+
91
+ /**
92
+ * Optional getter for the debug-log snapshot. Defaults to auto-snapshotting
93
+ * any property on `state` that looks like a foundation-state-machine
94
+ * instance (has `state`, `context`, and `complete` fields). Override when
95
+ * the default doesn't capture what you need — e.g. multiple machines, or
96
+ * non-machine state worth recording.
97
+ *
98
+ * Return value must be JSON-serializable.
99
+ */
100
+ getDebugSnapshot?: (state: S) => unknown;
101
+ }
102
+
103
+ /**
104
+ * Walk a state object and snapshot any machine-shaped values found on it.
105
+ * Recognises foundation-state-machine instances by structural shape rather
106
+ * than `instanceof`, so the helper stays free of a runtime dep on
107
+ * foundation-state-machine.
108
+ *
109
+ * Strips the `errors`/`error` framework noise from each machine's context —
110
+ * these are foundation-state-machine internals (an ErrorMap instance and its
111
+ * last-error pointer) that bloat the debug payload without aiding debugging.
112
+ */
113
+ function defaultStatefulDebugSnapshot(state: unknown): unknown {
114
+ if (!state || typeof state !== 'object') return state;
115
+ const result: Record<string, unknown> = {};
116
+ for (const [key, val] of Object.entries(state as Record<string, unknown>)) {
117
+ if (val && typeof val === 'object' && 'state' in val && 'context' in val && 'complete' in val) {
118
+ const m = val as {
119
+ state: unknown;
120
+ context: Record<string, unknown>;
121
+ complete: unknown;
122
+ output?: unknown;
123
+ };
124
+ const ctx = m.context ?? {};
125
+ const { errors: _e, error: _err, ...userContext } = ctx;
126
+ result[key] = {
127
+ state: m.state,
128
+ context: userContext,
129
+ complete: m.complete,
130
+ output: m.output,
131
+ };
132
+ }
133
+ }
134
+ return Object.keys(result).length
135
+ ? result
136
+ : '<no auto-snapshot — state has no machine-shaped properties; provide getDebugSnapshot>';
137
+ }
138
+
139
+ /**
140
+ * Build an `AgentConfig` whose `systemPrompt`, `toolDefinitions`, and tool
141
+ * handlers all close over a long-lived state object created on activation.
142
+ *
143
+ * The framework wires the lifecycle: `init` on `onActivate`, `dispose` on
144
+ * `onDeactivate`. State is held inside the helper's closure — never exposed on
145
+ * the resulting `AgentConfig` — so the redux serializer doesn't see it.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * const guidedBooking = defineStatefulAgent<{ machine: GuidedBookingMachine }>({
150
+ * name: 'Guided Booking',
151
+ * description: 'Books a trade via a guided wizard.',
152
+ * excludeFromClassifier: true,
153
+ * manualSelection: { enabled: true, hint: 'Step-by-step trade booking' },
154
+ * init: () => ({ machine: new GuidedBookingMachine() }),
155
+ * dispose: ({ state }) => state.machine.stop(),
156
+ * systemPrompt: ({ state }) => composeFromMachine(state.machine),
157
+ * toolDefinitions: ({ state }) => toolsForState(state.machine.state),
158
+ * toolHandlers: ({ machine }) => ({ ... }),
159
+ * });
160
+ * ```
161
+ *
162
+ * @beta
163
+ */
164
+ export function defineStatefulAgent<S>(opts: StatefulAgentInit<S>): AgentConfig {
165
+ let state: S | undefined;
166
+ let cachedHandlers: ChatToolHandlers | undefined;
167
+
168
+ const assertNoFolds = (handlers: ChatToolHandlers): void => {
169
+ for (const [name, handler] of Object.entries(handlers)) {
170
+ if ((handler as any)[TOOL_FOLD_SYMBOL]) {
171
+ throw new Error(
172
+ `Stateful agent "${opts.name}" tool "${name}" carries fold metadata. ` +
173
+ `Folds and state-machine-driven tool filtering both try to control the ` +
174
+ `LLM's tool view — pick one. Remove the fold or migrate this agent to ` +
175
+ `a non-stateful AgentConfig.`,
176
+ );
177
+ }
178
+ }
179
+ };
180
+
181
+ // Wrap the optional dynamic-tools factory so it threads `state` in and asserts
182
+ // we never accidentally resolved a fold-tagged tool. Static arrays are passed
183
+ // through unchanged.
184
+ const wrappedTools: ToolDefinitionsInput | undefined = (() => {
185
+ const td = opts.toolDefinitions;
186
+ if (typeof td === 'function') {
187
+ return async (ctx: SystemPromptContext) => {
188
+ if (!state) {
189
+ throw new Error(`Stateful agent "${opts.name}" tools called before init`);
190
+ }
191
+ return td({ ...ctx, state });
192
+ };
193
+ }
194
+ return td;
195
+ })();
196
+
197
+ // Each proxy entry calls into `cachedHandlers`, which is populated eagerly
198
+ // inside `onActivate` (see below). Eager population means the no-folds
199
+ // assertion fires at activation time — not on first tool call — so a
200
+ // misconfigured agent fails loud and immediately instead of silently
201
+ // appearing to work until the LLM happens to invoke a tool.
202
+ const buildHandlerProxy = (names: readonly string[]): ChatToolHandlers => {
203
+ const out: ChatToolHandlers = {};
204
+ for (const name of names) {
205
+ out[name] = async (args, ctx) => {
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}"`);
212
+ }
213
+ return handler(args, ctx);
214
+ };
215
+ }
216
+ return out;
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;
227
+
228
+ // For the dynamic case we patch this object in place on activation. The
229
+ // driver reads `toolHandlers` by reference, so mutating in place is enough.
230
+ const resolvedHandlers: ChatToolHandlers = staticHandlerProxy ?? {};
231
+
232
+ const base = {
233
+ name: opts.name,
234
+ primerHistory: opts.primerHistory,
235
+ subAgents: opts.subAgents,
236
+ manualSelection: opts.manualSelection,
237
+ chatInputDuringExecution: opts.chatInputDuringExecution,
238
+ toolDefinitions: wrappedTools,
239
+ toolHandlers: opts.toolHandlers ? resolvedHandlers : undefined,
240
+
241
+ onActivate: async (ctx: AgentLifecycleContext) => {
242
+ state = await opts.init(ctx);
243
+ // Sample handlers eagerly and validate up-front. Throws here are visible
244
+ // at the agent-switch boundary instead of buried inside a future tool
245
+ // dispatch.
246
+ cachedHandlers = opts.toolHandlers?.(state) ?? {};
247
+ assertNoFolds(cachedHandlers);
248
+ // Dynamic-tools path: handler names are discovered from the sample.
249
+ if (!staticHandlerProxy && opts.toolHandlers) {
250
+ const dynamicProxy = buildHandlerProxy(Object.keys(cachedHandlers));
251
+ for (const key of Object.keys(resolvedHandlers)) delete resolvedHandlers[key];
252
+ Object.assign(resolvedHandlers, dynamicProxy);
253
+ }
254
+ },
255
+
256
+ onDeactivate: async (ctx: AgentLifecycleContext) => {
257
+ if (state !== undefined && opts.dispose) {
258
+ await opts.dispose({ ...ctx, state });
259
+ }
260
+ // Orchestrator serializes onActivate/onDeactivate; concurrent calls
261
+ // cannot interleave the read-then-write of `state`.
262
+ // eslint-disable-next-line require-atomic-updates
263
+ state = undefined;
264
+ cachedHandlers = undefined;
265
+ },
266
+
267
+ systemPrompt: opts.systemPrompt
268
+ ? async (ctx: SystemPromptContext) => {
269
+ if (!state) {
270
+ throw new Error(`Stateful agent "${opts.name}" systemPrompt called before init`);
271
+ }
272
+ return opts.systemPrompt!({ ...ctx, state });
273
+ }
274
+ : undefined,
275
+
276
+ // Called per LLM call by ChatDriver, and once at debug-log export time.
277
+ // Returns `undefined` before activation (state hasn't been built yet) and
278
+ // after deactivation (state was disposed) — the snapshotter is best-effort
279
+ // and the driver tolerates undefined.
280
+ getDebugSnapshot: () => {
281
+ if (!state) return undefined;
282
+ return opts.getDebugSnapshot
283
+ ? opts.getDebugSnapshot(state)
284
+ : defaultStatefulDebugSnapshot(state);
285
+ },
286
+ };
287
+
288
+ return {
289
+ ...base,
290
+ description: opts.description,
291
+ excludeFromClassifier: opts.excludeFromClassifier,
292
+ } as AgentConfig;
293
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export * from './components/popout-manager';
9
9
  export * from './channel/ai-activity-channel';
10
10
  export * from './channel/ai-activity-bus';
11
11
  export * from './config/config';
12
+ export * from './config/define-stateful-agent';
12
13
  export * from './config/fallback-agents';
13
14
  export * from './utils/tool-fold';
14
15
  export type { AiChatWidget } from './types/ai-chat-widget';
@@ -275,15 +275,8 @@ ${(tc) => (tc.foldPath?.length ? `${tc.foldPath.join(' › ')} › ` : '')}<stro
275
275
  class="agent-toggle-button"
276
276
  part="agent-toggle-button"
277
277
  appearance="stealth"
278
- title=${(x) =>
279
- x.agentPickerOpen
280
- ? 'Close agent picker'
281
- : x.pinnedAgentName !== null
282
- ? x.pinnedAgentHint
283
- ? `${x.pinnedAgentName} — ${x.pinnedAgentHint}`
284
- : (x.pinnedAgentName ?? '')
285
- : 'Attempts to route messages to the correct available agent. Click to manually pin an agent.'}
286
- ?disabled=${(x) => x.state === 'loading'}
278
+ title=${(x) => x.agentToggleTitle}
279
+ ?disabled=${(x) => x.state === 'loading' || x.pinLocked}
287
280
  @click=${(x) => x.toggleAgentPicker()}
288
281
  >
289
282
  ${when(