@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
|
@@ -2,16 +2,22 @@ import type {
|
|
|
2
2
|
AIProvider,
|
|
3
3
|
AIProviderRegistry,
|
|
4
4
|
CachePolicy,
|
|
5
|
+
ChatDriverResult,
|
|
5
6
|
ChatMessage,
|
|
6
7
|
ChatRequestOptions,
|
|
7
8
|
ChatToolCall,
|
|
8
9
|
ChatToolChoice,
|
|
9
10
|
ChatToolDefinition,
|
|
10
11
|
InteractionResult,
|
|
12
|
+
TurnFailureReason,
|
|
13
|
+
} from '@genesislcap/foundation-ai';
|
|
14
|
+
import {
|
|
15
|
+
isChatToolCallUnknown,
|
|
16
|
+
MalformedFunctionCallError,
|
|
17
|
+
ResponseTruncatedError,
|
|
11
18
|
} from '@genesislcap/foundation-ai';
|
|
12
|
-
import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
|
|
13
19
|
import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
|
|
14
|
-
import {
|
|
20
|
+
import { type ActivityBus, AgenticActivityBus } from '../../channel/ai-activity-bus';
|
|
15
21
|
import type { AgentConfig } from '../../config/config';
|
|
16
22
|
import { clearMetaEventRegistry, getMetaEvents } from '../../state/debug-event-log';
|
|
17
23
|
import { sumCosts } from '../../utils/sum-costs';
|
|
@@ -114,18 +120,18 @@ const callsTool = (name: string, id: string): ChatMessage => ({
|
|
|
114
120
|
const agent = (overrides: Partial<AgentConfig> & { name: string }): AgentConfig =>
|
|
115
121
|
({ description: 'test agent', ...overrides }) as AgentConfig;
|
|
116
122
|
|
|
117
|
-
const makeDriver = (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
50,
|
|
125
|
-
5,
|
|
126
|
-
undefined,
|
|
123
|
+
const makeDriver = (
|
|
124
|
+
config: AgentConfig,
|
|
125
|
+
provider: AIProvider,
|
|
126
|
+
sessionKey = '',
|
|
127
|
+
activityBus?: ActivityBus,
|
|
128
|
+
): ChatDriver => {
|
|
129
|
+
const driver = new ChatDriver(makeRegistry(provider), {
|
|
130
|
+
maxToolIterations: 50,
|
|
131
|
+
maxFoldOperations: 5,
|
|
127
132
|
sessionKey,
|
|
128
|
-
|
|
133
|
+
activityBus,
|
|
134
|
+
});
|
|
129
135
|
driver.applyAgent(config);
|
|
130
136
|
return driver;
|
|
131
137
|
};
|
|
@@ -153,13 +159,6 @@ const unresolvedEvents = (sessionKey: string): Array<Record<string, unknown>> =>
|
|
|
153
159
|
|
|
154
160
|
const stale = createLogicSuite('ChatDriver stale-tool detection');
|
|
155
161
|
|
|
156
|
-
// The driver imports the `agenticActivityBus` singleton, which opens a
|
|
157
|
-
// BroadcastChannel at module load. An open channel keeps the test page alive
|
|
158
|
-
// and hangs the runner, so close it once the suite finishes.
|
|
159
|
-
stale.after(() => {
|
|
160
|
-
agenticActivityBus.close();
|
|
161
|
-
});
|
|
162
|
-
|
|
163
162
|
stale('guides the model when it calls a tool that an earlier state exposed', async () => {
|
|
164
163
|
// State A exposes tool_a; calling it advances to state B, which exposes only
|
|
165
164
|
// tool_b. A factory-form agent narrows the tool set per turn, mirroring how
|
|
@@ -357,10 +356,6 @@ stale.run();
|
|
|
357
356
|
|
|
358
357
|
const hook = createLogicSuite('ChatDriver onUnresolvedTool hook');
|
|
359
358
|
|
|
360
|
-
hook.after(() => {
|
|
361
|
-
agenticActivityBus.close();
|
|
362
|
-
});
|
|
363
|
-
|
|
364
359
|
hook('replaces the default for a hallucinated tool when the hook returns a string', async () => {
|
|
365
360
|
const config = agent({
|
|
366
361
|
name: 'Hooked',
|
|
@@ -716,12 +711,6 @@ cancel.run();
|
|
|
716
711
|
|
|
717
712
|
const subagent = createLogicSuite('ChatDriver sub-agents');
|
|
718
713
|
|
|
719
|
-
subagent.after(() => {
|
|
720
|
-
// Safe to call again even if `stale` already closed it — close() is
|
|
721
|
-
// idempotent and cross-tab publishes are guarded by `&& this.channel`.
|
|
722
|
-
agenticActivityBus.close();
|
|
723
|
-
});
|
|
724
|
-
|
|
725
714
|
/** A sub-agent named `worker` that finishes by calling `completeSubAgent`. */
|
|
726
715
|
const completingWorker = (result: unknown): AgentConfig =>
|
|
727
716
|
agent({
|
|
@@ -1530,17 +1519,18 @@ interactionContextLifecycle.run();
|
|
|
1530
1519
|
|
|
1531
1520
|
const interactionBus = createLogicSuite('ChatDriver interaction activity-bus signals');
|
|
1532
1521
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1522
|
+
// These two tests need to observe what the driver publishes, so they inject a local
|
|
1523
|
+
// in-memory bus (no `crossTabEvents` → no BroadcastChannel is ever opened, nothing to close)
|
|
1524
|
+
// rather than the shared singleton. Every other suite lets the driver default to the no-op bus.
|
|
1536
1525
|
|
|
1537
1526
|
interactionBus('brackets a park with interaction-requested then -resolved', async () => {
|
|
1538
1527
|
const events: string[] = [];
|
|
1528
|
+
const bus = new AgenticActivityBus();
|
|
1539
1529
|
const unsubs = [
|
|
1540
|
-
|
|
1541
|
-
|
|
1530
|
+
bus.subscribe('interaction-requested', () => events.push('requested')),
|
|
1531
|
+
bus.subscribe('interaction-resolved', () => events.push('resolved')),
|
|
1542
1532
|
];
|
|
1543
|
-
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]));
|
|
1533
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]), '', bus);
|
|
1544
1534
|
|
|
1545
1535
|
const pending = driver.requestInteraction('w', {});
|
|
1546
1536
|
assert.equal(events, ['requested'], 'parking fires interaction-requested');
|
|
@@ -1556,8 +1546,9 @@ interactionBus('brackets a park with interaction-requested then -resolved', asyn
|
|
|
1556
1546
|
|
|
1557
1547
|
interactionBus('a timed-out interaction still fires interaction-resolved', async () => {
|
|
1558
1548
|
const events: string[] = [];
|
|
1559
|
-
const
|
|
1560
|
-
const
|
|
1549
|
+
const bus = new AgenticActivityBus();
|
|
1550
|
+
const unsub = bus.subscribe('interaction-resolved', () => events.push('resolved'));
|
|
1551
|
+
const driver = makeDriver(agent({ name: 'a' }), scriptedProvider([]), '', bus);
|
|
1561
1552
|
|
|
1562
1553
|
// Never resolved by a user — the timeout path runs the same teardown, so it
|
|
1563
1554
|
// must signal the bus too (else the button would stay enabled after a timeout).
|
|
@@ -1713,17 +1704,13 @@ const makeObservableRegistry = (initial: AIProvider): ObservableTestRegistry =>
|
|
|
1713
1704
|
};
|
|
1714
1705
|
|
|
1715
1706
|
const makeDriverWithRegistry = (config: AgentConfig, registry: AIProviderRegistry): ChatDriver => {
|
|
1716
|
-
const driver = new ChatDriver(registry, {
|
|
1707
|
+
const driver = new ChatDriver(registry, { maxToolIterations: 50, maxFoldOperations: 5 });
|
|
1717
1708
|
driver.applyAgent(config);
|
|
1718
1709
|
return driver;
|
|
1719
1710
|
};
|
|
1720
1711
|
|
|
1721
1712
|
const observable = createLogicSuite('ChatDriver observable provider registry');
|
|
1722
1713
|
|
|
1723
|
-
observable.after(() => {
|
|
1724
|
-
agenticActivityBus.close();
|
|
1725
|
-
});
|
|
1726
|
-
|
|
1727
1714
|
observable(
|
|
1728
1715
|
'a registry change clears the resolved-provider cache so the next turn uses the new provider',
|
|
1729
1716
|
async () => {
|
|
@@ -1822,10 +1809,6 @@ const modelProvider = (model: string | undefined, responses: ChatMessage[]): AIP
|
|
|
1822
1809
|
|
|
1823
1810
|
const modelAttr = createLogicSuite('ChatDriver per-message model attribution');
|
|
1824
1811
|
|
|
1825
|
-
modelAttr.after(() => {
|
|
1826
|
-
agenticActivityBus.close();
|
|
1827
|
-
});
|
|
1828
|
-
|
|
1829
1812
|
modelAttr('stamps the resolved model and registry name onto an assistant reply', async () => {
|
|
1830
1813
|
const provider = modelProvider('gemini-2.5-flash-lite', [
|
|
1831
1814
|
{ role: 'assistant', content: 'hi there' },
|
|
@@ -1912,9 +1895,6 @@ modelAttr.run();
|
|
|
1912
1895
|
// ---------------------------------------------------------------------------
|
|
1913
1896
|
|
|
1914
1897
|
const condense = createLogicSuite('ChatDriver condenseWhen');
|
|
1915
|
-
condense.after(() => {
|
|
1916
|
-
agenticActivityBus.close();
|
|
1917
|
-
});
|
|
1918
1898
|
|
|
1919
1899
|
const bigBody = 'A'.repeat(2000);
|
|
1920
1900
|
|
|
@@ -2175,3 +2155,210 @@ condense('phaseEnd: collapses at endPhase() while the agent keeps running', asyn
|
|
|
2175
2155
|
});
|
|
2176
2156
|
|
|
2177
2157
|
condense.run();
|
|
2158
|
+
|
|
2159
|
+
// ---------------------------------------------------------------------------
|
|
2160
|
+
// turn-outcome surfacing (PTC-0) — the typed failure reason must reach BOTH
|
|
2161
|
+
// the loop-boundary result (`ChatDriverResult.failureReason`) and the
|
|
2162
|
+
// `tool-loop-end` activity-bus event detail. Historically every exit flattened
|
|
2163
|
+
// to `{ reason: 'done' }` and the bus detail was `undefined`; these lock the
|
|
2164
|
+
// two seams together, one per `TurnFailureReason`, plus a happy-path compat
|
|
2165
|
+
// check that the legacy shape is byte-unchanged.
|
|
2166
|
+
// ---------------------------------------------------------------------------
|
|
2167
|
+
|
|
2168
|
+
const outcome = createLogicSuite('ChatDriver turn-outcome surfacing');
|
|
2169
|
+
|
|
2170
|
+
// This branch routes activity events to an INJECTED bus (default NOOP), not the module
|
|
2171
|
+
// singleton, so these tests inject a local bus and observe the same seam the driver publishes to.
|
|
2172
|
+
const outcomeBus = new AgenticActivityBus();
|
|
2173
|
+
|
|
2174
|
+
outcome.after(() => {
|
|
2175
|
+
outcomeBus.close();
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
/**
|
|
2179
|
+
* Subscribe to `tool-loop-end` and expose the most recent detail seen. `fired()` reports
|
|
2180
|
+
* whether the event was seen at all — distinct from `detail()`, which is `undefined` both
|
|
2181
|
+
* when no event fired and when a clean turn fired with the historical `undefined` detail.
|
|
2182
|
+
*/
|
|
2183
|
+
const captureLoopEnd = (): {
|
|
2184
|
+
detail: () => { failureReason?: TurnFailureReason } | undefined;
|
|
2185
|
+
fired: () => boolean;
|
|
2186
|
+
stop: () => void;
|
|
2187
|
+
} => {
|
|
2188
|
+
let last: { failureReason?: TurnFailureReason } | undefined;
|
|
2189
|
+
let seen = false;
|
|
2190
|
+
const stop = outcomeBus.subscribe('tool-loop-end', (d) => {
|
|
2191
|
+
last = d;
|
|
2192
|
+
seen = true;
|
|
2193
|
+
});
|
|
2194
|
+
return { detail: () => (seen ? last : undefined), fired: () => seen, stop };
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2197
|
+
/** A ChatDriver with an explicit (small) tool-iteration cap. */
|
|
2198
|
+
const makeCappedDriver = (
|
|
2199
|
+
config: AgentConfig,
|
|
2200
|
+
provider: AIProvider,
|
|
2201
|
+
maxIterations: number,
|
|
2202
|
+
sessionKey = '',
|
|
2203
|
+
): ChatDriver => {
|
|
2204
|
+
const driver = new ChatDriver(makeRegistry(provider), {
|
|
2205
|
+
maxToolIterations: maxIterations,
|
|
2206
|
+
maxFoldOperations: 5,
|
|
2207
|
+
sessionKey,
|
|
2208
|
+
activityBus: outcomeBus,
|
|
2209
|
+
});
|
|
2210
|
+
driver.applyAgent(config);
|
|
2211
|
+
return driver;
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* Assert that a turn driven by `provider` surfaces `expected` at BOTH seams.
|
|
2216
|
+
* `driverFor` lets the max-iterations case swap in a low cap.
|
|
2217
|
+
*/
|
|
2218
|
+
const assertSurfacesReason = async (
|
|
2219
|
+
label: string,
|
|
2220
|
+
provider: AIProvider,
|
|
2221
|
+
expected: TurnFailureReason,
|
|
2222
|
+
driverFor: (config: AgentConfig, provider: AIProvider, sessionKey: string) => ChatDriver = (
|
|
2223
|
+
c,
|
|
2224
|
+
p,
|
|
2225
|
+
k,
|
|
2226
|
+
) => makeDriver(c, p, k, outcomeBus),
|
|
2227
|
+
): Promise<void> => {
|
|
2228
|
+
clearMetaEventRegistry();
|
|
2229
|
+
const sessionKey = `outcome-${label}`;
|
|
2230
|
+
const config = agent({
|
|
2231
|
+
name: 'Static',
|
|
2232
|
+
toolDefinitions: [def('noop')],
|
|
2233
|
+
toolHandlers: { noop: async () => 'ok' },
|
|
2234
|
+
});
|
|
2235
|
+
const driver = driverFor(config, provider, sessionKey);
|
|
2236
|
+
const cap = captureLoopEnd();
|
|
2237
|
+
|
|
2238
|
+
const result: ChatDriverResult = await driver.sendMessage('go');
|
|
2239
|
+
|
|
2240
|
+
// Seam 1 — the loop-boundary result. Discriminant stays 'done' (compat).
|
|
2241
|
+
assert.is(result.reason, 'done', `[${label}] discriminant stays 'done'`);
|
|
2242
|
+
assert.is(
|
|
2243
|
+
result.reason === 'done' ? result.failureReason : undefined,
|
|
2244
|
+
expected,
|
|
2245
|
+
`[${label}] result.failureReason surfaces the typed reason`,
|
|
2246
|
+
);
|
|
2247
|
+
|
|
2248
|
+
// Seam 2 — the activity-bus tool-loop-end detail.
|
|
2249
|
+
assert.ok(cap.detail(), `[${label}] a tool-loop-end event fired`);
|
|
2250
|
+
assert.is(
|
|
2251
|
+
cap.detail()!.failureReason,
|
|
2252
|
+
expected,
|
|
2253
|
+
`[${label}] the bus detail carries the same reason`,
|
|
2254
|
+
);
|
|
2255
|
+
|
|
2256
|
+
// Consistency: the debug-log turn.error records the same taxonomy.
|
|
2257
|
+
const err = getMetaEvents(sessionKey).find((e) => e.type === 'turn.error');
|
|
2258
|
+
assert.ok(err, `[${label}] a turn.error is recorded`);
|
|
2259
|
+
assert.is(err!.detail?.reason, expected, `[${label}] the debug-log reason matches`);
|
|
2260
|
+
|
|
2261
|
+
cap.stop();
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
/** A provider that returns a MALFORMED_FUNCTION_CALL on every call. */
|
|
2265
|
+
const malformedProvider = (): AIProvider => ({
|
|
2266
|
+
chat: async (): Promise<ChatMessage> => {
|
|
2267
|
+
throw new MalformedFunctionCallError('bad call');
|
|
2268
|
+
},
|
|
2269
|
+
});
|
|
2270
|
+
|
|
2271
|
+
/** A provider that returns an empty response on every call. */
|
|
2272
|
+
const emptyProvider = (): AIProvider => ({
|
|
2273
|
+
chat: async (): Promise<ChatMessage> => ({ role: 'assistant', content: '' }),
|
|
2274
|
+
});
|
|
2275
|
+
|
|
2276
|
+
/** A provider that throws a ResponseTruncatedError (deterministic, no retry). */
|
|
2277
|
+
const truncatedProvider = (): AIProvider => ({
|
|
2278
|
+
chat: async (): Promise<ChatMessage> => {
|
|
2279
|
+
throw new ResponseTruncatedError('test-model', 1024, 1024, ['noop']);
|
|
2280
|
+
},
|
|
2281
|
+
});
|
|
2282
|
+
|
|
2283
|
+
/** A provider that throws a generic error (the sendMessage catch-all → 'exception'). */
|
|
2284
|
+
const throwingProvider = (): AIProvider => ({
|
|
2285
|
+
chat: async (): Promise<ChatMessage> => {
|
|
2286
|
+
throw new Error('boom');
|
|
2287
|
+
},
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
/** A provider that never stops calling a valid tool (drives the iteration cap). */
|
|
2291
|
+
const neverStopsProvider = (): AIProvider => {
|
|
2292
|
+
let n = 0;
|
|
2293
|
+
return {
|
|
2294
|
+
chat: async (): Promise<ChatMessage> => {
|
|
2295
|
+
const id = `noop-${n}`;
|
|
2296
|
+
n += 1;
|
|
2297
|
+
return { role: 'assistant', content: '', toolCalls: [{ id, name: 'noop', args: {} }] };
|
|
2298
|
+
},
|
|
2299
|
+
};
|
|
2300
|
+
};
|
|
2301
|
+
|
|
2302
|
+
/** A provider that keeps calling a tool with no handler (hallucinated → limit). */
|
|
2303
|
+
const hallucinatedProvider = (): AIProvider =>
|
|
2304
|
+
scriptedProvider(Array.from({ length: 6 }, (_u, i) => callsTool('ghost', `ghost-${i}`)));
|
|
2305
|
+
|
|
2306
|
+
outcome('malformed-function-call surfaces at both seams', async () => {
|
|
2307
|
+
await assertSurfacesReason('malformed', malformedProvider(), 'malformed-function-call');
|
|
2308
|
+
});
|
|
2309
|
+
|
|
2310
|
+
outcome('empty-response surfaces at both seams', async () => {
|
|
2311
|
+
await assertSurfacesReason('empty', emptyProvider(), 'empty-response');
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
outcome('response-truncated surfaces at both seams', async () => {
|
|
2315
|
+
await assertSurfacesReason('truncated', truncatedProvider(), 'response-truncated');
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
outcome('exception surfaces at both seams', async () => {
|
|
2319
|
+
await assertSurfacesReason('exception', throwingProvider(), 'exception');
|
|
2320
|
+
});
|
|
2321
|
+
|
|
2322
|
+
outcome('unknown-tool-limit surfaces at both seams', async () => {
|
|
2323
|
+
await assertSurfacesReason('unknown-tool', hallucinatedProvider(), 'unknown-tool-limit');
|
|
2324
|
+
});
|
|
2325
|
+
|
|
2326
|
+
outcome('max-iterations surfaces at both seams', async () => {
|
|
2327
|
+
await assertSurfacesReason('max-iter', neverStopsProvider(), 'max-iterations', (c, p, k) =>
|
|
2328
|
+
makeCappedDriver(c, p, 2, k),
|
|
2329
|
+
);
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
outcome('a clean turn leaves the legacy shape byte-unchanged (no failureReason)', async () => {
|
|
2333
|
+
clearMetaEventRegistry();
|
|
2334
|
+
const config = agent({ name: 'Static' });
|
|
2335
|
+
// Plain-text reply ends the turn cleanly on the first call.
|
|
2336
|
+
const driver = makeDriver(
|
|
2337
|
+
config,
|
|
2338
|
+
scriptedProvider([{ role: 'assistant', content: 'hi' }]),
|
|
2339
|
+
'outcome-ok',
|
|
2340
|
+
outcomeBus,
|
|
2341
|
+
);
|
|
2342
|
+
const cap = captureLoopEnd();
|
|
2343
|
+
|
|
2344
|
+
const result: ChatDriverResult = await driver.sendMessage('go');
|
|
2345
|
+
|
|
2346
|
+
// The result is exactly `{ reason: 'done' }` — no `failureReason` key added.
|
|
2347
|
+
assert.equal(result, { reason: 'done' }, 'happy-path result is the historical shape');
|
|
2348
|
+
assert.not.ok('failureReason' in result, 'no failureReason key is present on a clean turn');
|
|
2349
|
+
|
|
2350
|
+
// The bus event still fires, and its detail is the historical `undefined` — a clean
|
|
2351
|
+
// turn emits no detail object at all (byte-shape compat), not `{ failureReason: undefined }`.
|
|
2352
|
+
assert.ok(cap.fired(), 'a tool-loop-end event fired');
|
|
2353
|
+
assert.is(cap.detail(), undefined, 'a clean turn emits the historical undefined detail');
|
|
2354
|
+
|
|
2355
|
+
// And no turn.error was recorded.
|
|
2356
|
+
assert.not.ok(
|
|
2357
|
+
getMetaEvents('outcome-ok').some((e) => e.type === 'turn.error'),
|
|
2358
|
+
'a clean turn records no turn.error',
|
|
2359
|
+
);
|
|
2360
|
+
|
|
2361
|
+
cap.stop();
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
outcome.run();
|