@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.
- package/dist/ai-assistant.api.json +1513 -70
- package/dist/ai-assistant.d.ts +367 -7
- package/dist/dts/components/ai-driver/ai-driver.d.ts +8 -0
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +79 -3
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +23 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +106 -2
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/config/define-stateful-agent.d.ts +115 -0
- package/dist/dts/config/define-stateful-agent.d.ts.map +1 -0
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +36 -4
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +126 -11
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +192 -33
- package/dist/esm/config/define-stateful-agent.js +174 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/main/main.js +164 -21
- package/dist/esm/main/main.template.js +2 -11
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +16 -16
- package/src/components/ai-driver/ai-driver.ts +9 -0
- package/src/components/chat-driver/chat-driver.ts +178 -8
- package/src/components/orchestrating-driver/orchestrating-driver.ts +191 -17
- package/src/config/config.ts +112 -2
- package/src/config/define-stateful-agent.ts +293 -0
- package/src/index.ts +1 -0
- package/src/main/main.template.ts +2 -9
- package/src/main/main.ts +167 -14
package/src/config/config.ts
CHANGED
|
@@ -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?:
|
|
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?:
|
|
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
|
-
|
|
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(
|