@genesislcap/ai-assistant 14.494.0 → 14.495.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 +12 -3
- package/dist/ai-assistant.d.ts +29 -1
- package/dist/custom-elements.json +265 -189
- 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/components/chat-driver/chat-driver.d.ts +17 -0
- 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/index.d.ts +1 -0
- package/dist/dts/index.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/components/chat-driver/chat-driver.js +51 -13
- package/dist/esm/components/chat-driver/chat-driver.test.js +133 -1
- package/package.json +17 -17
- package/src/channel/ai-activity-channel.ts +9 -1
- package/src/components/chat-driver/chat-driver.test.ts +214 -1
- package/src/components/chat-driver/chat-driver.ts +57 -13
- package/src/index.ts +1 -0
- package/src/state/debug-event-log.ts +9 -18
|
@@ -2,14 +2,20 @@ 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
20
|
import { agenticActivityBus } from '../../channel/ai-activity-bus';
|
|
15
21
|
import type { AgentConfig } from '../../config/config';
|
|
@@ -2175,3 +2181,210 @@ condense('phaseEnd: collapses at endPhase() while the agent keeps running', asyn
|
|
|
2175
2181
|
});
|
|
2176
2182
|
|
|
2177
2183
|
condense.run();
|
|
2184
|
+
|
|
2185
|
+
// ---------------------------------------------------------------------------
|
|
2186
|
+
// turn-outcome surfacing (PTC-0) — the typed failure reason must reach BOTH
|
|
2187
|
+
// the loop-boundary result (`ChatDriverResult.failureReason`) and the
|
|
2188
|
+
// `tool-loop-end` activity-bus event detail. Historically every exit flattened
|
|
2189
|
+
// to `{ reason: 'done' }` and the bus detail was `undefined`; these lock the
|
|
2190
|
+
// two seams together, one per `TurnFailureReason`, plus a happy-path compat
|
|
2191
|
+
// check that the legacy shape is byte-unchanged.
|
|
2192
|
+
// ---------------------------------------------------------------------------
|
|
2193
|
+
|
|
2194
|
+
const outcome = createLogicSuite('ChatDriver turn-outcome surfacing');
|
|
2195
|
+
|
|
2196
|
+
outcome.after(() => {
|
|
2197
|
+
agenticActivityBus.close();
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
/**
|
|
2201
|
+
* Subscribe to `tool-loop-end` and expose the most recent detail seen. `fired()` reports
|
|
2202
|
+
* whether the event was seen at all — distinct from `detail()`, which is `undefined` both
|
|
2203
|
+
* when no event fired and when a clean turn fired with the historical `undefined` detail.
|
|
2204
|
+
*/
|
|
2205
|
+
const captureLoopEnd = (): {
|
|
2206
|
+
detail: () => { failureReason?: TurnFailureReason } | undefined;
|
|
2207
|
+
fired: () => boolean;
|
|
2208
|
+
stop: () => void;
|
|
2209
|
+
} => {
|
|
2210
|
+
let last: { failureReason?: TurnFailureReason } | undefined;
|
|
2211
|
+
let seen = false;
|
|
2212
|
+
const stop = agenticActivityBus.subscribe('tool-loop-end', (d) => {
|
|
2213
|
+
last = d;
|
|
2214
|
+
seen = true;
|
|
2215
|
+
});
|
|
2216
|
+
return { detail: () => (seen ? last : undefined), fired: () => seen, stop };
|
|
2217
|
+
};
|
|
2218
|
+
|
|
2219
|
+
/** A ChatDriver with an explicit (small) tool-iteration cap. */
|
|
2220
|
+
const makeCappedDriver = (
|
|
2221
|
+
config: AgentConfig,
|
|
2222
|
+
provider: AIProvider,
|
|
2223
|
+
maxIterations: number,
|
|
2224
|
+
sessionKey = '',
|
|
2225
|
+
): ChatDriver => {
|
|
2226
|
+
const driver = new ChatDriver(
|
|
2227
|
+
makeRegistry(provider),
|
|
2228
|
+
{},
|
|
2229
|
+
[],
|
|
2230
|
+
undefined,
|
|
2231
|
+
undefined,
|
|
2232
|
+
maxIterations,
|
|
2233
|
+
5,
|
|
2234
|
+
undefined,
|
|
2235
|
+
sessionKey,
|
|
2236
|
+
);
|
|
2237
|
+
driver.applyAgent(config);
|
|
2238
|
+
return driver;
|
|
2239
|
+
};
|
|
2240
|
+
|
|
2241
|
+
/**
|
|
2242
|
+
* Assert that a turn driven by `provider` surfaces `expected` at BOTH seams.
|
|
2243
|
+
* `driverFor` lets the max-iterations case swap in a low cap.
|
|
2244
|
+
*/
|
|
2245
|
+
const assertSurfacesReason = async (
|
|
2246
|
+
label: string,
|
|
2247
|
+
provider: AIProvider,
|
|
2248
|
+
expected: TurnFailureReason,
|
|
2249
|
+
driverFor: (config: AgentConfig, provider: AIProvider, sessionKey: string) => ChatDriver = (
|
|
2250
|
+
c,
|
|
2251
|
+
p,
|
|
2252
|
+
k,
|
|
2253
|
+
) => makeDriver(c, p, k),
|
|
2254
|
+
): Promise<void> => {
|
|
2255
|
+
clearMetaEventRegistry();
|
|
2256
|
+
const sessionKey = `outcome-${label}`;
|
|
2257
|
+
const config = agent({
|
|
2258
|
+
name: 'Static',
|
|
2259
|
+
toolDefinitions: [def('noop')],
|
|
2260
|
+
toolHandlers: { noop: async () => 'ok' },
|
|
2261
|
+
});
|
|
2262
|
+
const driver = driverFor(config, provider, sessionKey);
|
|
2263
|
+
const cap = captureLoopEnd();
|
|
2264
|
+
|
|
2265
|
+
const result: ChatDriverResult = await driver.sendMessage('go');
|
|
2266
|
+
|
|
2267
|
+
// Seam 1 — the loop-boundary result. Discriminant stays 'done' (compat).
|
|
2268
|
+
assert.is(result.reason, 'done', `[${label}] discriminant stays 'done'`);
|
|
2269
|
+
assert.is(
|
|
2270
|
+
result.reason === 'done' ? result.failureReason : undefined,
|
|
2271
|
+
expected,
|
|
2272
|
+
`[${label}] result.failureReason surfaces the typed reason`,
|
|
2273
|
+
);
|
|
2274
|
+
|
|
2275
|
+
// Seam 2 — the activity-bus tool-loop-end detail.
|
|
2276
|
+
assert.ok(cap.detail(), `[${label}] a tool-loop-end event fired`);
|
|
2277
|
+
assert.is(
|
|
2278
|
+
cap.detail()!.failureReason,
|
|
2279
|
+
expected,
|
|
2280
|
+
`[${label}] the bus detail carries the same reason`,
|
|
2281
|
+
);
|
|
2282
|
+
|
|
2283
|
+
// Consistency: the debug-log turn.error records the same taxonomy.
|
|
2284
|
+
const err = getMetaEvents(sessionKey).find((e) => e.type === 'turn.error');
|
|
2285
|
+
assert.ok(err, `[${label}] a turn.error is recorded`);
|
|
2286
|
+
assert.is(err!.detail?.reason, expected, `[${label}] the debug-log reason matches`);
|
|
2287
|
+
|
|
2288
|
+
cap.stop();
|
|
2289
|
+
};
|
|
2290
|
+
|
|
2291
|
+
/** A provider that returns a MALFORMED_FUNCTION_CALL on every call. */
|
|
2292
|
+
const malformedProvider = (): AIProvider => ({
|
|
2293
|
+
chat: async (): Promise<ChatMessage> => {
|
|
2294
|
+
throw new MalformedFunctionCallError('bad call');
|
|
2295
|
+
},
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
/** A provider that returns an empty response on every call. */
|
|
2299
|
+
const emptyProvider = (): AIProvider => ({
|
|
2300
|
+
chat: async (): Promise<ChatMessage> => ({ role: 'assistant', content: '' }),
|
|
2301
|
+
});
|
|
2302
|
+
|
|
2303
|
+
/** A provider that throws a ResponseTruncatedError (deterministic, no retry). */
|
|
2304
|
+
const truncatedProvider = (): AIProvider => ({
|
|
2305
|
+
chat: async (): Promise<ChatMessage> => {
|
|
2306
|
+
throw new ResponseTruncatedError('test-model', 1024, 1024, ['noop']);
|
|
2307
|
+
},
|
|
2308
|
+
});
|
|
2309
|
+
|
|
2310
|
+
/** A provider that throws a generic error (the sendMessage catch-all → 'exception'). */
|
|
2311
|
+
const throwingProvider = (): AIProvider => ({
|
|
2312
|
+
chat: async (): Promise<ChatMessage> => {
|
|
2313
|
+
throw new Error('boom');
|
|
2314
|
+
},
|
|
2315
|
+
});
|
|
2316
|
+
|
|
2317
|
+
/** A provider that never stops calling a valid tool (drives the iteration cap). */
|
|
2318
|
+
const neverStopsProvider = (): AIProvider => {
|
|
2319
|
+
let n = 0;
|
|
2320
|
+
return {
|
|
2321
|
+
chat: async (): Promise<ChatMessage> => {
|
|
2322
|
+
const id = `noop-${n}`;
|
|
2323
|
+
n += 1;
|
|
2324
|
+
return { role: 'assistant', content: '', toolCalls: [{ id, name: 'noop', args: {} }] };
|
|
2325
|
+
},
|
|
2326
|
+
};
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2329
|
+
/** A provider that keeps calling a tool with no handler (hallucinated → limit). */
|
|
2330
|
+
const hallucinatedProvider = (): AIProvider =>
|
|
2331
|
+
scriptedProvider(Array.from({ length: 6 }, (_u, i) => callsTool('ghost', `ghost-${i}`)));
|
|
2332
|
+
|
|
2333
|
+
outcome('malformed-function-call surfaces at both seams', async () => {
|
|
2334
|
+
await assertSurfacesReason('malformed', malformedProvider(), 'malformed-function-call');
|
|
2335
|
+
});
|
|
2336
|
+
|
|
2337
|
+
outcome('empty-response surfaces at both seams', async () => {
|
|
2338
|
+
await assertSurfacesReason('empty', emptyProvider(), 'empty-response');
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
outcome('response-truncated surfaces at both seams', async () => {
|
|
2342
|
+
await assertSurfacesReason('truncated', truncatedProvider(), 'response-truncated');
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
outcome('exception surfaces at both seams', async () => {
|
|
2346
|
+
await assertSurfacesReason('exception', throwingProvider(), 'exception');
|
|
2347
|
+
});
|
|
2348
|
+
|
|
2349
|
+
outcome('unknown-tool-limit surfaces at both seams', async () => {
|
|
2350
|
+
await assertSurfacesReason('unknown-tool', hallucinatedProvider(), 'unknown-tool-limit');
|
|
2351
|
+
});
|
|
2352
|
+
|
|
2353
|
+
outcome('max-iterations surfaces at both seams', async () => {
|
|
2354
|
+
await assertSurfacesReason('max-iter', neverStopsProvider(), 'max-iterations', (c, p, k) =>
|
|
2355
|
+
makeCappedDriver(c, p, 2, k),
|
|
2356
|
+
);
|
|
2357
|
+
});
|
|
2358
|
+
|
|
2359
|
+
outcome('a clean turn leaves the legacy shape byte-unchanged (no failureReason)', async () => {
|
|
2360
|
+
clearMetaEventRegistry();
|
|
2361
|
+
const config = agent({ name: 'Static' });
|
|
2362
|
+
// Plain-text reply ends the turn cleanly on the first call.
|
|
2363
|
+
const driver = makeDriver(
|
|
2364
|
+
config,
|
|
2365
|
+
scriptedProvider([{ role: 'assistant', content: 'hi' }]),
|
|
2366
|
+
'outcome-ok',
|
|
2367
|
+
);
|
|
2368
|
+
const cap = captureLoopEnd();
|
|
2369
|
+
|
|
2370
|
+
const result: ChatDriverResult = await driver.sendMessage('go');
|
|
2371
|
+
|
|
2372
|
+
// The result is exactly `{ reason: 'done' }` — no `failureReason` key added.
|
|
2373
|
+
assert.equal(result, { reason: 'done' }, 'happy-path result is the historical shape');
|
|
2374
|
+
assert.not.ok('failureReason' in result, 'no failureReason key is present on a clean turn');
|
|
2375
|
+
|
|
2376
|
+
// The bus event still fires, and its detail is the historical `undefined` — a clean
|
|
2377
|
+
// turn emits no detail object at all (byte-shape compat), not `{ failureReason: undefined }`.
|
|
2378
|
+
assert.ok(cap.fired(), 'a tool-loop-end event fired');
|
|
2379
|
+
assert.is(cap.detail(), undefined, 'a clean turn emits the historical undefined detail');
|
|
2380
|
+
|
|
2381
|
+
// And no turn.error was recorded.
|
|
2382
|
+
assert.not.ok(
|
|
2383
|
+
getMetaEvents('outcome-ok').some((e) => e.type === 'turn.error'),
|
|
2384
|
+
'a clean turn records no turn.error',
|
|
2385
|
+
);
|
|
2386
|
+
|
|
2387
|
+
cap.stop();
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2390
|
+
outcome.run();
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
InteractionResult,
|
|
18
18
|
SubAgentFailureReason,
|
|
19
19
|
SubAgentRequestOptions,
|
|
20
|
+
TurnFailureReason,
|
|
20
21
|
} from '@genesislcap/foundation-ai';
|
|
21
22
|
import {
|
|
22
23
|
isObservableAIProviderRegistry,
|
|
@@ -631,6 +632,35 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
631
632
|
return { reason: 'done' };
|
|
632
633
|
}
|
|
633
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Build the `done` loop result, carrying the typed failure reason when the turn
|
|
637
|
+
* bailed (PTC-0). The discriminant stays `'done'` either way — the same value a
|
|
638
|
+
* clean turn returns — so consumers matching on `reason === 'done'` are unchanged;
|
|
639
|
+
* `failureReason` is simply present on a failure and absent on success. Omitted
|
|
640
|
+
* (rather than set to `undefined`) so a happy-path result stays byte-identical to
|
|
641
|
+
* the historical `{ reason: 'done' }`.
|
|
642
|
+
*/
|
|
643
|
+
private turnDone(failureReason?: TurnFailureReason): ChatDriverResult {
|
|
644
|
+
return failureReason ? { reason: 'done', failureReason } : { reason: 'done' };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/** The typed failure reason on a loop result, or `undefined` for a clean turn / handoff. */
|
|
648
|
+
private static failureReasonOf(result: ChatDriverResult): TurnFailureReason | undefined {
|
|
649
|
+
return result.reason === 'done' ? result.failureReason : undefined;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Build the `tool-loop-end` event detail for a turn's result. A failure carries a
|
|
654
|
+
* `{ failureReason }` detail; a clean turn emits `undefined` — the historical shape,
|
|
655
|
+
* kept byte-identical so subscribers see exactly what they always have.
|
|
656
|
+
*/
|
|
657
|
+
private static loopEndDetail(
|
|
658
|
+
result: ChatDriverResult,
|
|
659
|
+
): { failureReason: TurnFailureReason } | undefined {
|
|
660
|
+
const failureReason = ChatDriver.failureReasonOf(result);
|
|
661
|
+
return failureReason ? { failureReason } : undefined;
|
|
662
|
+
}
|
|
663
|
+
|
|
634
664
|
/**
|
|
635
665
|
* Swap in a new agent's configuration. Called by OrchestratingDriver before
|
|
636
666
|
* each specialist turn so the shared driver runs with the right tools and prompt.
|
|
@@ -1412,8 +1442,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1412
1442
|
});
|
|
1413
1443
|
agenticActivityBus.publish('tool-loop-start', undefined);
|
|
1414
1444
|
|
|
1445
|
+
// Captured so the `finally` can carry the turn's outcome onto `tool-loop-end`.
|
|
1446
|
+
let result: ChatDriverResult = { reason: 'done' };
|
|
1415
1447
|
try {
|
|
1416
|
-
|
|
1448
|
+
result = await this.runToolLoop(userInput, attachments);
|
|
1449
|
+
return result;
|
|
1417
1450
|
} catch (e) {
|
|
1418
1451
|
logger.error('ChatDriver error:', e);
|
|
1419
1452
|
recordTurnError(this.sessionKey, 'exception', {
|
|
@@ -1427,7 +1460,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1427
1460
|
role: 'assistant',
|
|
1428
1461
|
content: 'Sorry, something went wrong on my end. Please try again in a moment.',
|
|
1429
1462
|
});
|
|
1430
|
-
|
|
1463
|
+
result = this.turnDone('exception');
|
|
1464
|
+
return result;
|
|
1431
1465
|
} finally {
|
|
1432
1466
|
recordMetaEvent(this.sessionKey, 'turn.end', {
|
|
1433
1467
|
phase: 'sendMessage',
|
|
@@ -1436,7 +1470,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1436
1470
|
});
|
|
1437
1471
|
this.busy = false;
|
|
1438
1472
|
this.endTurn();
|
|
1439
|
-
agenticActivityBus.publish('tool-loop-end',
|
|
1473
|
+
agenticActivityBus.publish('tool-loop-end', ChatDriver.loopEndDetail(result));
|
|
1440
1474
|
}
|
|
1441
1475
|
}
|
|
1442
1476
|
|
|
@@ -1782,8 +1816,11 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1782
1816
|
agent: this.activeAgentName,
|
|
1783
1817
|
});
|
|
1784
1818
|
agenticActivityBus.publish('tool-loop-start', undefined);
|
|
1819
|
+
// Captured so the `finally` can carry the turn's outcome onto `tool-loop-end`.
|
|
1820
|
+
let result: ChatDriverResult = { reason: 'done' };
|
|
1785
1821
|
try {
|
|
1786
|
-
|
|
1822
|
+
result = await this.runToolLoop('', undefined, transientPrimer);
|
|
1823
|
+
return result;
|
|
1787
1824
|
} catch (e) {
|
|
1788
1825
|
logger.error('ChatDriver error:', e);
|
|
1789
1826
|
recordTurnError(this.sessionKey, 'exception', {
|
|
@@ -1797,7 +1834,8 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1797
1834
|
role: 'assistant',
|
|
1798
1835
|
content: 'Sorry, something went wrong on my end. Please try again in a moment.',
|
|
1799
1836
|
});
|
|
1800
|
-
|
|
1837
|
+
result = this.turnDone('exception');
|
|
1838
|
+
return result;
|
|
1801
1839
|
} finally {
|
|
1802
1840
|
recordMetaEvent(this.sessionKey, 'turn.end', {
|
|
1803
1841
|
phase: 'continueFromHistory',
|
|
@@ -1806,7 +1844,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
1806
1844
|
});
|
|
1807
1845
|
this.busy = false;
|
|
1808
1846
|
this.endTurn();
|
|
1809
|
-
agenticActivityBus.publish('tool-loop-end',
|
|
1847
|
+
agenticActivityBus.publish('tool-loop-end', ChatDriver.loopEndDetail(result));
|
|
1810
1848
|
}
|
|
1811
1849
|
}
|
|
1812
1850
|
|
|
@@ -2244,7 +2282,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2244
2282
|
'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
2283
|
});
|
|
2246
2284
|
}
|
|
2247
|
-
return
|
|
2285
|
+
return this.turnDone('malformed-function-call');
|
|
2248
2286
|
}
|
|
2249
2287
|
// The response was truncated at the provider's output-token cap while it
|
|
2250
2288
|
// still carried a tool call — its arguments are incomplete and unusable.
|
|
@@ -2273,7 +2311,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2273
2311
|
'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
2312
|
});
|
|
2275
2313
|
}
|
|
2276
|
-
return
|
|
2314
|
+
return this.turnDone('response-truncated');
|
|
2277
2315
|
}
|
|
2278
2316
|
// A request timeout from the transport (tagged `TimeoutError`) is not a
|
|
2279
2317
|
// bug on our end — surface it distinctly instead of letting it fall
|
|
@@ -2299,7 +2337,9 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2299
2337
|
'The request timed out. You can ask me to try again, or break this into a smaller step.',
|
|
2300
2338
|
});
|
|
2301
2339
|
}
|
|
2302
|
-
|
|
2340
|
+
// Recorded as `exception` above (there is no separate `timeout` member of
|
|
2341
|
+
// TurnFailureReason for the main turn); surface the same reason here.
|
|
2342
|
+
return this.turnDone('exception');
|
|
2303
2343
|
}
|
|
2304
2344
|
// The request was aborted: either a user cancel (turnController) or a
|
|
2305
2345
|
// driver dispose (lifecycleController, chained into the turn). A user
|
|
@@ -2380,7 +2420,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2380
2420
|
'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
2421
|
});
|
|
2382
2422
|
}
|
|
2383
|
-
return
|
|
2423
|
+
return this.turnDone('empty-response');
|
|
2384
2424
|
} else {
|
|
2385
2425
|
// Split one model response into separate, individually-toggleable messages so each has its
|
|
2386
2426
|
// own visibility toggle and debug-log category:
|
|
@@ -2744,7 +2784,7 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2744
2784
|
"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
2785
|
});
|
|
2746
2786
|
}
|
|
2747
|
-
return
|
|
2787
|
+
return this.turnDone('unknown-tool-limit');
|
|
2748
2788
|
}
|
|
2749
2789
|
|
|
2750
2790
|
const firstContinuation = systemCalls[0];
|
|
@@ -2759,10 +2799,13 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2759
2799
|
// Sub-agent early exit — checked here so the exit point mirrors the
|
|
2760
2800
|
// system-call pattern above. Set by completeSubAgent() in a tool handler.
|
|
2761
2801
|
if (this.subAgentCompletion) {
|
|
2762
|
-
return
|
|
2802
|
+
return this.turnDone();
|
|
2763
2803
|
}
|
|
2764
2804
|
}
|
|
2765
2805
|
|
|
2806
|
+
// The loop fell through: either it hit the iteration cap (a failure) or it
|
|
2807
|
+
// broke on a clean final answer (success). Only the former carries a reason.
|
|
2808
|
+
let failureReason: TurnFailureReason | undefined;
|
|
2766
2809
|
if (iterations >= this.maxToolIterations) {
|
|
2767
2810
|
logger.warn('ChatDriver: reached max tool iterations, stopping');
|
|
2768
2811
|
recordTurnError(this.sessionKey, 'max-iterations', {
|
|
@@ -2781,9 +2824,10 @@ export class ChatDriver extends EventTarget implements AiDriver {
|
|
|
2781
2824
|
"I've reached my limit for this response. You can ask me to continue and I'll pick up where I left off.",
|
|
2782
2825
|
});
|
|
2783
2826
|
}
|
|
2827
|
+
failureReason = 'max-iterations';
|
|
2784
2828
|
}
|
|
2785
2829
|
|
|
2786
|
-
return
|
|
2830
|
+
return this.turnDone(failureReason);
|
|
2787
2831
|
}
|
|
2788
2832
|
|
|
2789
2833
|
private appendToHistory(message: ChatMessage): void {
|
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';
|
|
@@ -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`
|