@genesislcap/ai-assistant 14.494.0 → 14.496.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 +588 -214
- package/dist/ai-assistant.d.ts +157 -6
- package/dist/chat-driver.cjs +5608 -0
- package/dist/chat-driver.cjs.map +7 -0
- package/dist/chat-driver.mjs +5566 -0
- package/dist/chat-driver.mjs.map +7 -0
- package/dist/custom-elements.json +713 -333
- package/dist/dts/channel/ai-activity-bus.d.ts +36 -0
- package/dist/dts/channel/ai-activity-bus.d.ts.map +1 -1
- package/dist/dts/channel/ai-activity-channel.d.ts +10 -1
- package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
- package/dist/dts/chat-driver-node.d.ts +28 -0
- package/dist/dts/chat-driver-node.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/chat-driver.d.ts +66 -5
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.test.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +3 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +37 -2
- package/dist/dts/config/config.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.map +1 -1
- package/dist/dts/state/debug-event-log.d.ts +8 -12
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/esm/channel/ai-activity-bus.js +48 -12
- package/dist/esm/chat-driver-node.js +33 -0
- package/dist/esm/components/chat-driver/chat-driver.js +77 -25
- package/dist/esm/components/chat-driver/chat-driver.test.js +160 -36
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +7 -1
- package/dist/esm/main/main.js +8 -1
- package/dist/esm/main/popout-interaction-gate.test.js +6 -10
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +29 -19
- package/scripts/build-chat-driver-node.mjs +42 -0
- package/src/channel/ai-activity-bus.ts +64 -10
- package/src/channel/ai-activity-channel.ts +9 -1
- package/src/chat-driver-node.ts +54 -0
- package/src/components/chat-driver/chat-driver.test.ts +237 -50
- package/src/components/chat-driver/chat-driver.ts +151 -39
- package/src/components/orchestrating-driver/orchestrating-driver.ts +10 -11
- package/src/config/config.ts +40 -1
- package/src/index.ts +1 -0
- package/src/main/main.ts +8 -11
- package/src/main/popout-interaction-gate.test.ts +6 -11
- package/src/state/debug-event-log.ts +9 -18
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
CachePolicy,
|
|
6
6
|
ChatAttachment,
|
|
7
7
|
ChatDriverResult,
|
|
8
|
+
ChatFallback,
|
|
8
9
|
ChatMessage,
|
|
9
10
|
ChatRequestOptions,
|
|
10
11
|
ChatToolCall,
|
|
@@ -17,17 +18,19 @@ import type {
|
|
|
17
18
|
InteractionResult,
|
|
18
19
|
SubAgentFailureReason,
|
|
19
20
|
SubAgentRequestOptions,
|
|
21
|
+
TurnFailureReason,
|
|
20
22
|
} from '@genesislcap/foundation-ai';
|
|
21
23
|
import {
|
|
22
24
|
isObservableAIProviderRegistry,
|
|
23
25
|
MalformedFunctionCallError,
|
|
24
26
|
ResponseTruncatedError,
|
|
25
27
|
} from '@genesislcap/foundation-ai';
|
|
26
|
-
import {
|
|
28
|
+
import { type ActivityBus, NOOP_ACTIVITY_BUS } from '../../channel/ai-activity-bus';
|
|
27
29
|
import type {
|
|
28
30
|
AgentConfig,
|
|
29
31
|
CachePolicyInput,
|
|
30
32
|
ProviderInput,
|
|
33
|
+
ResponseSchemaInput,
|
|
31
34
|
SystemPromptContext,
|
|
32
35
|
SystemPromptInput,
|
|
33
36
|
TailContextInput,
|
|
@@ -191,6 +194,39 @@ interface FoldStackFrame {
|
|
|
191
194
|
previousHandlers: ChatToolHandlers;
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Construction-time configuration for {@link ChatDriver}. Everything except the provider
|
|
199
|
+
* registry is optional — most fields are also settable per-agent via `applyAgent`, so a
|
|
200
|
+
* bare `new ChatDriver(registry)` is valid. Mirrors the `(registry, options)` shape of
|
|
201
|
+
* `OrchestratingDriver`.
|
|
202
|
+
*
|
|
203
|
+
* @beta
|
|
204
|
+
*/
|
|
205
|
+
export interface ChatDriverConfig {
|
|
206
|
+
/** Initial tool handlers (static map or per-turn factory). Default `{}`. */
|
|
207
|
+
toolHandlers?: ToolHandlersInput;
|
|
208
|
+
/** Initial tool definitions (static array or per-turn factory). Default `[]`. */
|
|
209
|
+
toolDefinitions?: ToolDefinitionsInput;
|
|
210
|
+
/** Initial system prompt (string or per-turn resolver). */
|
|
211
|
+
systemPrompt?: SystemPromptInput;
|
|
212
|
+
/** Primer history prepended to the conversation. */
|
|
213
|
+
primerHistory?: ChatMessage[];
|
|
214
|
+
/** Hard cap on tool-loop iterations. Default `50`. */
|
|
215
|
+
maxToolIterations?: number;
|
|
216
|
+
/** Hard cap on fold operations. Default `5`. */
|
|
217
|
+
maxFoldOperations?: number;
|
|
218
|
+
/** Ring-buffer size for per-turn snapshots. Default `400`. */
|
|
219
|
+
maxTurnSnapshots?: number;
|
|
220
|
+
/** Session identity used to file meta events onto the shared debug-log timeline. */
|
|
221
|
+
sessionKey?: string;
|
|
222
|
+
/**
|
|
223
|
+
* Activity bus for lifecycle/halo/tool-loop events. Injected by the browser host
|
|
224
|
+
* (the shared cross-tab singleton); omitted off-browser (Node, tests, headless), where
|
|
225
|
+
* it defaults to {@link NOOP_ACTIVITY_BUS} so no `BroadcastChannel` is ever opened.
|
|
226
|
+
*/
|
|
227
|
+
activityBus?: ActivityBus;
|
|
228
|
+
}
|
|
229
|
+
|
|
194
230
|
/**
|
|
195
231
|
* Plain TS class that drives a multi-turn chat conversation, including the tool-call loop.
|
|
196
232
|
* Owned by `FoundationAiAssistant` — created in `connectedCallback`, torn down in `disconnectedCallback`.
|
|
@@ -453,6 +489,16 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
453
489
|
* the resolved string in a `<system-reminder>` marker and injects it at the message tail.
|
|
454
490
|
*/
|
|
455
491
|
private activeTailContextInput?: TailContextInput;
|
|
492
|
+
/**
|
|
493
|
+
* Active agent's structured-output schema selector (static value or per-turn resolver).
|
|
494
|
+
* When it resolves to a schema, the model's final answer is constrained to it this turn.
|
|
495
|
+
*/
|
|
496
|
+
private activeResponseSchemaInput?: ResponseSchemaInput;
|
|
497
|
+
/**
|
|
498
|
+
* Active agent's refusal-fallback chain (static). Passed through to the provider so a refused
|
|
499
|
+
* turn (e.g. Fable 5) is re-run on the next model server-side.
|
|
500
|
+
*/
|
|
501
|
+
private activeFallbacks?: ChatFallback[];
|
|
456
502
|
/**
|
|
457
503
|
* Active agent's unresolved-tool hook, captured from `applyAgent`. Consulted
|
|
458
504
|
* only when a tool call cannot be dispatched (a stale or hallucinated name);
|
|
@@ -498,19 +544,32 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
498
544
|
*/
|
|
499
545
|
private unsubscribeRegistry?: () => void;
|
|
500
546
|
|
|
547
|
+
/** Hard cap on tool-loop iterations. */
|
|
548
|
+
private readonly maxToolIterations: number;
|
|
549
|
+
/** Session identity used to file meta events onto the shared debug-log timeline. */
|
|
550
|
+
private readonly sessionKey: string;
|
|
551
|
+
/** Injected activity bus; defaults to a no-op off-browser (Node/tests/headless). */
|
|
552
|
+
private readonly activityBus: ActivityBus;
|
|
553
|
+
|
|
501
554
|
constructor(
|
|
502
555
|
private readonly providerRegistry: AIProviderRegistry,
|
|
503
|
-
|
|
504
|
-
toolDefinitions: ToolDefinitionsInput = [],
|
|
505
|
-
systemPrompt?: SystemPromptInput,
|
|
506
|
-
primerHistory?: ChatMessage[],
|
|
507
|
-
private readonly maxToolIterations: number = DEFAULT_MAX_TOOL_ITERATIONS,
|
|
508
|
-
maxFoldOperations: number = DEFAULT_MAX_FOLD_OPERATIONS,
|
|
509
|
-
maxTurnSnapshots: number = DEFAULT_MAX_TURN_SNAPSHOTS,
|
|
510
|
-
/** Session identity used to file meta events onto the shared debug-log timeline. */
|
|
511
|
-
private readonly sessionKey: string = '',
|
|
556
|
+
config: ChatDriverConfig = {},
|
|
512
557
|
) {
|
|
513
558
|
super();
|
|
559
|
+
const {
|
|
560
|
+
toolHandlers = {},
|
|
561
|
+
toolDefinitions = [],
|
|
562
|
+
systemPrompt,
|
|
563
|
+
primerHistory,
|
|
564
|
+
maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS,
|
|
565
|
+
maxFoldOperations = DEFAULT_MAX_FOLD_OPERATIONS,
|
|
566
|
+
maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
|
|
567
|
+
sessionKey = '',
|
|
568
|
+
activityBus = NOOP_ACTIVITY_BUS,
|
|
569
|
+
} = config;
|
|
570
|
+
this.maxToolIterations = maxToolIterations;
|
|
571
|
+
this.sessionKey = sessionKey;
|
|
572
|
+
this.activityBus = activityBus;
|
|
514
573
|
if (typeof toolHandlers === 'function') {
|
|
515
574
|
this.toolHandlersFactory = toolHandlers;
|
|
516
575
|
this.toolHandlers = {};
|
|
@@ -631,6 +690,35 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
631
690
|
return { reason: 'done' };
|
|
632
691
|
}
|
|
633
692
|
|
|
693
|
+
/**
|
|
694
|
+
* Build the `done` loop result, carrying the typed failure reason when the turn
|
|
695
|
+
* bailed (PTC-0). The discriminant stays `'done'` either way — the same value a
|
|
696
|
+
* clean turn returns — so consumers matching on `reason === 'done'` are unchanged;
|
|
697
|
+
* `failureReason` is simply present on a failure and absent on success. Omitted
|
|
698
|
+
* (rather than set to `undefined`) so a happy-path result stays byte-identical to
|
|
699
|
+
* the historical `{ reason: 'done' }`.
|
|
700
|
+
*/
|
|
701
|
+
private turnDone(failureReason?: TurnFailureReason): ChatDriverResult {
|
|
702
|
+
return failureReason ? { reason: 'done', failureReason } : { reason: 'done' };
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
|
|
706
|
+
private static failureReasonOf(result: ChatDriverResult): TurnFailureReason | undefined {
|
|
707
|
+
return result.reason === 'done' ? result.failureReason : undefined;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Build the `tool-loop-end` event detail for a turn's result. A failure carries a
|
|
712
|
+
* `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
|
|
713
|
+
* kept byte-identical so subscribers see exactly what they always have.
|
|
714
|
+
*/
|
|
715
|
+
private static loopEndDetail(
|
|
716
|
+
result: ChatDriverResult,
|
|
717
|
+
): { failureReason: TurnFailureReason } | undefined {
|
|
718
|
+
const failureReason = ChatDriver.failureReasonOf(result);
|
|
719
|
+
return failureReason ? { failureReason } : undefined;
|
|
720
|
+
}
|
|
721
|
+
|
|
634
722
|
/**
|
|
635
723
|
* Swap in a new agent's configuration. Called by OrchestratingDriver before
|
|
636
724
|
* each specialist turn so the shared driver runs with the right tools and prompt.
|
|
@@ -675,6 +763,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
675
763
|
this.activeToolChoiceInput = config.toolChoice;
|
|
676
764
|
this.activeCachePolicyInput = config.cachePolicy;
|
|
677
765
|
this.activeTailContextInput = config.tailContext;
|
|
766
|
+
this.activeResponseSchemaInput = config.responseSchema;
|
|
767
|
+
this.activeFallbacks = config.fallbacks;
|
|
678
768
|
this.activeOnUnresolvedTool = config.onUnresolvedTool;
|
|
679
769
|
this.resolvedProviderCache.clear();
|
|
680
770
|
this.lastResolvedProviderName = undefined;
|
|
@@ -1267,7 +1357,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1267
1357
|
// "actively computing" from "parked awaiting the user" — the latter is a
|
|
1268
1358
|
// safe window for actions disallowed mid-request (e.g. switching provider
|
|
1269
1359
|
// during a long journey step). Paired with `interaction-resolved`.
|
|
1270
|
-
|
|
1360
|
+
this.activityBus.publish('interaction-requested', undefined);
|
|
1271
1361
|
if (chatInputDuringExecution) {
|
|
1272
1362
|
this.dispatchEvent(
|
|
1273
1363
|
new CustomEvent('interaction-start', {
|
|
@@ -1342,7 +1432,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1342
1432
|
// The park is ending and the loop is about to resume computing — paired
|
|
1343
1433
|
// with `interaction-requested`. Fires for every resolution path (user
|
|
1344
1434
|
// completion, timeout, cancellation), since all route through here.
|
|
1345
|
-
|
|
1435
|
+
this.activityBus.publish('interaction-resolved', undefined);
|
|
1346
1436
|
interaction.resolve(result);
|
|
1347
1437
|
this.pendingInteractions.delete(interactionId);
|
|
1348
1438
|
// Tear down the live context on RESOLVE (not on element unmount) — this closes
|
|
@@ -1410,10 +1500,13 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1410
1500
|
phase: 'sendMessage',
|
|
1411
1501
|
agent: this.activeAgentName,
|
|
1412
1502
|
});
|
|
1413
|
-
|
|
1503
|
+
this.activityBus.publish('tool-loop-start', undefined);
|
|
1414
1504
|
|
|
1505
|
+
// Captured so the `finally` can carry the turn's outcome onto `tool-loop-end`.
|
|
1506
|
+
let result: ChatDriverResult = { reason: 'done' };
|
|
1415
1507
|
try {
|
|
1416
|
-
|
|
1508
|
+
result = await this.runToolLoop(userInput, attachments);
|
|
1509
|
+
return result;
|
|
1417
1510
|
} catch (e) {
|
|
1418
1511
|
logger.error('ChatDriver error:', e);
|
|
1419
1512
|
recordTurnError(this.sessionKey, 'exception', {
|
|
@@ -1427,7 +1520,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1427
1520
|
role: 'assistant',
|
|
1428
1521
|
content: 'Sorry, something went wrong on my end. Please try again in a moment.',
|
|
1429
1522
|
});
|
|
1430
|
-
|
|
1523
|
+
result = this.turnDone('exception');
|
|
1524
|
+
return result;
|
|
1431
1525
|
} finally {
|
|
1432
1526
|
recordMetaEvent(this.sessionKey, 'turn.end', {
|
|
1433
1527
|
phase: 'sendMessage',
|
|
@@ -1436,7 +1530,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1436
1530
|
});
|
|
1437
1531
|
this.busy = false;
|
|
1438
1532
|
this.endTurn();
|
|
1439
|
-
|
|
1533
|
+
this.activityBus.publish('tool-loop-end', ChatDriver.loopEndDetail(result));
|
|
1440
1534
|
}
|
|
1441
1535
|
}
|
|
1442
1536
|
|
|
@@ -1602,17 +1696,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1602
1696
|
// be harvested into THIS session on completion and then discarded.
|
|
1603
1697
|
const invocationId = crypto.randomUUID();
|
|
1604
1698
|
const childSessionKey = `${this.sessionKey}::sub:${invocationId}`;
|
|
1605
|
-
const child = new ChatDriver(
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
undefined,
|
|
1612
|
-
undefined,
|
|
1613
|
-
undefined,
|
|
1614
|
-
childSessionKey,
|
|
1615
|
-
);
|
|
1699
|
+
const child = new ChatDriver(this.providerRegistry, {
|
|
1700
|
+
sessionKey: childSessionKey,
|
|
1701
|
+
// Inherit the parent's bus so the sub-agent's tool-loop events still surface
|
|
1702
|
+
// (off-browser this is the shared no-op).
|
|
1703
|
+
activityBus: this.activityBus,
|
|
1704
|
+
});
|
|
1616
1705
|
// Mark before the first turn so the child forces tool use and reports a
|
|
1617
1706
|
// typed failure (rather than user-facing text) if it never completes.
|
|
1618
1707
|
child.markAsSubAgent();
|
|
@@ -1781,9 +1870,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1781
1870
|
phase: 'continueFromHistory',
|
|
1782
1871
|
agent: this.activeAgentName,
|
|
1783
1872
|
});
|
|
1784
|
-
|
|
1873
|
+
this.activityBus.publish('tool-loop-start', undefined);
|
|
1874
|
+
// Captured so the `finally` can carry the turn's outcome onto `tool-loop-end`.
|
|
1875
|
+
let result: ChatDriverResult = { reason: 'done' };
|
|
1785
1876
|
try {
|
|
1786
|
-
|
|
1877
|
+
result = await this.runToolLoop('', undefined, transientPrimer);
|
|
1878
|
+
return result;
|
|
1787
1879
|
} catch (e) {
|
|
1788
1880
|
logger.error('ChatDriver error:', e);
|
|
1789
1881
|
recordTurnError(this.sessionKey, 'exception', {
|
|
@@ -1797,7 +1889,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1797
1889
|
role: 'assistant',
|
|
1798
1890
|
content: 'Sorry, something went wrong on my end. Please try again in a moment.',
|
|
1799
1891
|
});
|
|
1800
|
-
|
|
1892
|
+
result = this.turnDone('exception');
|
|
1893
|
+
return result;
|
|
1801
1894
|
} finally {
|
|
1802
1895
|
recordMetaEvent(this.sessionKey, 'turn.end', {
|
|
1803
1896
|
phase: 'continueFromHistory',
|
|
@@ -1806,7 +1899,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1806
1899
|
});
|
|
1807
1900
|
this.busy = false;
|
|
1808
1901
|
this.endTurn();
|
|
1809
|
-
|
|
1902
|
+
this.activityBus.publish('tool-loop-end', ChatDriver.loopEndDetail(result));
|
|
1810
1903
|
}
|
|
1811
1904
|
}
|
|
1812
1905
|
|
|
@@ -2140,13 +2233,20 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2140
2233
|
// provider is resolved — static value or a function of the turn context
|
|
2141
2234
|
// (which carries the live state for stateful agents). Resolved before the
|
|
2142
2235
|
// snapshot so the debug log records the exact request config the model saw.
|
|
2143
|
-
const [
|
|
2236
|
+
const [
|
|
2237
|
+
resolvedTemperature,
|
|
2238
|
+
resolvedToolChoice,
|
|
2239
|
+
resolvedCachePolicy,
|
|
2240
|
+
resolvedTailContext,
|
|
2241
|
+
resolvedResponseSchema,
|
|
2242
|
+
] =
|
|
2144
2243
|
// oxlint-disable-next-line no-await-in-loop
|
|
2145
2244
|
await Promise.all([
|
|
2146
2245
|
this.resolveTurnInput<number>(this.activeTemperatureInput, promptCtx),
|
|
2147
2246
|
this.resolveTurnInput<ChatToolChoice>(this.activeToolChoiceInput, promptCtx),
|
|
2148
2247
|
this.resolveTurnInput<CachePolicy>(this.activeCachePolicyInput, promptCtx),
|
|
2149
2248
|
this.resolveTurnInput<string>(this.activeTailContextInput, promptCtx),
|
|
2249
|
+
this.resolveTurnInput<object | undefined>(this.activeResponseSchemaInput, promptCtx),
|
|
2150
2250
|
]);
|
|
2151
2251
|
// The system prompt is always just the agent's resolved prompt — byte-stable, so it can be
|
|
2152
2252
|
// cached. The framework's volatile additions (fold suffix, retry nudge) and the agent's tail
|
|
@@ -2197,6 +2297,12 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2197
2297
|
cachePolicy: resolvedCachePolicy,
|
|
2198
2298
|
// Framed volatile context injected at the message tail (never stored). Undefined → none.
|
|
2199
2299
|
tailContext,
|
|
2300
|
+
// Structured-output schema for this turn (agent/state-resolved). When set, the transport
|
|
2301
|
+
// constrains the final answer to it natively where the model supports it. Undefined → free text.
|
|
2302
|
+
responseSchema: resolvedResponseSchema,
|
|
2303
|
+
// Refusal-fallback chain (e.g. Fable 5 → Opus 4.8). Passed to the provider; applied
|
|
2304
|
+
// server-side where supported. Undefined → no fallback.
|
|
2305
|
+
fallbacks: this.activeFallbacks,
|
|
2200
2306
|
};
|
|
2201
2307
|
|
|
2202
2308
|
// Resolve the active provider for this turn. Static names were validated
|
|
@@ -2244,7 +2350,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2244
2350
|
'While working on your request, I repeatedly called my tools incorrectly. This often works on a second try — would you like me to try again? If it happens again, try breaking your request into smaller steps.',
|
|
2245
2351
|
});
|
|
2246
2352
|
}
|
|
2247
|
-
return
|
|
2353
|
+
return this.turnDone('malformed-function-call');
|
|
2248
2354
|
}
|
|
2249
2355
|
// The response was truncated at the provider's output-token cap while it
|
|
2250
2356
|
// still carried a tool call — its arguments are incomplete and unusable.
|
|
@@ -2273,7 +2379,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2273
2379
|
'My response was cut off because a single step reached the model output limit. This usually means one step tried to produce too much at once — try breaking your request into smaller steps.',
|
|
2274
2380
|
});
|
|
2275
2381
|
}
|
|
2276
|
-
return
|
|
2382
|
+
return this.turnDone('response-truncated');
|
|
2277
2383
|
}
|
|
2278
2384
|
// A request timeout from the transport (tagged `TimeoutError`) is not a
|
|
2279
2385
|
// bug on our end — surface it distinctly instead of letting it fall
|
|
@@ -2299,7 +2405,9 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2299
2405
|
'The request timed out. You can ask me to try again, or break this into a smaller step.',
|
|
2300
2406
|
});
|
|
2301
2407
|
}
|
|
2302
|
-
|
|
2408
|
+
// Recorded as `exception` above (there is no separate `timeout` member of
|
|
2409
|
+
// TurnFailureReason for the main turn); surface the same reason here.
|
|
2410
|
+
return this.turnDone('exception');
|
|
2303
2411
|
}
|
|
2304
2412
|
// The request was aborted: either a user cancel (turnController) or a
|
|
2305
2413
|
// driver dispose (lifecycleController, chained into the turn). A user
|
|
@@ -2380,7 +2488,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2380
2488
|
'While working on your request, I repeatedly generated a blank response. This often works on a second try — would you like me to try again? If it happens again, try breaking your request into smaller steps.',
|
|
2381
2489
|
});
|
|
2382
2490
|
}
|
|
2383
|
-
return
|
|
2491
|
+
return this.turnDone('empty-response');
|
|
2384
2492
|
} else {
|
|
2385
2493
|
// Split one model response into separate, individually-toggleable messages so each has its
|
|
2386
2494
|
// own visibility toggle and debug-log category:
|
|
@@ -2744,7 +2852,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2744
2852
|
"I'm sorry, I repeatedly tried to use tools that aren't available to me, so I couldn't complete that. If a 'Download agent log' option appears in the Settings (cog) menu, you can download the log and share it with whoever set up this assistant to help fix the issue.",
|
|
2745
2853
|
});
|
|
2746
2854
|
}
|
|
2747
|
-
return
|
|
2855
|
+
return this.turnDone('unknown-tool-limit');
|
|
2748
2856
|
}
|
|
2749
2857
|
|
|
2750
2858
|
const firstContinuation = systemCalls[0];
|
|
@@ -2759,10 +2867,13 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2759
2867
|
// Sub-agent early exit — checked here so the exit point mirrors the
|
|
2760
2868
|
// system-call pattern above. Set by completeSubAgent() in a tool handler.
|
|
2761
2869
|
if (this.subAgentCompletion) {
|
|
2762
|
-
return
|
|
2870
|
+
return this.turnDone();
|
|
2763
2871
|
}
|
|
2764
2872
|
}
|
|
2765
2873
|
|
|
2874
|
+
// The loop fell through: either it hit the iteration cap (a failure) or it
|
|
2875
|
+
// broke on a clean final answer (success). Only the former carries a reason.
|
|
2876
|
+
let failureReason: TurnFailureReason | undefined;
|
|
2766
2877
|
if (iterations >= this.maxToolIterations) {
|
|
2767
2878
|
logger.warn('ChatDriver: reached max tool iterations, stopping');
|
|
2768
2879
|
recordTurnError(this.sessionKey, 'max-iterations', {
|
|
@@ -2781,9 +2892,10 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2781
2892
|
"I've reached my limit for this response. You can ask me to continue and I'll pick up where I left off.",
|
|
2782
2893
|
});
|
|
2783
2894
|
}
|
|
2895
|
+
failureReason = 'max-iterations';
|
|
2784
2896
|
}
|
|
2785
2897
|
|
|
2786
|
-
return
|
|
2898
|
+
return this.turnDone(failureReason);
|
|
2787
2899
|
}
|
|
2788
2900
|
|
|
2789
2901
|
private appendToHistory(message: ChatMessage): void {
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
ChatMessage,
|
|
6
6
|
ChatRequestOptions,
|
|
7
7
|
} from '@genesislcap/foundation-ai';
|
|
8
|
+
import type { ActivityBus } from '../../channel/ai-activity-bus';
|
|
8
9
|
import type {
|
|
9
10
|
AgentConfig,
|
|
10
11
|
FallbackAgentConfig,
|
|
@@ -137,6 +138,8 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
137
138
|
maxToolIterations?: number;
|
|
138
139
|
maxFoldOperations?: number;
|
|
139
140
|
maxTurnSnapshots?: number;
|
|
141
|
+
/** Activity bus passed through to the inner ChatDriver (browser host injects the singleton). */
|
|
142
|
+
activityBus?: ActivityBus;
|
|
140
143
|
} = {},
|
|
141
144
|
) {
|
|
142
145
|
super();
|
|
@@ -166,17 +169,13 @@ export class OrchestratingDriver extends EventTarget implements AiDriver {
|
|
|
166
169
|
? { ...rawFallback, systemPrompt: buildFallbackSystemPrompt(rawFallback, this.specialists) }
|
|
167
170
|
: undefined;
|
|
168
171
|
|
|
169
|
-
this.chatDriver = new ChatDriver(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
options.maxFoldOperations,
|
|
177
|
-
options.maxTurnSnapshots,
|
|
178
|
-
this.sessionKey,
|
|
179
|
-
);
|
|
172
|
+
this.chatDriver = new ChatDriver(providerRegistry, {
|
|
173
|
+
maxToolIterations: options.maxToolIterations,
|
|
174
|
+
maxFoldOperations: options.maxFoldOperations,
|
|
175
|
+
maxTurnSnapshots: options.maxTurnSnapshots,
|
|
176
|
+
sessionKey: this.sessionKey,
|
|
177
|
+
activityBus: options.activityBus,
|
|
178
|
+
});
|
|
180
179
|
|
|
181
180
|
// Proxy events from the shared driver
|
|
182
181
|
this.chatDriver.addEventListener('history-updated', (e: Event) => {
|
package/src/config/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CachePolicy,
|
|
3
|
+
ChatFallback,
|
|
3
4
|
ChatInputDuringExecutionMode,
|
|
4
5
|
ChatMessage,
|
|
5
6
|
ChatToolChoice,
|
|
@@ -7,7 +8,7 @@ import type {
|
|
|
7
8
|
ChatToolHandlers,
|
|
8
9
|
} from '@genesislcap/foundation-ai';
|
|
9
10
|
|
|
10
|
-
export type { CachePolicy, ChatInputDuringExecutionMode, ChatToolChoice };
|
|
11
|
+
export type { CachePolicy, ChatFallback, ChatInputDuringExecutionMode, ChatToolChoice };
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Context passed to `onActivate` / `onDeactivate` lifecycle hooks on an agent.
|
|
@@ -174,6 +175,25 @@ export type CachePolicyInput =
|
|
|
174
175
|
*/
|
|
175
176
|
export type TailContextInput = string | ((ctx: SystemPromptContext) => string | Promise<string>);
|
|
176
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Structured-output schema for an agent. When set, the model's final (non-tool) answer is
|
|
180
|
+
* constrained to this JSON Schema instead of free text. Either a static schema or a function
|
|
181
|
+
* resolved each tool-loop iteration — return the schema on the turn(s) it should apply and
|
|
182
|
+
* `undefined` otherwise, so "whole-loop" vs "finalize-turn" enforcement is just what the
|
|
183
|
+
* resolver returns (e.g. `({ state }) => state.machine.matches('finalizing') ? schema : undefined`).
|
|
184
|
+
*
|
|
185
|
+
* Orthogonal to {@link ToolChoiceInput}: an agent can carry both `tools` and a `responseSchema`.
|
|
186
|
+
* Each provider applies it natively where possible (Anthropic `output_config.format`, Gemini
|
|
187
|
+
* JSON mode) and degrades gracefully otherwise. The schema is a plain JSON Schema object; keep to
|
|
188
|
+
* the portable subset providers share (`additionalProperties: false`, explicit `required`, enums,
|
|
189
|
+
* `anyOf` for nullables — no numeric/string constraints or recursion).
|
|
190
|
+
*
|
|
191
|
+
* @beta
|
|
192
|
+
*/
|
|
193
|
+
export type ResponseSchemaInput =
|
|
194
|
+
| object
|
|
195
|
+
| ((ctx: SystemPromptContext) => object | undefined | Promise<object | undefined>);
|
|
196
|
+
|
|
177
197
|
/**
|
|
178
198
|
* Context passed to an agent's `onUnresolvedTool` hook when the model calls a
|
|
179
199
|
* tool the driver cannot dispatch.
|
|
@@ -337,6 +357,25 @@ interface BaseAgentConfig {
|
|
|
337
357
|
* @beta
|
|
338
358
|
*/
|
|
339
359
|
tailContext?: TailContextInput;
|
|
360
|
+
/**
|
|
361
|
+
* Structured-output schema for this agent. When resolved to a schema on a turn, the model's
|
|
362
|
+
* final (non-tool) answer is constrained to it instead of free text. Composes with `tools`;
|
|
363
|
+
* resolved per turn like {@link BaseAgentConfig.cachePolicy}, so returning `undefined` on
|
|
364
|
+
* working turns and the schema on the finalize turn gives finalize-turn enforcement for free.
|
|
365
|
+
* See {@link ResponseSchemaInput}.
|
|
366
|
+
*
|
|
367
|
+
* @beta
|
|
368
|
+
*/
|
|
369
|
+
responseSchema?: ResponseSchemaInput;
|
|
370
|
+
/**
|
|
371
|
+
* Refusal-fallback chain for this agent (provider-neutral). If the model declines a turn
|
|
372
|
+
* (`stop_reason: 'refusal'` — e.g. Fable 5 safety classifiers), the provider re-runs it on the
|
|
373
|
+
* next listed model. Typical use: a Fable 5 agent with `[{ model: 'claude-opus-4-8' }]`. Static
|
|
374
|
+
* (not per-turn); providers that support it apply it server-side, others ignore it.
|
|
375
|
+
*
|
|
376
|
+
* @beta
|
|
377
|
+
*/
|
|
378
|
+
fallbacks?: ChatFallback[];
|
|
340
379
|
/**
|
|
341
380
|
* Optional hook consulted when the model calls a tool the driver cannot
|
|
342
381
|
* dispatch — either a *stale* tool (advertised earlier this activation but
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from './state/persistence';
|
|
|
16
16
|
export * from './provider/ai-provider-switcher';
|
|
17
17
|
export * from './provider/assistant-app-settings';
|
|
18
18
|
export * from './utils/tool-fold';
|
|
19
|
+
export type { TurnFailureReason } from './state/debug-event-log';
|
|
19
20
|
export type { TimelineMessage } from './utils/flatten-sub-agent-messages';
|
|
20
21
|
export type { CostSessionModelEntry, CostSessionRecord } from './utils/cost-session-history';
|
|
21
22
|
export type { ModelTagAppearance } from '@genesislcap/foundation-ai';
|
package/src/main/main.ts
CHANGED
|
@@ -1249,20 +1249,17 @@ export class FoundationAiAssistant extends GenesisElement {
|
|
|
1249
1249
|
maxToolIterations: agent.maxToolIterations,
|
|
1250
1250
|
maxFoldOperations: agent.maxFoldOperations,
|
|
1251
1251
|
maxTurnSnapshots: agent.maxTurnSnapshots,
|
|
1252
|
+
activityBus: agenticActivityBus,
|
|
1252
1253
|
});
|
|
1253
1254
|
}
|
|
1254
1255
|
|
|
1255
|
-
return new ChatDriver(
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
agent.maxFoldOperations,
|
|
1263
|
-
agent.maxTurnSnapshots,
|
|
1264
|
-
this.getStateKey() ?? '',
|
|
1265
|
-
);
|
|
1256
|
+
return new ChatDriver(this.providerRegistry, {
|
|
1257
|
+
maxToolIterations: agent.maxToolIterations,
|
|
1258
|
+
maxFoldOperations: agent.maxFoldOperations,
|
|
1259
|
+
maxTurnSnapshots: agent.maxTurnSnapshots,
|
|
1260
|
+
sessionKey: this.getStateKey() ?? '',
|
|
1261
|
+
activityBus: agenticActivityBus,
|
|
1262
|
+
});
|
|
1266
1263
|
}
|
|
1267
1264
|
|
|
1268
1265
|
/**
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ChatMessage } from '@genesislcap/foundation-ai';
|
|
2
2
|
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
3
|
-
import { agenticActivityBus } from '../channel/ai-activity-bus';
|
|
4
3
|
import { FoundationAiAssistant } from './main';
|
|
5
4
|
|
|
6
5
|
// Hold a reference so the custom-element registration isn't tree-shaken.
|
|
@@ -14,19 +13,15 @@ FoundationAiAssistant;
|
|
|
14
13
|
// (`main.template.ts`). This suite pins the GATE LOGIC (`hasActivePendingInteraction`)
|
|
15
14
|
// that binding depends on, so a change to when the gate opens/closes is caught.
|
|
16
15
|
//
|
|
17
|
-
// Note: this deliberately does NOT mount the assistant. Mounting runs
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
16
|
+
// Note: this deliberately does NOT mount the assistant. Mounting runs `connectedCallback`,
|
|
17
|
+
// which subscribes to `agenticActivityBus` — opening a cross-tab `BroadcastChannel` in a browser
|
|
18
|
+
// env and leaving the runner's event loop open. A getter-level test needs no mount and stays fast
|
|
19
|
+
// + reliable. `document.createElement` only upgrades the element (constructor) — it does not
|
|
20
|
+
// connect — so nothing subscribes, and the bus opens its channel lazily on first use, so none is
|
|
21
|
+
// ever created here (no teardown needed).
|
|
23
22
|
|
|
24
23
|
const Suite = createLogicSuite('FoundationAiAssistant popout interaction gate');
|
|
25
24
|
|
|
26
|
-
Suite.after(() => {
|
|
27
|
-
agenticActivityBus.close();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
25
|
/** A fresh (unconnected) element with a fake session store wired in. */
|
|
31
26
|
function elementWith(state: string, messages: ChatMessage[]): FoundationAiAssistant {
|
|
32
27
|
const el = document.createElement('foundation-ai-assistant') as FoundationAiAssistant;
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* @internal
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import type { TurnFailureReason } from '@genesislcap/foundation-ai';
|
|
28
|
+
|
|
27
29
|
/**
|
|
28
30
|
* Catalogue of meta event names. This is the documented surface — extend it as
|
|
29
31
|
* new events are wired in (Tier 2/3 lifecycle, interaction, provider events).
|
|
@@ -211,25 +213,14 @@ export function recordMetaEvent(
|
|
|
211
213
|
|
|
212
214
|
/**
|
|
213
215
|
* Why a turn failed or was retried — stamped as `detail.reason` on `turn.error`
|
|
214
|
-
* and `turn.retry` events
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* - `unknown-tool-limit` — the model repeatedly called tools it couldn't dispatch,
|
|
221
|
-
* whether hallucinated or stale (real earlier, retired now).
|
|
222
|
-
* - `max-iterations` — the tool loop hit its iteration cap.
|
|
223
|
-
* - `response-truncated` — a turn stopped at the provider's output-token cap with an
|
|
224
|
-
* incomplete tool call; deterministic, so it bails without retry.
|
|
216
|
+
* and `turn.retry` events, and (since PTC-0) surfaced at the driver's loop
|
|
217
|
+
* boundary via {@link ChatDriverResult.failureReason}. Re-exported from
|
|
218
|
+
* `@genesislcap/foundation-ai` — where it lives alongside `ChatDriverResult` so
|
|
219
|
+
* the boundary field and this log surface can never drift apart — under its
|
|
220
|
+
* historical name so existing importers are unaffected. Enumerated so the set
|
|
221
|
+
* stays in sync with the README and call sites can't drift to ad-hoc strings.
|
|
225
222
|
*/
|
|
226
|
-
export type TurnFailureReason
|
|
227
|
-
| 'exception'
|
|
228
|
-
| 'malformed-function-call'
|
|
229
|
-
| 'empty-response'
|
|
230
|
-
| 'unknown-tool-limit'
|
|
231
|
-
| 'max-iterations'
|
|
232
|
-
| 'response-truncated';
|
|
223
|
+
export type { TurnFailureReason };
|
|
233
224
|
|
|
234
225
|
/**
|
|
235
226
|
* Record a turn-ending failure (`turn.error`, importance `high`). The `reason`
|