@genesislcap/ai-assistant 15.84.2 → 15.85.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 +2175 -138
- package/dist/ai-assistant.d.ts +420 -43
- package/dist/chat-driver.cjs +780 -210
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +780 -209
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +2902 -1812
- package/dist/dts/components/agent-session/agent-session.d.ts +50 -2
- package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +59 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +13 -1
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.constants.d.ts +0 -8
- package/dist/dts/components/chat-driver/chat-driver.constants.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +74 -15
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/hand-off.d.ts +20 -0
- package/dist/dts/components/orchestrating-driver/hand-off.d.ts.map +1 -0
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +121 -22
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/config/config.d.ts +66 -0
- package/dist/dts/config/config.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +3 -0
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.types.d.ts +6 -0
- package/dist/dts/main/main.types.d.ts.map +1 -1
- package/dist/dts/react.d.ts +4 -1
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-snapshot.d.ts +33 -1
- package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
- package/dist/dts/utils/condense-history.d.ts +10 -3
- package/dist/dts/utils/condense-history.d.ts.map +1 -1
- package/dist/esm/components/agent-session/agent-session.js +128 -7
- package/dist/esm/components/ai-driver/ai-driver.js +3 -0
- package/dist/esm/components/ai-driver/base-driver-decorator.js +24 -0
- package/dist/esm/components/chat-driver/chat-driver.constants.js +0 -8
- package/dist/esm/components/chat-driver/chat-driver.js +247 -74
- package/dist/esm/components/orchestrating-driver/hand-off.js +59 -0
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +480 -299
- package/dist/esm/main/main.js +32 -6
- package/dist/esm/main/main.types.js +6 -0
- package/dist/esm/state/debug-event-log.js +1 -1
- package/dist/esm/state/persistence/session-persister.js +55 -8
- package/dist/esm/state/persistence/session-snapshot.js +12 -6
- package/dist/esm/utils/condense-history.js +13 -4
- package/dist/react.cjs +16 -1
- package/dist/react.mjs +16 -1
- package/docs/sub_agent.md +1 -1
- 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,
|
|
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
|
|
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
|
|
335
|
-
* config before installing it (the
|
|
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
|
|
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
|
-
|
|
795
|
-
|
|
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
|
|
807
|
-
*
|
|
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
|
-
|
|
1643
|
+
const registered = rebaseRestoredCondensePolicy(entry, clocks);
|
|
1644
|
+
this.condensePolicies.set(entry.toolCallId, registered);
|
|
1517
1645
|
restored += 1;
|
|
1518
|
-
|
|
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(${(
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2949
|
-
|
|
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 = ((
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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:
|
|
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' && ((
|
|
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
|
-
...((
|
|
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
|
-
...((
|
|
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
|
|
3328
|
-
//
|
|
3329
|
-
//
|
|
3330
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|