@genesislcap/ai-assistant 15.84.3 → 15.85.0-FUI-2666.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/ai-assistant.api.json +2175 -138
  2. package/dist/ai-assistant.d.ts +420 -43
  3. package/dist/chat-driver.cjs +780 -210
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +780 -209
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +1514 -424
  8. package/dist/dts/components/agent-session/agent-session.d.ts +50 -2
  9. package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -1
  10. package/dist/dts/components/ai-driver/ai-driver.d.ts +59 -1
  11. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  12. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +13 -1
  13. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
  14. package/dist/dts/components/chat-driver/chat-driver.constants.d.ts +0 -8
  15. package/dist/dts/components/chat-driver/chat-driver.constants.d.ts.map +1 -1
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts +74 -15
  17. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  18. package/dist/dts/components/orchestrating-driver/hand-off.d.ts +20 -0
  19. package/dist/dts/components/orchestrating-driver/hand-off.d.ts.map +1 -0
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +121 -22
  21. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  22. package/dist/dts/config/config.d.ts +66 -0
  23. package/dist/dts/config/config.d.ts.map +1 -1
  24. package/dist/dts/main/main.d.ts +3 -0
  25. package/dist/dts/main/main.d.ts.map +1 -1
  26. package/dist/dts/main/main.types.d.ts +6 -0
  27. package/dist/dts/main/main.types.d.ts.map +1 -1
  28. package/dist/dts/react.d.ts +4 -1
  29. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  30. package/dist/dts/state/persistence/session-snapshot.d.ts +33 -1
  31. package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
  32. package/dist/dts/utils/condense-history.d.ts +10 -3
  33. package/dist/dts/utils/condense-history.d.ts.map +1 -1
  34. package/dist/esm/components/agent-session/agent-session.js +128 -7
  35. package/dist/esm/components/ai-driver/ai-driver.js +3 -0
  36. package/dist/esm/components/ai-driver/base-driver-decorator.js +24 -0
  37. package/dist/esm/components/chat-driver/chat-driver.constants.js +0 -8
  38. package/dist/esm/components/chat-driver/chat-driver.js +247 -74
  39. package/dist/esm/components/orchestrating-driver/hand-off.js +59 -0
  40. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +480 -299
  41. package/dist/esm/main/main.js +32 -6
  42. package/dist/esm/main/main.types.js +6 -0
  43. package/dist/esm/state/debug-event-log.js +1 -1
  44. package/dist/esm/state/persistence/session-persister.js +55 -8
  45. package/dist/esm/state/persistence/session-snapshot.js +12 -6
  46. package/dist/esm/utils/condense-history.js +13 -4
  47. package/dist/react.cjs +16 -1
  48. package/dist/react.mjs +16 -1
  49. package/docs/sub_agent.md +1 -1
  50. package/package.json +17 -17
@@ -13,13 +13,13 @@ import { freshUserMessages } from '../../utils/preserved-user-messages';
13
13
  import { resolveTurnInput } from '../../utils/resolve-turn-input';
14
14
  import { messageUsage, sumUsage } from '../../utils/sum-usage';
15
15
  import { AgentSession } from '../agent-session/agent-session';
16
- import { DEFAULT_MAX_FOLD_OPERATIONS, DEFAULT_MAX_TOOL_ITERATIONS, DEFAULT_MAX_TURN_SNAPSHOTS, DEFAULT_MAX_UNKNOWN_TOOL_CALLS, EMPTY_RESPONSE_NUDGE, HANDOFF_TOOL_RESULT_PLACEHOLDER, HARVESTED_SUBAGENT_EVENTS, MALFORMED_CALL_NUDGE, MAX_EMPTY_RESPONSE_RETRIES, MAX_MALFORMED_RETRIES, MAX_SETUP_TRANSPORT_RETRIES, MAX_STALE_TOOL_CALLS, REQUEST_CONTINUATION_TOOL, } from './chat-driver.constants';
16
+ import { DEFAULT_MAX_FOLD_OPERATIONS, DEFAULT_MAX_TOOL_ITERATIONS, DEFAULT_MAX_TURN_SNAPSHOTS, DEFAULT_MAX_UNKNOWN_TOOL_CALLS, EMPTY_RESPONSE_NUDGE, HARVESTED_SUBAGENT_EVENTS, MALFORMED_CALL_NUDGE, MAX_EMPTY_RESPONSE_RETRIES, MAX_MALFORMED_RETRIES, MAX_SETUP_TRANSPORT_RETRIES, MAX_STALE_TOOL_CALLS, } from './chat-driver.constants';
17
17
  import { FoldController } from './fold-controller';
18
18
  import { InteractionController, } from './interaction-controller';
19
19
  import { TURN_FAILURE_EXITS, turnDone } from './turn-failures';
20
20
  import { TurnRecorder } from './turn-recorder';
21
21
  /** Re-exported so `from './chat-driver'` keeps resolving these. */
22
- export { EMPTY_RESPONSE_NUDGE, MALFORMED_CALL_NUDGE, REQUEST_CONTINUATION_TOOL, } from './chat-driver.constants';
22
+ export { EMPTY_RESPONSE_NUDGE, MALFORMED_CALL_NUDGE } from './chat-driver.constants';
23
23
  /**
24
24
  * Refusals about one request rather than the service: the parent may well succeed, so a sub-agent's
25
25
  * is not shown to the user.
@@ -43,6 +43,22 @@ const SERVICE_WIDE_REFUSALS = new Set([
43
43
  'ACCESS_DENIED',
44
44
  'VENDOR_UNREACHABLE',
45
45
  ]);
46
+ /**
47
+ * The tail note of a caller's closing call, once a handler's `handOff` is accepted (GENC-1737).
48
+ */
49
+ const handOffClosingNote = (to) => `[Hand-off]: You handed this request to ${to}, which takes it from here. Tell the user so in one short line. Call no tools, and do not answer the rest of the request.`;
50
+ /** The tail note of an agent's closing call, once a handler calls `completeHandOff` (GENC-1737). */
51
+ const HAND_BACK_CLOSING_NOTE = '[Hand-back]: You have finished this request and handed it back. Tell the user so in one short line. Call no tools, and start nothing new.';
52
+ /** A JSON copy of `value`, or `undefined` when it cannot be stored as JSON (GENC-1737). */
53
+ const jsonCopy = (value) => {
54
+ try {
55
+ const text = JSON.stringify(value);
56
+ return text === undefined ? undefined : JSON.parse(text);
57
+ }
58
+ catch (_a) {
59
+ return undefined;
60
+ }
61
+ };
46
62
  /**
47
63
  * Lift the reportable facts off a {@link BudgetExhaustedError}, or `undefined`
48
64
  * when it carries none.
@@ -283,6 +299,17 @@ export class ChatDriver extends EventTarget {
283
299
  * doesn't leak forward.
284
300
  */
285
301
  this.agentReleaseRequested = false;
302
+ /**
303
+ * The `agentEnd` activation of each caller waiting in a hand-off, by agent name, with the phase
304
+ * epoch it was suspended at (GENC-1737). A waiting caller is suspended, not ended, so neither its
305
+ * `agentEnd` payloads nor its `phaseEnd` ones (to another agent's `endPhase()`) collapse while the
306
+ * target runs; when it is installed again its entries move to the new activation and, while
307
+ * their phase had not ended, to the current epoch. A caller restored from a saved session gets
308
+ * an activation placeholder below zero, which no live activation uses.
309
+ */
310
+ this.suspendedActivations = new Map();
311
+ /** The last placeholder handed out for a restored caller; see `suspendedActivations`. */
312
+ this.restoredSuspensionSeq = 0;
286
313
  /**
287
314
  * Aborted by `dispose()` on driver teardown (e.g. an agent-config swap).
288
315
  * Threaded into every provider call as `ChatRequestOptions.signal`, so a
@@ -331,8 +358,8 @@ export class ChatDriver extends EventTarget {
331
358
  * The stateful-agent contract for this driver: active agent, user pin, flow lock, restored
332
359
  * state, and the `onActivate` / `onDeactivate` hooks (§7.1).
333
360
  *
334
- * **Public because `OrchestratingDriver` reaches through to it.** The orchestrator decorates a
335
- * config before installing it (the cross-agent continuation tool), so it cannot route through
361
+ * **Public because `OrchestratingDriver` reaches through to it.** The orchestrator builds a
362
+ * config before installing it (the fallback's prompt), so it cannot route through
336
363
  * {@link ChatDriver.activateAgent} and must drive the session itself — and one shared session is
337
364
  * the point: two would let `getActiveAgent()` disagree between the outer and inner driver.
338
365
  * `@internal` rather than private so that reach is legal without six delegating one-liners.
@@ -495,7 +522,7 @@ export class ChatDriver extends EventTarget {
495
522
  cancel() {
496
523
  // The message is over, running or not: a handler holding `turnSignal` — including one in a
497
524
  // sub-agent, which inherits it — sees the stop. Ended even when idle, because an orchestrator
498
- // stops between this driver's turns too (its re-classify, an agent install): a continuation
525
+ // stops between this driver's turns too (its classify, an agent install, a hand-off): a continuation
499
526
  // afterwards would otherwise carry the stopped message's denials and change rounds.
500
527
  this.messageController.abort(new DOMException('Cancelled by user', 'AbortError'));
501
528
  if (!this.busy)
@@ -790,21 +817,15 @@ export class ChatDriver extends EventTarget {
790
817
  this.aiServiceNoticesShown.add(code);
791
818
  return false;
792
819
  }
793
- /**
794
- * Whether a sub-agent hit a wall this turn that stops this driver too. A handoff goes on past an
795
- * AI-service wall: the next agent may use another vendor, and if it uses this one its own call
796
- * meets the wall and says so.
797
- */
798
- subAgentWallThisTurn(handingOff = false) {
799
- return (this.providerRefusedThisTurn ||
800
- this.budgetExhaustedThisTurn ||
801
- (this.aiServiceErrorThisTurn && !handingOff));
820
+ /** Whether a sub-agent hit a wall this turn that stops this driver too. */
821
+ subAgentWallThisTurn() {
822
+ return (this.providerRefusedThisTurn || this.budgetExhaustedThisTurn || this.aiServiceErrorThisTurn);
802
823
  }
803
824
  /**
804
825
  * Ends the turn when a sub-agent hit a wall this turn that stops this driver too: the provider
805
826
  * refusing the account, the AI service (GENC-1611), or the budget. Checked before each model call,
806
- * and again right after a tool batch, before a handoff or the iteration cap can report the turn as
807
- * something else.
827
+ * and again right after a tool batch, before the iteration cap can report the turn as something
828
+ * else.
808
829
  */
809
830
  endTurnOnSubAgentWall() {
810
831
  var _a, _b, _c, _d, _e;
@@ -1066,6 +1087,7 @@ export class ChatDriver extends EventTarget {
1066
1087
  // does not spuriously advance the clock and prematurely drop its payloads.
1067
1088
  if (config.name !== this.activeAgentName) {
1068
1089
  this.currentActivation += 1;
1090
+ this.resumeSuspendedActivation(config.name);
1069
1091
  this.resumeRestoredCondensePolicies(config.name);
1070
1092
  }
1071
1093
  this.systemPrompt = config.systemPrompt;
@@ -1300,6 +1322,111 @@ export class ChatDriver extends EventTarget {
1300
1322
  getAgentReleaseRequested() {
1301
1323
  return this.agentReleaseRequested;
1302
1324
  }
1325
+ /**
1326
+ * Offer `handOff` and `completeHandOff` to tool handlers, checking each `handOff` with `check`
1327
+ * (GENC-1737). Set by `OrchestratingDriver`, which knows the agents and the hand-off stack and acts
1328
+ * on the request after the call; a bare `ChatDriver` has no other agent to hand to.
1329
+ *
1330
+ * @internal
1331
+ */
1332
+ setHandOffCheck(check) {
1333
+ this.checkHandOff = check;
1334
+ }
1335
+ /** The `handOff` the most recent call accepted, if any (GENC-1737). */
1336
+ getHandOffRequest() {
1337
+ return this.handOffRequested;
1338
+ }
1339
+ /** What `completeHandOff` handed back in the most recent call, if it was called. */
1340
+ getHandOffCompletion() {
1341
+ return this.handOffCompletion;
1342
+ }
1343
+ /**
1344
+ * A handler's `handOff`: refused once this call has handed off or released, or when `data` is
1345
+ * not JSON, and otherwise checked by the orchestrator. `data` is kept as a JSON copy, since it is
1346
+ * saved with the session and the handler may change its own object afterwards.
1347
+ */
1348
+ requestHandOff(request) {
1349
+ var _a;
1350
+ if (this.handOffRequested)
1351
+ return { ok: false, reason: 'already-requested' };
1352
+ if (this.agentReleaseRequested)
1353
+ return { ok: false, reason: 'released' };
1354
+ let data;
1355
+ if ((request === null || request === void 0 ? void 0 : request.data) !== undefined) {
1356
+ data = jsonCopy(request.data);
1357
+ if (data === undefined)
1358
+ return { ok: false, reason: 'not-json' };
1359
+ }
1360
+ const accepted = Object.assign(Object.assign({ to: request.to, message: String((_a = request.message) !== null && _a !== void 0 ? _a : '') }, (data !== undefined && { data })), (request.returnTo === false && { returnTo: false }));
1361
+ const result = this.checkHandOff(accepted, this.activeAgentName);
1362
+ if (result.ok)
1363
+ this.handOffRequested = accepted;
1364
+ return result;
1365
+ }
1366
+ /**
1367
+ * The note of the closing call a hand-off asks for (GENC-1737), or `undefined` when none: once a
1368
+ * handler's `handOff` is accepted, or `completeHandOff` is called, the rest of that batch runs,
1369
+ * then one model call with no tools, so the agent can say so but not carry on, which a model told
1370
+ * so in a tool result does anyway. Then the turn ends.
1371
+ */
1372
+ closingCallNote() {
1373
+ if (this.handOffRequested)
1374
+ return handOffClosingNote(this.handOffRequested.to);
1375
+ return this.handOffCompletion ? HAND_BACK_CLOSING_NOTE : undefined;
1376
+ }
1377
+ /**
1378
+ * Suspend the active agent's activation for a hand-off it made (GENC-1737): installing the target
1379
+ * advances the `agentEnd` clock as any swap does, but this caller's payloads stay live.
1380
+ */
1381
+ suspendActivation(agentName) {
1382
+ if (agentName === this.activeAgentName) {
1383
+ this.suspendedActivations.set(agentName, {
1384
+ activation: this.currentActivation,
1385
+ phaseEpoch: this.currentPhaseEpoch,
1386
+ });
1387
+ }
1388
+ }
1389
+ /**
1390
+ * Suspend restored callers before their condense policies are restored, so entries still live
1391
+ * at save time stay live instead of waiting for "the next activation", which is the target's.
1392
+ */
1393
+ suspendRestoredActivations(agentNames) {
1394
+ for (const name of agentNames) {
1395
+ this.restoredSuspensionSeq -= 1;
1396
+ this.suspendedActivations.set(name, {
1397
+ activation: this.restoredSuspensionSeq,
1398
+ phaseEpoch: this.currentPhaseEpoch,
1399
+ });
1400
+ }
1401
+ }
1402
+ /** End every suspension: the hand-offs were unwound, so their callers' activations are over. */
1403
+ endSuspendedActivations() {
1404
+ this.suspendedActivations.clear();
1405
+ }
1406
+ /**
1407
+ * A caller installed again takes its suspended entries into the activation it resumes in, and
1408
+ * the ones whose phase had not ended into the current phase epoch.
1409
+ */
1410
+ resumeSuspendedActivation(agentName) {
1411
+ const suspended = this.suspendedActivations.get(agentName);
1412
+ if (suspended === undefined)
1413
+ return;
1414
+ this.suspendedActivations.delete(agentName);
1415
+ for (const entry of this.condensePolicies.values()) {
1416
+ if (entry.activation !== suspended.activation)
1417
+ continue;
1418
+ entry.activation = this.currentActivation;
1419
+ if (entry.phaseEpoch >= suspended.phaseEpoch)
1420
+ entry.phaseEpoch = this.currentPhaseEpoch;
1421
+ }
1422
+ }
1423
+ /**
1424
+ * The suspended activations, each with the phase epoch it was suspended at, for the `agentEnd`
1425
+ * and `phaseEnd` checks and for persistence.
1426
+ */
1427
+ suspendedClocks() {
1428
+ return new Map([...this.suspendedActivations.values()].map((s) => [s.activation, s.phaseEpoch]));
1429
+ }
1303
1430
  /**
1304
1431
  * Act on a `releaseAgent` call from a tool handler: fire `onDeactivate` and drop the flow lock,
1305
1432
  * so the next turn starts clean and the persister does not serialize a flow that is over.
@@ -1462,7 +1589,7 @@ export class ChatDriver extends EventTarget {
1462
1589
  * @beta
1463
1590
  */
1464
1591
  getCondensePoliciesForPersistence(messages) {
1465
- return snapshotCondensePolicies(this.condensePolicies, this.condenseClocks(), messages);
1592
+ return snapshotCondensePolicies(this.condensePolicies, this.condenseClocks(), messages, this.suspendedClocks());
1466
1593
  }
1467
1594
  /**
1468
1595
  * Re-register policies saved by {@link ChatDriver.getCondensePoliciesForPersistence} against
@@ -1513,9 +1640,18 @@ export class ChatDriver extends EventTarget {
1513
1640
  continue;
1514
1641
  if (this.condensePolicies.has(entry.toolCallId))
1515
1642
  continue;
1516
- this.condensePolicies.set(entry.toolCallId, rebaseRestoredCondensePolicy(entry, clocks));
1643
+ const registered = rebaseRestoredCondensePolicy(entry, clocks);
1644
+ this.condensePolicies.set(entry.toolCallId, registered);
1517
1645
  restored += 1;
1518
- if (entry.elapsed.activations === 0 && entry.agent !== this.activeAgentName) {
1646
+ // A caller waiting in a hand-off owns it: live, but suspended until its hand-back, not left
1647
+ // for the next activation, which is the target's (GENC-1737).
1648
+ const suspended = entry.elapsed.activations === 0 && entry.agent !== undefined
1649
+ ? this.suspendedActivations.get(entry.agent)
1650
+ : undefined;
1651
+ if (suspended !== undefined) {
1652
+ registered.activation = suspended.activation;
1653
+ }
1654
+ else if (entry.elapsed.activations === 0 && entry.agent !== this.activeAgentName) {
1519
1655
  this.condenseAwaitingReactivation.add(entry.toolCallId);
1520
1656
  awaitingReactivation += 1;
1521
1657
  }
@@ -1895,6 +2031,8 @@ export class ChatDriver extends EventTarget {
1895
2031
  this.subAgentCompletion = undefined;
1896
2032
  this.subAgentFailure = undefined;
1897
2033
  this.agentReleaseRequested = false;
2034
+ this.handOffRequested = undefined;
2035
+ this.handOffCompletion = undefined;
1898
2036
  this.budgetExhaustedThisTurn = false;
1899
2037
  this.budgetWallDetail = undefined;
1900
2038
  this.budgetWallViaSubAgent = false;
@@ -1978,7 +2116,7 @@ export class ChatDriver extends EventTarget {
1978
2116
  let opened;
1979
2117
  let recorded = false;
1980
2118
  const capabilities = this.buildCapabilities();
1981
- return Object.assign(Object.assign(Object.assign(Object.assign({}, (capabilities && { capabilities })), { requestInteraction: (componentName, data, options) => this.requestInteraction(componentName, data, options, (interactionId) => {
2119
+ return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (capabilities && { capabilities })), { requestInteraction: (componentName, data, options) => this.requestInteraction(componentName, data, options, (interactionId) => {
1982
2120
  opened = interactionId;
1983
2121
  }, undefined,
1984
2122
  // What an interceptor did to an interaction is reported in THIS call's result
@@ -2023,16 +2161,42 @@ export class ChatDriver extends EventTarget {
2023
2161
  // `agentEnd` payloads collapse and a re-run starts a fresh activation.
2024
2162
  this.currentActivation += 1;
2025
2163
  }, releaseAgent: () => {
2026
- var _a;
2164
+ var _a, _b;
2165
+ if (this.handOffRequested) {
2166
+ logger.warn(`ChatDriver(${(_a = this.activeAgentName) !== null && _a !== void 0 ? _a : 'unknown'}): releaseAgent called after handOff — ignoring; the hand-off ends this agent's turn`);
2167
+ return;
2168
+ }
2027
2169
  if (this.agentReleaseRequested) {
2028
- logger.warn(`ChatDriver(${(_a = this.activeAgentName) !== null && _a !== void 0 ? _a : 'unknown'}): releaseAgent called more than once — ignoring`);
2170
+ logger.warn(`ChatDriver(${(_b = this.activeAgentName) !== null && _b !== void 0 ? _b : 'unknown'}): releaseAgent called more than once — ignoring`);
2029
2171
  return;
2030
2172
  }
2031
2173
  this.agentReleaseRequested = true;
2032
2174
  // The stateful flow has wrapped up — advance the clock so its `agentEnd`
2033
2175
  // payloads collapse and a re-run starts a fresh activation.
2034
2176
  this.currentActivation += 1;
2035
- }, endPhase: () => {
2177
+ } }), (this.checkHandOff && {
2178
+ handOff: (request) => this.requestHandOff(request),
2179
+ // A copy, so a handler cannot change what is saved.
2180
+ handOffData: () => { var _a; return jsonCopy(this.agentSession.getHandOffData((_a = this.activeAgentName) !== null && _a !== void 0 ? _a : '')); },
2181
+ completeHandOff: (completion) => {
2182
+ var _a, _b, _c;
2183
+ if (this.agentReleaseRequested || this.handOffRequested) {
2184
+ logger.warn(`ChatDriver(${(_a = this.activeAgentName) !== null && _a !== void 0 ? _a : 'unknown'}): completeHandOff called after a release or a hand-off — ignoring`);
2185
+ return;
2186
+ }
2187
+ let data;
2188
+ if ((completion === null || completion === void 0 ? void 0 : completion.data) !== undefined) {
2189
+ data = jsonCopy(completion.data);
2190
+ if (data === undefined) {
2191
+ logger.warn(`ChatDriver(${(_b = this.activeAgentName) !== null && _b !== void 0 ? _b : 'unknown'}): completeHandOff data cannot be saved as JSON — handing back without it`);
2192
+ }
2193
+ }
2194
+ this.handOffCompletion = Object.assign({ message: String((_c = completion === null || completion === void 0 ? void 0 : completion.message) !== null && _c !== void 0 ? _c : '') }, (data !== undefined && { data }));
2195
+ // The same release as `releaseAgent`, with a payload for the caller.
2196
+ this.agentReleaseRequested = true;
2197
+ this.currentActivation += 1;
2198
+ },
2199
+ })), { endPhase: () => {
2036
2200
  // Advance the phase epoch so payloads registered with `{ on: phaseEnd }`
2037
2201
  // during the phase that just ended collapse on the next provider call.
2038
2202
  // Repeatable (no once-only guard, unlike releaseAgent/completeSubAgent):
@@ -2360,6 +2524,11 @@ export class ChatDriver extends EventTarget {
2360
2524
  this.beginTurn();
2361
2525
  this.subAgentCompletion = undefined;
2362
2526
  this.subAgentFailure = undefined;
2527
+ // A release or a hand-off is what this call asks for, as for a `sendMessage`: the orchestrator
2528
+ // acts on one after each call, so an earlier one must not be read again (GENC-1737).
2529
+ this.agentReleaseRequested = false;
2530
+ this.handOffRequested = undefined;
2531
+ this.handOffCompletion = undefined;
2363
2532
  this.budgetExhaustedThisTurn = false;
2364
2533
  this.budgetWallDetail = undefined;
2365
2534
  this.budgetWallViaSubAgent = false;
@@ -2419,7 +2588,7 @@ export class ChatDriver extends EventTarget {
2419
2588
  // oxlint-disable-next-line complexity, max-lines-per-function
2420
2589
  runToolLoop(userInput, attachments, transientPrimer, tailNote) {
2421
2590
  return __awaiter(this, void 0, void 0, function* () {
2422
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
2591
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
2423
2592
  if (!this.systemPrompt) {
2424
2593
  logger.warn('ChatDriver: no systemPrompt set. The assistant will have no instructions — provide a systemPrompt via agents config or the foundation-ai-assistant property.');
2425
2594
  }
@@ -2449,7 +2618,11 @@ export class ChatDriver extends EventTarget {
2449
2618
  // Resolved per USER turn instead, which is where the docs say to choose it.
2450
2619
  let pinnedThinkingPolicy;
2451
2620
  let thinkingPolicyPinned = false;
2452
- while (iterations < this.maxToolIterations) {
2621
+ // A `handOff` accepted in a batch gets its closing call even past the iteration cap.
2622
+ // A closing call (see `closingCallNote`) is one model call, made even past the iteration cap;
2623
+ // whatever path brings the loop back after it, a retry included, the turn ends there.
2624
+ let closingCallMade = false;
2625
+ while (iterations < this.maxToolIterations || (this.closingCallNote() && !closingCallMade)) {
2453
2626
  iterations += 1;
2454
2627
  // Monotonic across the driver's life — the age clock. Unlike `iterations`,
2455
2628
  // it is NOT reset per turn and NOT decremented for folds, so `age` keeps
@@ -2465,6 +2638,9 @@ export class ChatDriver extends EventTarget {
2465
2638
  const wall = this.endTurnOnSubAgentWall();
2466
2639
  if (wall)
2467
2640
  return wall;
2641
+ if (closingCallMade)
2642
+ return turnDone();
2643
+ const closing = this.closingCallNote();
2468
2644
  const promptCtx = {
2469
2645
  agentName: (_a = this.activeAgentName) !== null && _a !== void 0 ? _a : '',
2470
2646
  history: this.history,
@@ -2533,7 +2709,8 @@ export class ChatDriver extends EventTarget {
2533
2709
  // A continuation's tail note (GENC-1662) holds for the first model call — retries included,
2534
2710
  // since they rewind `iterations` — and goes before the retry nudge, which must stay last.
2535
2711
  const firstCallNote = tailNote && iterations === 1 ? `\n\n${tailNote}` : '';
2536
- const frameworkSystemSuffix = `${foldSuffix}${firstCallNote}${retryNudge}`;
2712
+ const closingNote = closing ? `\n\n${closing}` : '';
2713
+ const frameworkSystemSuffix = `${foldSuffix}${firstCallNote}${closingNote}${retryNudge}`;
2537
2714
  // Normalize the primer at the same chokepoint as the driver's own history
2538
2715
  // (GENC-1351 §5.7): a parent's `compacted-summary` message rides into a
2539
2716
  // sub-agent's primer (`requestSubAgent` snapshots the parent's raw history),
@@ -2552,15 +2729,17 @@ export class ChatDriver extends EventTarget {
2552
2729
  // <conversation_summary> user message (GENC-1351 §5.7) before any per-agent
2553
2730
  // masking, so the raw display-only role can never reach a transport. Each
2554
2731
  // step runs on a copy — stored history is untouched.
2732
+ const suspended = this.suspendedClocks();
2555
2733
  const condensedHistory = applyCondensation([...baseHistory], this.condensePolicies, {
2556
2734
  modelCall: this.modelCallSeq,
2557
2735
  turn: this.turnSeq,
2558
2736
  // A call's activation has ended once a later activation is current — a
2559
2737
  // swap or release/complete both advance the counter.
2560
- activationEnded: (activation) => activation < this.currentActivation,
2738
+ // A caller waiting in a hand-off is suspended, not ended (GENC-1737).
2739
+ activationEnded: (activation) => activation < this.currentActivation && !suspended.has(activation),
2561
2740
  // A call's phase has ended once the app declared a later phase epoch
2562
- // current via `endPhase()`.
2563
- phaseEnded: (phaseEpoch) => phaseEpoch < this.currentPhaseEpoch,
2741
+ // current via `endPhase()`; a waiting caller's, only by one before it waited.
2742
+ phaseEnded: (phaseEpoch, activation) => { var _a; return phaseEpoch < ((_a = suspended.get(activation)) !== null && _a !== void 0 ? _a : this.currentPhaseEpoch); },
2564
2743
  batchCalls: this.condenseBatchCalls,
2565
2744
  }, (detail) => recordMetaEvent(this.sessionKey, 'context.condensed', detail));
2566
2745
  const normalizedHistory = normalizeForProvider(condensedHistory);
@@ -2607,7 +2786,11 @@ export class ChatDriver extends EventTarget {
2607
2786
  // finish by calling a tool (their completion tool) so the turn can't end
2608
2787
  // on a free-text answer; top-level agents stay 'auto'. (Transports no-op a
2609
2788
  // force when no tools are advertised.)
2610
- const effectiveToolChoice = resolvedToolChoice !== null && resolvedToolChoice !== void 0 ? resolvedToolChoice : (this.isSubAgent ? 'required' : undefined);
2789
+ // The tools stay advertised on a closing call: a request whose history holds tool calls must
2790
+ // define them (Anthropic refuses one that doesn't).
2791
+ const effectiveToolChoice = closing
2792
+ ? 'none'
2793
+ : (resolvedToolChoice !== null && resolvedToolChoice !== void 0 ? resolvedToolChoice : (this.isSubAgent ? 'required' : undefined));
2611
2794
  const turnSnapshot = this.turnRecorder.record({
2612
2795
  agentName: this.activeAgentName,
2613
2796
  agentLabel: this.activeAgentLabel,
@@ -2712,6 +2895,8 @@ export class ChatDriver extends EventTarget {
2712
2895
  });
2713
2896
  return this.endTurnWithFailure(TURN_FAILURE_EXITS.contextGuard);
2714
2897
  }
2898
+ if (closing)
2899
+ closingCallMade = true;
2715
2900
  let response;
2716
2901
  try {
2717
2902
  // oxlint-disable-next-line no-await-in-loop
@@ -2945,15 +3130,23 @@ export class ChatDriver extends EventTarget {
2945
3130
  // fallback chain answered on a different model than the one we asked for.
2946
3131
  if (response.model !== undefined)
2947
3132
  turnSnapshot.model = response.model;
2948
- const isThinkingStep = response.content && ((_g = response.toolCalls) === null || _g === void 0 ? void 0 : _g.length);
2949
- const isEmptyResponse = !((_h = response.content) === null || _h === void 0 ? void 0 : _h.trim()) && !((_j = response.toolCalls) === null || _j === void 0 ? void 0 : _j.length);
3133
+ if (closing) {
3134
+ // What it tried to call is not run, on a transport that ignores `toolChoice` too. Saying
3135
+ // nothing is fine, a refusal included: no failure that would hold the hand-off up.
3136
+ if ((_g = response.toolCalls) === null || _g === void 0 ? void 0 : _g.length)
3137
+ response = Object.assign(Object.assign({}, response), { toolCalls: undefined });
3138
+ if (!((_h = response.content) === null || _h === void 0 ? void 0 : _h.trim()))
3139
+ return turnDone();
3140
+ }
3141
+ const isThinkingStep = response.content && ((_j = response.toolCalls) === null || _j === void 0 ? void 0 : _j.length);
3142
+ const isEmptyResponse = !((_k = response.content) === null || _k === void 0 ? void 0 : _k.trim()) && !((_l = response.toolCalls) === null || _l === void 0 ? void 0 : _l.length);
2950
3143
  // A pre-output refusal (safety-classifier decline, e.g. Fable 5 `stop_reason: 'refusal'`)
2951
3144
  // comes back with empty content, so it looks like a blank response — but it is deterministic:
2952
3145
  // retrying re-sends the identical request and refuses again, burning up to
2953
3146
  // MAX_EMPTY_RESPONSE_RETRIES turns on the most expensive models for the same outcome, ending
2954
3147
  // in the misleading "blank response" message. Treat it as a terminal, non-retried failure with
2955
3148
  // its own reason and message. (GENC-1461)
2956
- const isRefusal = ((_k = response.responseMeta) === null || _k === void 0 ? void 0 : _k.finishReason) === 'refusal';
3149
+ const isRefusal = ((_m = response.responseMeta) === null || _m === void 0 ? void 0 : _m.finishReason) === 'refusal';
2957
3150
  if (isEmptyResponse) {
2958
3151
  emptyResponseAttempts += 1;
2959
3152
  if (!isRefusal && emptyResponseAttempts < MAX_EMPTY_RESPONSE_RETRIES) {
@@ -3024,16 +3217,12 @@ export class ChatDriver extends EventTarget {
3024
3217
  emptyResponseAttempts = 0;
3025
3218
  malformedAttempts = 0;
3026
3219
  setupTransportAttempts = 0;
3027
- if (!((_l = response.toolCalls) === null || _l === void 0 ? void 0 : _l.length)) {
3220
+ if (closing)
3221
+ return turnDone();
3222
+ if (!((_o = response.toolCalls) === null || _o === void 0 ? void 0 : _o.length)) {
3028
3223
  break;
3029
3224
  }
3030
- const [toolCalls, systemCalls] = response.toolCalls.reduce((acc, tc) => {
3031
- if (tc.name === REQUEST_CONTINUATION_TOOL)
3032
- acc[1].push(tc);
3033
- else
3034
- acc[0].push(tc);
3035
- return acc;
3036
- }, [[], []]);
3225
+ const toolCalls = response.toolCalls;
3037
3226
  const executedById = new Map();
3038
3227
  const unknownToolIds = new Set();
3039
3228
  // Subset of unknownToolIds that were stale (previously available) rather
@@ -3229,34 +3418,25 @@ export class ChatDriver extends EventTarget {
3229
3418
  // Tag tool calls with fold UI metadata before appending results
3230
3419
  const foldPath = this.folds.activeNames();
3231
3420
  for (const tc of response.toolCalls) {
3232
- if (tc.name === REQUEST_CONTINUATION_TOOL) {
3421
+ const r = executedById.get(tc.id);
3422
+ if (r) {
3423
+ // Persist only `toolCallId`/`content` on the tool result — the sub-agent
3424
+ // trace lives on the tool *call* (back-patched below from `executedById`),
3425
+ // which is what the UI's trace panel and `sumCosts` read. Appending the
3426
+ // trace here too would duplicate the entire child conversation in history
3427
+ // (and the exported debug log) for no reader.
3233
3428
  this.appendToHistory({
3234
3429
  role: 'tool',
3235
3430
  content: '',
3236
- toolResult: { toolCallId: tc.id, content: HANDOFF_TOOL_RESULT_PLACEHOLDER },
3431
+ toolResult: Object.assign({ toolCallId: r.toolCallId, content: r.content }, (r.attachments ? { attachments: r.attachments } : {})),
3237
3432
  });
3238
3433
  }
3239
- else {
3240
- const r = executedById.get(tc.id);
3241
- if (r) {
3242
- // Persist only `toolCallId`/`content` on the tool result — the sub-agent
3243
- // trace lives on the tool *call* (back-patched below from `executedById`),
3244
- // which is what the UI's trace panel and `sumCosts` read. Appending the
3245
- // trace here too would duplicate the entire child conversation in history
3246
- // (and the exported debug log) for no reader.
3247
- this.appendToHistory({
3248
- role: 'tool',
3249
- content: '',
3250
- toolResult: Object.assign({ toolCallId: r.toolCallId, content: r.content }, (r.attachments ? { attachments: r.attachments } : {})),
3251
- });
3252
- }
3253
- }
3254
3434
  }
3255
3435
  // Back-patch foldEvent and foldPath onto the tool call message we just appended.
3256
3436
  // The response was appended before execution — find it and annotate.
3257
3437
  let tcMsgIdx = -1;
3258
3438
  for (let i = this.history.length - 1; i >= 0; i -= 1) {
3259
- if (this.history[i].role === 'assistant' && ((_m = this.history[i].toolCalls) === null || _m === void 0 ? void 0 : _m.length)) {
3439
+ if (this.history[i].role === 'assistant' && ((_p = this.history[i].toolCalls) === null || _p === void 0 ? void 0 : _p.length)) {
3260
3440
  tcMsgIdx = i;
3261
3441
  break;
3262
3442
  }
@@ -3294,7 +3474,7 @@ export class ChatDriver extends EventTarget {
3294
3474
  const unknownTools = [
3295
3475
  ...new Set([
3296
3476
  ...this.recentUnknownToolNames,
3297
- ...((_o = response.toolCalls) !== null && _o !== void 0 ? _o : [])
3477
+ ...((_q = response.toolCalls) !== null && _q !== void 0 ? _q : [])
3298
3478
  .filter((tc) => unknownToolIds.has(tc.id))
3299
3479
  .map((tc) => tc.name),
3300
3480
  ]),
@@ -3306,7 +3486,7 @@ export class ChatDriver extends EventTarget {
3306
3486
  const staleTools = [
3307
3487
  ...new Set([
3308
3488
  ...this.recentStaleToolNames,
3309
- ...((_p = response.toolCalls) !== null && _p !== void 0 ? _p : [])
3489
+ ...((_r = response.toolCalls) !== null && _r !== void 0 ? _r : [])
3310
3490
  .filter((tc) => staleToolIds.has(tc.id))
3311
3491
  .map((tc) => tc.name),
3312
3492
  ]),
@@ -3324,29 +3504,22 @@ export class ChatDriver extends EventTarget {
3324
3504
  return this.endTurnWithFailure(TURN_FAILURE_EXITS.unknownToolLimit);
3325
3505
  }
3326
3506
  // A wall a sub-agent hit in this batch wins over the exits below, which would report the turn
3327
- // as a handoff or the iteration cap, or lose the wall altogether. Three things win over it: a
3328
- // completion, which needs no further model call; a handoff past an AI-service wall; and a stop
3329
- // the user pressed while the batch ran, which ends the turn as "Stopped." as it would at the
3330
- // next model call.
3331
- const firstContinuation = systemCalls[0];
3332
- if (!this.subAgentCompletion && this.subAgentWallThisTurn(!!firstContinuation)) {
3507
+ // as the iteration cap, or lose the wall altogether. Two things win over it: a completion,
3508
+ // which needs no further model call; and a stop the user pressed while the batch ran, which
3509
+ // ends the turn as "Stopped." as it would at the next model call.
3510
+ if (!this.subAgentCompletion && this.subAgentWallThisTurn()) {
3333
3511
  if (this.turnController.signal.aborted)
3334
3512
  return this.completeAbortedTurn();
3335
3513
  const wallAfterBatch = this.endTurnOnSubAgentWall();
3336
3514
  if (wallAfterBatch)
3337
3515
  return wallAfterBatch;
3338
3516
  }
3339
- // A completion or a handoff went on past a child's AI-service refusal: say it once.
3517
+ // A completion went on past a child's AI-service refusal: say it once.
3340
3518
  if (this.aiServiceErrorThisTurn && this.aiServiceWall) {
3341
3519
  this.noticeAIServiceRefusal(this.aiServiceWall.code);
3342
3520
  this.flushAIServiceNotices();
3343
3521
  }
3344
- if (firstContinuation) {
3345
- const { summary, remaining_task: remainingTask } = firstContinuation.args;
3346
- return { reason: 'agent-handoff', summary, remainingTask };
3347
- }
3348
- // Sub-agent early exit — checked here so the exit point mirrors the
3349
- // system-call pattern above. Set by completeSubAgent() in a tool handler.
3522
+ // Sub-agent early exit, set by completeSubAgent() in a tool handler.
3350
3523
  if (this.subAgentCompletion) {
3351
3524
  return turnDone();
3352
3525
  }
@@ -0,0 +1,59 @@
1
+ const OUTCOME_KINDS = [
2
+ 'completed',
3
+ 'released',
4
+ 'cleared',
5
+ 'agent-removed',
6
+ 'state-lost',
7
+ ];
8
+ /**
9
+ * The tail note a hand-off's target runs its first call with (GENC-1737). The brief itself is in
10
+ * the transcript, as a message delegated by the caller, so the request already closes on it.
11
+ */
12
+ export function handOffNote(caller) {
13
+ return `[Hand-off]: ${caller} handed this request to you. Its brief is the message above. Work on it as you would on a request from the user.`;
14
+ }
15
+ /**
16
+ * The tail note a caller resumes with when its hand-off ends (GENC-1737). It quotes nothing: a
17
+ * completed target's summary is in the transcript as a message it handed back, and a released
18
+ * target's last reply already is.
19
+ */
20
+ export function handBackNote(frame, outcome) {
21
+ const how = {
22
+ completed: `${frame.target} has finished the request you handed it.`,
23
+ released: `${frame.target} has finished the request you handed it, without a summary. Its last reply is above.`,
24
+ cleared: 'The hand-off was cleared.',
25
+ 'agent-removed': `${frame.target} is no longer available, so the request you handed it was not finished.`,
26
+ 'state-lost': `The hand-off to ${frame.target} could not be resumed.`,
27
+ };
28
+ const summary = outcome.kind === 'completed' && outcome.message ? ' Its summary is the message above.' : '';
29
+ return `[Hand-back]: ${how[outcome.kind]}${summary} Carry on with the user from here.`;
30
+ }
31
+ /**
32
+ * A copy of `frame` for a hook or an event, so a host that changes it changes neither the stack
33
+ * nor what is saved. Frames are JSON.
34
+ */
35
+ export const frameCopy = (frame) => structuredClone(frame);
36
+ const isOutcome = (value) => {
37
+ const o = value;
38
+ return (!!o &&
39
+ typeof o === 'object' &&
40
+ OUTCOME_KINDS.includes(o.kind) &&
41
+ (o.message === undefined || typeof o.message === 'string'));
42
+ };
43
+ /** The saved frames that are frames, oldest first. The snapshot is parsed data. */
44
+ export function parseHandOffFrames(saved) {
45
+ if (!Array.isArray(saved))
46
+ return [];
47
+ return saved.filter((value) => {
48
+ const f = value;
49
+ return (!!f &&
50
+ typeof f === 'object' &&
51
+ typeof f.id === 'string' &&
52
+ typeof f.caller === 'string' &&
53
+ typeof f.target === 'string' &&
54
+ typeof f.returnTo === 'boolean' &&
55
+ typeof f.message === 'string' &&
56
+ typeof f.startedAt === 'string' &&
57
+ (f.outcome === undefined || isOutcome(f.outcome)));
58
+ });
59
+ }