@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
package/dist/chat-driver.cjs
CHANGED
|
@@ -45,7 +45,6 @@ __export(chat_driver_node_exports, {
|
|
|
45
45
|
OrchestratingDriver: () => OrchestratingDriver,
|
|
46
46
|
PROVIDER_REFUSED_CODE: () => PROVIDER_REFUSED_CODE,
|
|
47
47
|
ProviderRefusedError: () => ProviderRefusedError,
|
|
48
|
-
REQUEST_CONTINUATION_TOOL: () => REQUEST_CONTINUATION_TOOL,
|
|
49
48
|
SUPPORTED_ANTHROPIC_MODEL_IDS: () => SUPPORTED_ANTHROPIC_MODEL_IDS,
|
|
50
49
|
SUPPORTED_GEMINI_MODEL_IDS: () => SUPPORTED_GEMINI_MODEL_IDS,
|
|
51
50
|
UNROUTABLE_INSTRUCTION: () => UNROUTABLE_INSTRUCTION,
|
|
@@ -4493,7 +4492,7 @@ function toolCallIdsIn(history) {
|
|
|
4493
4492
|
}
|
|
4494
4493
|
return ids;
|
|
4495
4494
|
}
|
|
4496
|
-
function snapshotCondensePolicies(policies, clocks, history) {
|
|
4495
|
+
function snapshotCondensePolicies(policies, clocks, history, suspended = /* @__PURE__ */ new Map()) {
|
|
4497
4496
|
if (policies.size === 0) return [];
|
|
4498
4497
|
const present = toolCallIdsIn(history);
|
|
4499
4498
|
const out = [];
|
|
@@ -4508,8 +4507,11 @@ function snapshotCondensePolicies(policies, clocks, history) {
|
|
|
4508
4507
|
// the clamp only stops a malformed entry from being written out as "the future".
|
|
4509
4508
|
modelCalls: Math.max(0, clocks.modelCall - entry.iteration),
|
|
4510
4509
|
turns: Math.max(0, clocks.turn - entry.turn),
|
|
4511
|
-
activations: Math.max(0, clocks.activation - entry.activation),
|
|
4512
|
-
phases: Math.max(
|
|
4510
|
+
activations: suspended.has(entry.activation) ? 0 : Math.max(0, clocks.activation - entry.activation),
|
|
4511
|
+
phases: Math.max(
|
|
4512
|
+
0,
|
|
4513
|
+
(suspended.get(entry.activation) ?? clocks.phaseEpoch) - entry.phaseEpoch
|
|
4514
|
+
)
|
|
4513
4515
|
}
|
|
4514
4516
|
});
|
|
4515
4517
|
}
|
|
@@ -4629,7 +4631,7 @@ function applyCondensation(history, policies, ctx, onCondensed) {
|
|
|
4629
4631
|
// backstop, when the whole activation has ended, so a phase boundary that
|
|
4630
4632
|
// is never declared still collapses at `agentEnd` rather than never.
|
|
4631
4633
|
case "phaseEnd":
|
|
4632
|
-
return ctx.phaseEnded(entry.phaseEpoch) || ctx.activationEnded(entry.activation);
|
|
4634
|
+
return ctx.phaseEnded(entry.phaseEpoch, entry.activation) || ctx.activationEnded(entry.activation);
|
|
4633
4635
|
default:
|
|
4634
4636
|
return latestByKey.get(trig.by) !== toolCallId;
|
|
4635
4637
|
}
|
|
@@ -5004,6 +5006,17 @@ var AgentSession = class {
|
|
|
5004
5006
|
this.restoredStates = {};
|
|
5005
5007
|
/** The tail of the activation queue — see {@link AgentSession.activate}. */
|
|
5006
5008
|
this.activation = Promise.resolve();
|
|
5009
|
+
/**
|
|
5010
|
+
* Callers waiting in a hand-off (GENC-1737), oldest first: still live, so never deactivated by
|
|
5011
|
+
* the hand-off, and resumed without `onActivate`. Empty after a reload, when a caller has no live
|
|
5012
|
+
* state and is activated again from its saved state instead.
|
|
5013
|
+
*/
|
|
5014
|
+
this.suspended = [];
|
|
5015
|
+
/**
|
|
5016
|
+
* The `data` of the hand-off that activated each agent (GENC-1737), for its tool handlers: kept
|
|
5017
|
+
* while it is active or waits as a caller, and dropped when its activation ends.
|
|
5018
|
+
*/
|
|
5019
|
+
this.handOffData = /* @__PURE__ */ new Map();
|
|
5007
5020
|
}
|
|
5008
5021
|
/** The active agent, or `undefined` before the first activation and after a release. */
|
|
5009
5022
|
getActive() {
|
|
@@ -5045,6 +5058,84 @@ var AgentSession = class {
|
|
|
5045
5058
|
hasRestoredState(agentName) {
|
|
5046
5059
|
return this.restoredStates[agentName] !== void 0;
|
|
5047
5060
|
}
|
|
5061
|
+
/** The `data` of the hand-off that activated `agentName`, if one did and carried any. */
|
|
5062
|
+
getHandOffData(agentName) {
|
|
5063
|
+
return this.handOffData.get(agentName);
|
|
5064
|
+
}
|
|
5065
|
+
/** Record the `data` a hand-off activated `agentName` with, or restore it; `undefined` drops it. */
|
|
5066
|
+
setHandOffData(agentName, data) {
|
|
5067
|
+
if (data === void 0) this.handOffData.delete(agentName);
|
|
5068
|
+
else this.handOffData.set(agentName, data);
|
|
5069
|
+
}
|
|
5070
|
+
/**
|
|
5071
|
+
* Drop the flow lock and leave the active agent active (GENC-1737): a plain agent holding it with
|
|
5072
|
+
* no hand-off behind it is done once its turn ends.
|
|
5073
|
+
*/
|
|
5074
|
+
dropFlowLock() {
|
|
5075
|
+
if (this.flowOwner === null) return;
|
|
5076
|
+
this.flowOwner = null;
|
|
5077
|
+
this.reportTransition({
|
|
5078
|
+
previousAgent: this.active,
|
|
5079
|
+
switched: false,
|
|
5080
|
+
flowOwnerChangedTo: null
|
|
5081
|
+
});
|
|
5082
|
+
}
|
|
5083
|
+
/** The callers waiting in a hand-off whose state is live, oldest first. */
|
|
5084
|
+
getSuspended() {
|
|
5085
|
+
return this.suspended;
|
|
5086
|
+
}
|
|
5087
|
+
/**
|
|
5088
|
+
* Make `target` active for a hand-off (GENC-1737), holding the flow lock whatever kind of agent
|
|
5089
|
+
* it is, since a waiting caller depends on it. With `suspendCaller` the active agent waits:
|
|
5090
|
+
* it is not deactivated and keeps its state. Without it the active agent is deactivated as on
|
|
5091
|
+
* any swap: a one-way hand-off from a target, whose waiting caller then waits for the new
|
|
5092
|
+
* target. Queued with {@link AgentSession.activate}.
|
|
5093
|
+
*/
|
|
5094
|
+
handOff(target, options) {
|
|
5095
|
+
const run = this.activation.then(
|
|
5096
|
+
() => this.activateNow(target, { lock: true, suspendPrevious: options.suspendCaller })
|
|
5097
|
+
);
|
|
5098
|
+
this.activation = run.catch(() => void 0);
|
|
5099
|
+
return run;
|
|
5100
|
+
}
|
|
5101
|
+
/**
|
|
5102
|
+
* Make a waiting caller active again when its hand-off ends (GENC-1737). One whose state is live
|
|
5103
|
+
* resumes without `onActivate`, since it was never deactivated; after a reload it is activated
|
|
5104
|
+
* from its saved state like any other agent. `lock` keeps the flow lock on it: it held it before,
|
|
5105
|
+
* or another caller is still waiting further down.
|
|
5106
|
+
*/
|
|
5107
|
+
resume(caller, lock) {
|
|
5108
|
+
const run = this.activation.then(async () => {
|
|
5109
|
+
const index = this.suspended.findIndex((a) => a.name === caller.name);
|
|
5110
|
+
if (index === -1) return this.activateNow(caller, { lock });
|
|
5111
|
+
const [live2] = this.suspended.splice(index, 1);
|
|
5112
|
+
const transition = { previousAgent: this.active, switched: true };
|
|
5113
|
+
recordMetaEvent(this.deps.sessionKey(), "agent.handoff", {
|
|
5114
|
+
from: this.active?.name ?? null,
|
|
5115
|
+
to: live2.name,
|
|
5116
|
+
restored: false
|
|
5117
|
+
});
|
|
5118
|
+
this.active = live2;
|
|
5119
|
+
if (lock && this.flowOwner !== caller.name) {
|
|
5120
|
+
this.flowOwner = caller.name;
|
|
5121
|
+
transition.flowOwnerChangedTo = caller.name;
|
|
5122
|
+
}
|
|
5123
|
+
this.reportTransition(transition);
|
|
5124
|
+
return transition;
|
|
5125
|
+
});
|
|
5126
|
+
this.activation = run.catch(() => void 0);
|
|
5127
|
+
return run;
|
|
5128
|
+
}
|
|
5129
|
+
/**
|
|
5130
|
+
* Deactivate the callers left waiting by hand-offs that ended without a hand-back, newest first,
|
|
5131
|
+
* when no teardown follows to do it (GENC-1737).
|
|
5132
|
+
*/
|
|
5133
|
+
async endSuspended() {
|
|
5134
|
+
for (const caller of this.suspended.splice(0).reverse()) {
|
|
5135
|
+
this.handOffData.delete(caller.name);
|
|
5136
|
+
await this.fireDeactivate(caller, "release");
|
|
5137
|
+
}
|
|
5138
|
+
}
|
|
5048
5139
|
/**
|
|
5049
5140
|
* Make `agent` active, firing the lifecycle hooks around the swap: outgoing `onDeactivate`
|
|
5050
5141
|
* first, then incoming `onActivate`. Both are awaited so heavy activation work (a machine
|
|
@@ -5065,7 +5156,11 @@ var AgentSession = class {
|
|
|
5065
5156
|
this.activation = run.catch(() => void 0);
|
|
5066
5157
|
return run;
|
|
5067
5158
|
}
|
|
5068
|
-
|
|
5159
|
+
/**
|
|
5160
|
+
* `lock` takes the flow lock whatever kind of agent it is, and `suspendPrevious` leaves the
|
|
5161
|
+
* outgoing agent waiting instead of deactivating it, as a hand-off does (GENC-1737).
|
|
5162
|
+
*/
|
|
5163
|
+
async activateNow(agent, { lock = false, suspendPrevious = false } = {}) {
|
|
5069
5164
|
const previousAgent = this.active;
|
|
5070
5165
|
const switched = !previousAgent || previousAgent.name !== agent.name;
|
|
5071
5166
|
const transition = { previousAgent, switched };
|
|
@@ -5077,7 +5172,14 @@ var AgentSession = class {
|
|
|
5077
5172
|
restored: this.hasRestoredState(agent.name)
|
|
5078
5173
|
});
|
|
5079
5174
|
}
|
|
5080
|
-
if (switched && previousAgent)
|
|
5175
|
+
if (switched && previousAgent) {
|
|
5176
|
+
if (suspendPrevious) {
|
|
5177
|
+
this.suspended.push(previousAgent);
|
|
5178
|
+
} else {
|
|
5179
|
+
this.handOffData.delete(previousAgent.name);
|
|
5180
|
+
await this.fireDeactivate(previousAgent, "switch");
|
|
5181
|
+
}
|
|
5182
|
+
}
|
|
5081
5183
|
if (switched && agent.onActivate) {
|
|
5082
5184
|
const restored = this.restoredStates[agent.name];
|
|
5083
5185
|
if (restored !== void 0) delete this.restoredStates[agent.name];
|
|
@@ -5094,9 +5196,12 @@ var AgentSession = class {
|
|
|
5094
5196
|
}
|
|
5095
5197
|
}
|
|
5096
5198
|
const hasLifecycleHooks = !!(agent.onActivate || agent.onDeactivate);
|
|
5097
|
-
if (switched && hasLifecycleHooks && this.flowOwner !== agent.name) {
|
|
5199
|
+
if (switched && (hasLifecycleHooks || lock) && this.flowOwner !== agent.name) {
|
|
5098
5200
|
this.flowOwner = agent.name;
|
|
5099
5201
|
transition.flowOwnerChangedTo = agent.name;
|
|
5202
|
+
} else if (switched && !suspendPrevious && this.flowOwner === previousAgent?.name) {
|
|
5203
|
+
this.flowOwner = null;
|
|
5204
|
+
transition.flowOwnerChangedTo = null;
|
|
5100
5205
|
}
|
|
5101
5206
|
this.active = agent;
|
|
5102
5207
|
if (switched) this.reportTransition(transition);
|
|
@@ -5114,6 +5219,7 @@ var AgentSession = class {
|
|
|
5114
5219
|
async release() {
|
|
5115
5220
|
const agent = this.active;
|
|
5116
5221
|
if (!agent) return void 0;
|
|
5222
|
+
this.handOffData.delete(agent.name);
|
|
5117
5223
|
await this.fireDeactivate(agent, "release");
|
|
5118
5224
|
this.active = void 0;
|
|
5119
5225
|
const transition = { previousAgent: agent, switched: true };
|
|
@@ -5151,6 +5257,11 @@ var AgentSession = class {
|
|
|
5151
5257
|
async dispose() {
|
|
5152
5258
|
const agent = this.active;
|
|
5153
5259
|
if (agent) await this.fireDeactivate(agent, "dispose");
|
|
5260
|
+
for (const caller of [...this.suspended].reverse()) {
|
|
5261
|
+
await this.fireDeactivate(caller, "dispose");
|
|
5262
|
+
}
|
|
5263
|
+
this.suspended = [];
|
|
5264
|
+
this.handOffData.clear();
|
|
5154
5265
|
this.lifecycleAbortController.abort();
|
|
5155
5266
|
this.active = void 0;
|
|
5156
5267
|
}
|
|
@@ -5190,8 +5301,6 @@ var HARVESTED_SUBAGENT_EVENTS = /* @__PURE__ */ new Set([
|
|
|
5190
5301
|
"tool.failed",
|
|
5191
5302
|
"tool.unresolved"
|
|
5192
5303
|
]);
|
|
5193
|
-
var REQUEST_CONTINUATION_TOOL = "request_continuation";
|
|
5194
|
-
var HANDOFF_TOOL_RESULT_PLACEHOLDER = "Handoff to another specialist \u2014 routing continues on the next turn.";
|
|
5195
5304
|
|
|
5196
5305
|
// src/utils/tool-fold.ts
|
|
5197
5306
|
var TOOL_FOLD_SYMBOL = Symbol("toolFold");
|
|
@@ -6819,6 +6928,16 @@ var SERVICE_WIDE_REFUSALS = /* @__PURE__ */ new Set([
|
|
|
6819
6928
|
"ACCESS_DENIED",
|
|
6820
6929
|
"VENDOR_UNREACHABLE"
|
|
6821
6930
|
]);
|
|
6931
|
+
var 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.`;
|
|
6932
|
+
var 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.";
|
|
6933
|
+
var jsonCopy2 = (value) => {
|
|
6934
|
+
try {
|
|
6935
|
+
const text = JSON.stringify(value);
|
|
6936
|
+
return text === void 0 ? void 0 : JSON.parse(text);
|
|
6937
|
+
} catch {
|
|
6938
|
+
return void 0;
|
|
6939
|
+
}
|
|
6940
|
+
};
|
|
6822
6941
|
var toolResultImages = (result) => {
|
|
6823
6942
|
if (!result || typeof result !== "object") return void 0;
|
|
6824
6943
|
const candidate = result.attachments;
|
|
@@ -7008,6 +7127,17 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
7008
7127
|
* doesn't leak forward.
|
|
7009
7128
|
*/
|
|
7010
7129
|
this.agentReleaseRequested = false;
|
|
7130
|
+
/**
|
|
7131
|
+
* The `agentEnd` activation of each caller waiting in a hand-off, by agent name, with the phase
|
|
7132
|
+
* epoch it was suspended at (GENC-1737). A waiting caller is suspended, not ended, so neither its
|
|
7133
|
+
* `agentEnd` payloads nor its `phaseEnd` ones (to another agent's `endPhase()`) collapse while the
|
|
7134
|
+
* target runs; when it is installed again its entries move to the new activation and, while
|
|
7135
|
+
* their phase had not ended, to the current epoch. A caller restored from a saved session gets
|
|
7136
|
+
* an activation placeholder below zero, which no live activation uses.
|
|
7137
|
+
*/
|
|
7138
|
+
this.suspendedActivations = /* @__PURE__ */ new Map();
|
|
7139
|
+
/** The last placeholder handed out for a restored caller; see `suspendedActivations`. */
|
|
7140
|
+
this.restoredSuspensionSeq = 0;
|
|
7011
7141
|
/**
|
|
7012
7142
|
* Aborted by `dispose()` on driver teardown (e.g. an agent-config swap).
|
|
7013
7143
|
* Threaded into every provider call as `ChatRequestOptions.signal`, so a
|
|
@@ -7056,8 +7186,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
7056
7186
|
* The stateful-agent contract for this driver: active agent, user pin, flow lock, restored
|
|
7057
7187
|
* state, and the `onActivate` / `onDeactivate` hooks (§7.1).
|
|
7058
7188
|
*
|
|
7059
|
-
* **Public because `OrchestratingDriver` reaches through to it.** The orchestrator
|
|
7060
|
-
* config before installing it (the
|
|
7189
|
+
* **Public because `OrchestratingDriver` reaches through to it.** The orchestrator builds a
|
|
7190
|
+
* config before installing it (the fallback's prompt), so it cannot route through
|
|
7061
7191
|
* {@link ChatDriver.activateAgent} and must drive the session itself — and one shared session is
|
|
7062
7192
|
* the point: two would let `getActiveAgent()` disagree between the outer and inner driver.
|
|
7063
7193
|
* `@internal` rather than private so that reach is legal without six delegating one-liners.
|
|
@@ -7464,19 +7594,15 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
7464
7594
|
this.aiServiceNoticesShown.add(code);
|
|
7465
7595
|
return false;
|
|
7466
7596
|
}
|
|
7467
|
-
/**
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
* meets the wall and says so.
|
|
7471
|
-
*/
|
|
7472
|
-
subAgentWallThisTurn(handingOff = false) {
|
|
7473
|
-
return this.providerRefusedThisTurn || this.budgetExhaustedThisTurn || this.aiServiceErrorThisTurn && !handingOff;
|
|
7597
|
+
/** Whether a sub-agent hit a wall this turn that stops this driver too. */
|
|
7598
|
+
subAgentWallThisTurn() {
|
|
7599
|
+
return this.providerRefusedThisTurn || this.budgetExhaustedThisTurn || this.aiServiceErrorThisTurn;
|
|
7474
7600
|
}
|
|
7475
7601
|
/**
|
|
7476
7602
|
* Ends the turn when a sub-agent hit a wall this turn that stops this driver too: the provider
|
|
7477
7603
|
* refusing the account, the AI service (GENC-1611), or the budget. Checked before each model call,
|
|
7478
|
-
* and again right after a tool batch, before
|
|
7479
|
-
*
|
|
7604
|
+
* and again right after a tool batch, before the iteration cap can report the turn as something
|
|
7605
|
+
* else.
|
|
7480
7606
|
*/
|
|
7481
7607
|
endTurnOnSubAgentWall() {
|
|
7482
7608
|
if (!this.subAgentWallThisTurn()) return void 0;
|
|
@@ -7684,6 +7810,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
7684
7810
|
installAgent(config) {
|
|
7685
7811
|
if (config.name !== this.activeAgentName) {
|
|
7686
7812
|
this.currentActivation += 1;
|
|
7813
|
+
this.resumeSuspendedActivation(config.name);
|
|
7687
7814
|
this.resumeRestoredCondensePolicies(config.name);
|
|
7688
7815
|
}
|
|
7689
7816
|
this.systemPrompt = config.systemPrompt;
|
|
@@ -7885,6 +8012,109 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
7885
8012
|
getAgentReleaseRequested() {
|
|
7886
8013
|
return this.agentReleaseRequested;
|
|
7887
8014
|
}
|
|
8015
|
+
/**
|
|
8016
|
+
* Offer `handOff` and `completeHandOff` to tool handlers, checking each `handOff` with `check`
|
|
8017
|
+
* (GENC-1737). Set by `OrchestratingDriver`, which knows the agents and the hand-off stack and acts
|
|
8018
|
+
* on the request after the call; a bare `ChatDriver` has no other agent to hand to.
|
|
8019
|
+
*
|
|
8020
|
+
* @internal
|
|
8021
|
+
*/
|
|
8022
|
+
setHandOffCheck(check) {
|
|
8023
|
+
this.checkHandOff = check;
|
|
8024
|
+
}
|
|
8025
|
+
/** The `handOff` the most recent call accepted, if any (GENC-1737). */
|
|
8026
|
+
getHandOffRequest() {
|
|
8027
|
+
return this.handOffRequested;
|
|
8028
|
+
}
|
|
8029
|
+
/** What `completeHandOff` handed back in the most recent call, if it was called. */
|
|
8030
|
+
getHandOffCompletion() {
|
|
8031
|
+
return this.handOffCompletion;
|
|
8032
|
+
}
|
|
8033
|
+
/**
|
|
8034
|
+
* A handler's `handOff`: refused once this call has handed off or released, or when `data` is
|
|
8035
|
+
* not JSON, and otherwise checked by the orchestrator. `data` is kept as a JSON copy, since it is
|
|
8036
|
+
* saved with the session and the handler may change its own object afterwards.
|
|
8037
|
+
*/
|
|
8038
|
+
requestHandOff(request) {
|
|
8039
|
+
if (this.handOffRequested) return { ok: false, reason: "already-requested" };
|
|
8040
|
+
if (this.agentReleaseRequested) return { ok: false, reason: "released" };
|
|
8041
|
+
let data;
|
|
8042
|
+
if (request?.data !== void 0) {
|
|
8043
|
+
data = jsonCopy2(request.data);
|
|
8044
|
+
if (data === void 0) return { ok: false, reason: "not-json" };
|
|
8045
|
+
}
|
|
8046
|
+
const accepted = {
|
|
8047
|
+
to: request.to,
|
|
8048
|
+
message: String(request.message ?? ""),
|
|
8049
|
+
...data !== void 0 && { data },
|
|
8050
|
+
...request.returnTo === false && { returnTo: false }
|
|
8051
|
+
};
|
|
8052
|
+
const result = this.checkHandOff(accepted, this.activeAgentName);
|
|
8053
|
+
if (result.ok) this.handOffRequested = accepted;
|
|
8054
|
+
return result;
|
|
8055
|
+
}
|
|
8056
|
+
/**
|
|
8057
|
+
* The note of the closing call a hand-off asks for (GENC-1737), or `undefined` when none: once a
|
|
8058
|
+
* handler's `handOff` is accepted, or `completeHandOff` is called, the rest of that batch runs,
|
|
8059
|
+
* then one model call with no tools, so the agent can say so but not carry on, which a model told
|
|
8060
|
+
* so in a tool result does anyway. Then the turn ends.
|
|
8061
|
+
*/
|
|
8062
|
+
closingCallNote() {
|
|
8063
|
+
if (this.handOffRequested) return handOffClosingNote(this.handOffRequested.to);
|
|
8064
|
+
return this.handOffCompletion ? HAND_BACK_CLOSING_NOTE : void 0;
|
|
8065
|
+
}
|
|
8066
|
+
/**
|
|
8067
|
+
* Suspend the active agent's activation for a hand-off it made (GENC-1737): installing the target
|
|
8068
|
+
* advances the `agentEnd` clock as any swap does, but this caller's payloads stay live.
|
|
8069
|
+
*/
|
|
8070
|
+
suspendActivation(agentName) {
|
|
8071
|
+
if (agentName === this.activeAgentName) {
|
|
8072
|
+
this.suspendedActivations.set(agentName, {
|
|
8073
|
+
activation: this.currentActivation,
|
|
8074
|
+
phaseEpoch: this.currentPhaseEpoch
|
|
8075
|
+
});
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
/**
|
|
8079
|
+
* Suspend restored callers before their condense policies are restored, so entries still live
|
|
8080
|
+
* at save time stay live instead of waiting for "the next activation", which is the target's.
|
|
8081
|
+
*/
|
|
8082
|
+
suspendRestoredActivations(agentNames) {
|
|
8083
|
+
for (const name of agentNames) {
|
|
8084
|
+
this.restoredSuspensionSeq -= 1;
|
|
8085
|
+
this.suspendedActivations.set(name, {
|
|
8086
|
+
activation: this.restoredSuspensionSeq,
|
|
8087
|
+
phaseEpoch: this.currentPhaseEpoch
|
|
8088
|
+
});
|
|
8089
|
+
}
|
|
8090
|
+
}
|
|
8091
|
+
/** End every suspension: the hand-offs were unwound, so their callers' activations are over. */
|
|
8092
|
+
endSuspendedActivations() {
|
|
8093
|
+
this.suspendedActivations.clear();
|
|
8094
|
+
}
|
|
8095
|
+
/**
|
|
8096
|
+
* A caller installed again takes its suspended entries into the activation it resumes in, and
|
|
8097
|
+
* the ones whose phase had not ended into the current phase epoch.
|
|
8098
|
+
*/
|
|
8099
|
+
resumeSuspendedActivation(agentName) {
|
|
8100
|
+
const suspended = this.suspendedActivations.get(agentName);
|
|
8101
|
+
if (suspended === void 0) return;
|
|
8102
|
+
this.suspendedActivations.delete(agentName);
|
|
8103
|
+
for (const entry of this.condensePolicies.values()) {
|
|
8104
|
+
if (entry.activation !== suspended.activation) continue;
|
|
8105
|
+
entry.activation = this.currentActivation;
|
|
8106
|
+
if (entry.phaseEpoch >= suspended.phaseEpoch) entry.phaseEpoch = this.currentPhaseEpoch;
|
|
8107
|
+
}
|
|
8108
|
+
}
|
|
8109
|
+
/**
|
|
8110
|
+
* The suspended activations, each with the phase epoch it was suspended at, for the `agentEnd`
|
|
8111
|
+
* and `phaseEnd` checks and for persistence.
|
|
8112
|
+
*/
|
|
8113
|
+
suspendedClocks() {
|
|
8114
|
+
return new Map(
|
|
8115
|
+
[...this.suspendedActivations.values()].map((s) => [s.activation, s.phaseEpoch])
|
|
8116
|
+
);
|
|
8117
|
+
}
|
|
7888
8118
|
/**
|
|
7889
8119
|
* Act on a `releaseAgent` call from a tool handler: fire `onDeactivate` and drop the flow lock,
|
|
7890
8120
|
* so the next turn starts clean and the persister does not serialize a flow that is over.
|
|
@@ -8023,7 +8253,12 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
8023
8253
|
* @beta
|
|
8024
8254
|
*/
|
|
8025
8255
|
getCondensePoliciesForPersistence(messages) {
|
|
8026
|
-
return snapshotCondensePolicies(
|
|
8256
|
+
return snapshotCondensePolicies(
|
|
8257
|
+
this.condensePolicies,
|
|
8258
|
+
this.condenseClocks(),
|
|
8259
|
+
messages,
|
|
8260
|
+
this.suspendedClocks()
|
|
8261
|
+
);
|
|
8027
8262
|
}
|
|
8028
8263
|
/**
|
|
8029
8264
|
* Re-register policies saved by {@link ChatDriver.getCondensePoliciesForPersistence} against
|
|
@@ -8072,9 +8307,13 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
8072
8307
|
const entry = parsePersistedCondensePolicy(raw);
|
|
8073
8308
|
if (!entry || !present.has(entry.toolCallId)) continue;
|
|
8074
8309
|
if (this.condensePolicies.has(entry.toolCallId)) continue;
|
|
8075
|
-
|
|
8310
|
+
const registered = rebaseRestoredCondensePolicy(entry, clocks);
|
|
8311
|
+
this.condensePolicies.set(entry.toolCallId, registered);
|
|
8076
8312
|
restored += 1;
|
|
8077
|
-
|
|
8313
|
+
const suspended = entry.elapsed.activations === 0 && entry.agent !== void 0 ? this.suspendedActivations.get(entry.agent) : void 0;
|
|
8314
|
+
if (suspended !== void 0) {
|
|
8315
|
+
registered.activation = suspended.activation;
|
|
8316
|
+
} else if (entry.elapsed.activations === 0 && entry.agent !== this.activeAgentName) {
|
|
8078
8317
|
this.condenseAwaitingReactivation.add(entry.toolCallId);
|
|
8079
8318
|
awaitingReactivation += 1;
|
|
8080
8319
|
}
|
|
@@ -8431,6 +8670,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
8431
8670
|
this.subAgentCompletion = void 0;
|
|
8432
8671
|
this.subAgentFailure = void 0;
|
|
8433
8672
|
this.agentReleaseRequested = false;
|
|
8673
|
+
this.handOffRequested = void 0;
|
|
8674
|
+
this.handOffCompletion = void 0;
|
|
8434
8675
|
this.budgetExhaustedThisTurn = false;
|
|
8435
8676
|
this.budgetWallDetail = void 0;
|
|
8436
8677
|
this.budgetWallViaSubAgent = false;
|
|
@@ -8564,6 +8805,12 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
8564
8805
|
this.currentActivation += 1;
|
|
8565
8806
|
},
|
|
8566
8807
|
releaseAgent: () => {
|
|
8808
|
+
if (this.handOffRequested) {
|
|
8809
|
+
logger2.warn(
|
|
8810
|
+
`ChatDriver(${this.activeAgentName ?? "unknown"}): releaseAgent called after handOff \u2014 ignoring; the hand-off ends this agent's turn`
|
|
8811
|
+
);
|
|
8812
|
+
return;
|
|
8813
|
+
}
|
|
8567
8814
|
if (this.agentReleaseRequested) {
|
|
8568
8815
|
logger2.warn(
|
|
8569
8816
|
`ChatDriver(${this.activeAgentName ?? "unknown"}): releaseAgent called more than once \u2014 ignoring`
|
|
@@ -8573,6 +8820,34 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
8573
8820
|
this.agentReleaseRequested = true;
|
|
8574
8821
|
this.currentActivation += 1;
|
|
8575
8822
|
},
|
|
8823
|
+
...this.checkHandOff && {
|
|
8824
|
+
handOff: (request) => this.requestHandOff(request),
|
|
8825
|
+
// A copy, so a handler cannot change what is saved.
|
|
8826
|
+
handOffData: () => jsonCopy2(this.agentSession.getHandOffData(this.activeAgentName ?? "")),
|
|
8827
|
+
completeHandOff: (completion) => {
|
|
8828
|
+
if (this.agentReleaseRequested || this.handOffRequested) {
|
|
8829
|
+
logger2.warn(
|
|
8830
|
+
`ChatDriver(${this.activeAgentName ?? "unknown"}): completeHandOff called after a release or a hand-off \u2014 ignoring`
|
|
8831
|
+
);
|
|
8832
|
+
return;
|
|
8833
|
+
}
|
|
8834
|
+
let data;
|
|
8835
|
+
if (completion?.data !== void 0) {
|
|
8836
|
+
data = jsonCopy2(completion.data);
|
|
8837
|
+
if (data === void 0) {
|
|
8838
|
+
logger2.warn(
|
|
8839
|
+
`ChatDriver(${this.activeAgentName ?? "unknown"}): completeHandOff data cannot be saved as JSON \u2014 handing back without it`
|
|
8840
|
+
);
|
|
8841
|
+
}
|
|
8842
|
+
}
|
|
8843
|
+
this.handOffCompletion = {
|
|
8844
|
+
message: String(completion?.message ?? ""),
|
|
8845
|
+
...data !== void 0 && { data }
|
|
8846
|
+
};
|
|
8847
|
+
this.agentReleaseRequested = true;
|
|
8848
|
+
this.currentActivation += 1;
|
|
8849
|
+
}
|
|
8850
|
+
},
|
|
8576
8851
|
endPhase: () => {
|
|
8577
8852
|
this.currentPhaseEpoch += 1;
|
|
8578
8853
|
},
|
|
@@ -8775,6 +9050,9 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
8775
9050
|
this.beginTurn();
|
|
8776
9051
|
this.subAgentCompletion = void 0;
|
|
8777
9052
|
this.subAgentFailure = void 0;
|
|
9053
|
+
this.agentReleaseRequested = false;
|
|
9054
|
+
this.handOffRequested = void 0;
|
|
9055
|
+
this.handOffCompletion = void 0;
|
|
8778
9056
|
this.budgetExhaustedThisTurn = false;
|
|
8779
9057
|
this.budgetWallDetail = void 0;
|
|
8780
9058
|
this.budgetWallViaSubAgent = false;
|
|
@@ -8842,7 +9120,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
8842
9120
|
let firstLlmCall = !!currentInput;
|
|
8843
9121
|
let pinnedThinkingPolicy;
|
|
8844
9122
|
let thinkingPolicyPinned = false;
|
|
8845
|
-
|
|
9123
|
+
let closingCallMade = false;
|
|
9124
|
+
while (iterations < this.maxToolIterations || this.closingCallNote() && !closingCallMade) {
|
|
8846
9125
|
iterations += 1;
|
|
8847
9126
|
this.modelCallSeq += 1;
|
|
8848
9127
|
if (this.turnController.signal.aborted) {
|
|
@@ -8850,6 +9129,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
8850
9129
|
}
|
|
8851
9130
|
const wall = this.endTurnOnSubAgentWall();
|
|
8852
9131
|
if (wall) return wall;
|
|
9132
|
+
if (closingCallMade) return turnDone();
|
|
9133
|
+
const closing = this.closingCallNote();
|
|
8853
9134
|
const promptCtx = {
|
|
8854
9135
|
agentName: this.activeAgentName ?? "",
|
|
8855
9136
|
history: this.history,
|
|
@@ -8891,13 +9172,17 @@ ${EMPTY_RESPONSE_NUDGE}` : "";
|
|
|
8891
9172
|
const firstCallNote = tailNote && iterations === 1 ? `
|
|
8892
9173
|
|
|
8893
9174
|
${tailNote}` : "";
|
|
8894
|
-
const
|
|
9175
|
+
const closingNote = closing ? `
|
|
9176
|
+
|
|
9177
|
+
${closing}` : "";
|
|
9178
|
+
const frameworkSystemSuffix = `${foldSuffix}${firstCallNote}${closingNote}${retryNudge}`;
|
|
8895
9179
|
const primer = normalizeForProvider([
|
|
8896
9180
|
...this.primerHistory ?? [],
|
|
8897
9181
|
...transientPrimer ?? []
|
|
8898
9182
|
]);
|
|
8899
9183
|
const baseHistory = firstLlmCall ? this.history.slice(0, -1) : this.history;
|
|
8900
9184
|
firstLlmCall = false;
|
|
9185
|
+
const suspended = this.suspendedClocks();
|
|
8901
9186
|
const condensedHistory = applyCondensation(
|
|
8902
9187
|
[...baseHistory],
|
|
8903
9188
|
this.condensePolicies,
|
|
@@ -8906,10 +9191,11 @@ ${tailNote}` : "";
|
|
|
8906
9191
|
turn: this.turnSeq,
|
|
8907
9192
|
// A call's activation has ended once a later activation is current — a
|
|
8908
9193
|
// swap or release/complete both advance the counter.
|
|
8909
|
-
|
|
9194
|
+
// A caller waiting in a hand-off is suspended, not ended (GENC-1737).
|
|
9195
|
+
activationEnded: (activation) => activation < this.currentActivation && !suspended.has(activation),
|
|
8910
9196
|
// A call's phase has ended once the app declared a later phase epoch
|
|
8911
|
-
// current via `endPhase()
|
|
8912
|
-
phaseEnded: (phaseEpoch) => phaseEpoch < this.currentPhaseEpoch,
|
|
9197
|
+
// current via `endPhase()`; a waiting caller's, only by one before it waited.
|
|
9198
|
+
phaseEnded: (phaseEpoch, activation) => phaseEpoch < (suspended.get(activation) ?? this.currentPhaseEpoch),
|
|
8913
9199
|
batchCalls: this.condenseBatchCalls
|
|
8914
9200
|
},
|
|
8915
9201
|
(detail) => recordMetaEvent(this.sessionKey, "context.condensed", detail)
|
|
@@ -8949,7 +9235,7 @@ ${tailNote}` : "";
|
|
|
8949
9235
|
const tailContext = tailBody ? `<system-reminder>
|
|
8950
9236
|
${tailBody}
|
|
8951
9237
|
</system-reminder>` : void 0;
|
|
8952
|
-
const effectiveToolChoice = resolvedToolChoice ?? (this.isSubAgent ? "required" : void 0);
|
|
9238
|
+
const effectiveToolChoice = closing ? "none" : resolvedToolChoice ?? (this.isSubAgent ? "required" : void 0);
|
|
8953
9239
|
const turnSnapshot = this.turnRecorder.record({
|
|
8954
9240
|
agentName: this.activeAgentName,
|
|
8955
9241
|
agentLabel: this.activeAgentLabel,
|
|
@@ -9021,6 +9307,7 @@ ${tailBody}
|
|
|
9021
9307
|
});
|
|
9022
9308
|
return this.endTurnWithFailure(TURN_FAILURE_EXITS.contextGuard);
|
|
9023
9309
|
}
|
|
9310
|
+
if (closing) closingCallMade = true;
|
|
9024
9311
|
let response;
|
|
9025
9312
|
try {
|
|
9026
9313
|
response = await activeProvider.chat(historyForCall, userInputForCall, options);
|
|
@@ -9154,6 +9441,10 @@ ${tailBody}
|
|
|
9154
9441
|
}
|
|
9155
9442
|
turnSnapshot.usage = messageUsage(response);
|
|
9156
9443
|
if (response.model !== void 0) turnSnapshot.model = response.model;
|
|
9444
|
+
if (closing) {
|
|
9445
|
+
if (response.toolCalls?.length) response = { ...response, toolCalls: void 0 };
|
|
9446
|
+
if (!response.content?.trim()) return turnDone();
|
|
9447
|
+
}
|
|
9157
9448
|
const isThinkingStep = response.content && response.toolCalls?.length;
|
|
9158
9449
|
const isEmptyResponse = !response.content?.trim() && !response.toolCalls?.length;
|
|
9159
9450
|
const isRefusal = response.responseMeta?.finishReason === "refusal";
|
|
@@ -9231,17 +9522,11 @@ ${tailBody}
|
|
|
9231
9522
|
emptyResponseAttempts = 0;
|
|
9232
9523
|
malformedAttempts = 0;
|
|
9233
9524
|
setupTransportAttempts = 0;
|
|
9525
|
+
if (closing) return turnDone();
|
|
9234
9526
|
if (!response.toolCalls?.length) {
|
|
9235
9527
|
break;
|
|
9236
9528
|
}
|
|
9237
|
-
const
|
|
9238
|
-
(acc, tc) => {
|
|
9239
|
-
if (tc.name === REQUEST_CONTINUATION_TOOL) acc[1].push(tc);
|
|
9240
|
-
else acc[0].push(tc);
|
|
9241
|
-
return acc;
|
|
9242
|
-
},
|
|
9243
|
-
[[], []]
|
|
9244
|
-
);
|
|
9529
|
+
const toolCalls = response.toolCalls;
|
|
9245
9530
|
const executedById = /* @__PURE__ */ new Map();
|
|
9246
9531
|
const unknownToolIds = /* @__PURE__ */ new Set();
|
|
9247
9532
|
const staleToolIds = /* @__PURE__ */ new Set();
|
|
@@ -9419,28 +9704,20 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
|
|
|
9419
9704
|
}
|
|
9420
9705
|
const foldPath = this.folds.activeNames();
|
|
9421
9706
|
for (const tc of response.toolCalls) {
|
|
9422
|
-
|
|
9707
|
+
const r = executedById.get(tc.id);
|
|
9708
|
+
if (r) {
|
|
9423
9709
|
this.appendToHistory({
|
|
9424
9710
|
role: "tool",
|
|
9425
9711
|
content: "",
|
|
9426
|
-
toolResult: {
|
|
9712
|
+
toolResult: {
|
|
9713
|
+
toolCallId: r.toolCallId,
|
|
9714
|
+
content: r.content,
|
|
9715
|
+
// Images DO persist, unlike the sub-agent trace above: they are the payload
|
|
9716
|
+
// the model must see on every replay of this turn, not a one-off UI artefact.
|
|
9717
|
+
// `condenseWhen` is what sheds them once they are spent — see condense-history.
|
|
9718
|
+
...r.attachments ? { attachments: r.attachments } : {}
|
|
9719
|
+
}
|
|
9427
9720
|
});
|
|
9428
|
-
} else {
|
|
9429
|
-
const r = executedById.get(tc.id);
|
|
9430
|
-
if (r) {
|
|
9431
|
-
this.appendToHistory({
|
|
9432
|
-
role: "tool",
|
|
9433
|
-
content: "",
|
|
9434
|
-
toolResult: {
|
|
9435
|
-
toolCallId: r.toolCallId,
|
|
9436
|
-
content: r.content,
|
|
9437
|
-
// Images DO persist, unlike the sub-agent trace above: they are the payload
|
|
9438
|
-
// the model must see on every replay of this turn, not a one-off UI artefact.
|
|
9439
|
-
// `condenseWhen` is what sheds them once they are spent — see condense-history.
|
|
9440
|
-
...r.attachments ? { attachments: r.attachments } : {}
|
|
9441
|
-
}
|
|
9442
|
-
});
|
|
9443
|
-
}
|
|
9444
9721
|
}
|
|
9445
9722
|
}
|
|
9446
9723
|
let tcMsgIdx = -1;
|
|
@@ -9504,8 +9781,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
|
|
|
9504
9781
|
});
|
|
9505
9782
|
return this.endTurnWithFailure(TURN_FAILURE_EXITS.unknownToolLimit);
|
|
9506
9783
|
}
|
|
9507
|
-
|
|
9508
|
-
if (!this.subAgentCompletion && this.subAgentWallThisTurn(!!firstContinuation)) {
|
|
9784
|
+
if (!this.subAgentCompletion && this.subAgentWallThisTurn()) {
|
|
9509
9785
|
if (this.turnController.signal.aborted) return this.completeAbortedTurn();
|
|
9510
9786
|
const wallAfterBatch = this.endTurnOnSubAgentWall();
|
|
9511
9787
|
if (wallAfterBatch) return wallAfterBatch;
|
|
@@ -9514,10 +9790,6 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
|
|
|
9514
9790
|
this.noticeAIServiceRefusal(this.aiServiceWall.code);
|
|
9515
9791
|
this.flushAIServiceNotices();
|
|
9516
9792
|
}
|
|
9517
|
-
if (firstContinuation) {
|
|
9518
|
-
const { summary, remaining_task: remainingTask } = firstContinuation.args;
|
|
9519
|
-
return { reason: "agent-handoff", summary, remainingTask };
|
|
9520
|
-
}
|
|
9521
9793
|
if (this.subAgentCompletion) {
|
|
9522
9794
|
return turnDone();
|
|
9523
9795
|
}
|
|
@@ -9593,6 +9865,9 @@ var AI_DRIVER_EVENT_TYPES = Object.keys({
|
|
|
9593
9865
|
"agent-changed": true,
|
|
9594
9866
|
"agent-released": true,
|
|
9595
9867
|
"flow-owner-changed": true,
|
|
9868
|
+
"handoff-started": true,
|
|
9869
|
+
"handoff-returned": true,
|
|
9870
|
+
"handoff-unwound": true,
|
|
9596
9871
|
"orchestrating-start": true,
|
|
9597
9872
|
"orchestrating-stop": true
|
|
9598
9873
|
});
|
|
@@ -9673,6 +9948,24 @@ var BaseDriverDecorator = class _BaseDriverDecorator extends EventTarget {
|
|
|
9673
9948
|
restoreCondensePolicies(entries) {
|
|
9674
9949
|
return this.inner.restoreCondensePolicies?.(entries) ?? { restored: 0, awaitingReactivation: 0 };
|
|
9675
9950
|
}
|
|
9951
|
+
getHandOffStack() {
|
|
9952
|
+
return this.inner.getHandOffStack?.() ?? [];
|
|
9953
|
+
}
|
|
9954
|
+
restoreHandOffStack(saved, savedStates, interrupted) {
|
|
9955
|
+
return this.inner.restoreHandOffStack?.(saved, savedStates, interrupted) ?? {};
|
|
9956
|
+
}
|
|
9957
|
+
unwindHandOffs(kind) {
|
|
9958
|
+
return this.inner.unwindHandOffs?.(kind) ?? Promise.resolve();
|
|
9959
|
+
}
|
|
9960
|
+
serializeWaitingCallers() {
|
|
9961
|
+
return this.inner.serializeWaitingCallers?.() ?? {};
|
|
9962
|
+
}
|
|
9963
|
+
getHandOffData() {
|
|
9964
|
+
return this.inner.getHandOffData?.() ?? {};
|
|
9965
|
+
}
|
|
9966
|
+
restoreHandOffData(saved, interrupted) {
|
|
9967
|
+
this.inner.restoreHandOffData?.(saved, interrupted);
|
|
9968
|
+
}
|
|
9676
9969
|
getActiveAgent() {
|
|
9677
9970
|
return this.inner.getActiveAgent();
|
|
9678
9971
|
}
|
|
@@ -9781,36 +10074,64 @@ var BaseDriverDecorator = class _BaseDriverDecorator extends EventTarget {
|
|
|
9781
10074
|
}
|
|
9782
10075
|
};
|
|
9783
10076
|
|
|
10077
|
+
// src/components/orchestrating-driver/hand-off.ts
|
|
10078
|
+
var OUTCOME_KINDS = [
|
|
10079
|
+
"completed",
|
|
10080
|
+
"released",
|
|
10081
|
+
"cleared",
|
|
10082
|
+
"agent-removed",
|
|
10083
|
+
"state-lost"
|
|
10084
|
+
];
|
|
10085
|
+
function handOffNote(caller) {
|
|
10086
|
+
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.`;
|
|
10087
|
+
}
|
|
10088
|
+
function handBackNote(frame, outcome) {
|
|
10089
|
+
const how = {
|
|
10090
|
+
completed: `${frame.target} has finished the request you handed it.`,
|
|
10091
|
+
released: `${frame.target} has finished the request you handed it, without a summary. Its last reply is above.`,
|
|
10092
|
+
cleared: "The hand-off was cleared.",
|
|
10093
|
+
"agent-removed": `${frame.target} is no longer available, so the request you handed it was not finished.`,
|
|
10094
|
+
"state-lost": `The hand-off to ${frame.target} could not be resumed.`
|
|
10095
|
+
};
|
|
10096
|
+
const summary = outcome.kind === "completed" && outcome.message ? " Its summary is the message above." : "";
|
|
10097
|
+
return `[Hand-back]: ${how[outcome.kind]}${summary} Carry on with the user from here.`;
|
|
10098
|
+
}
|
|
10099
|
+
var frameCopy = (frame) => structuredClone(frame);
|
|
10100
|
+
var isOutcome = (value) => {
|
|
10101
|
+
const o = value;
|
|
10102
|
+
return !!o && typeof o === "object" && OUTCOME_KINDS.includes(o.kind) && (o.message === void 0 || typeof o.message === "string");
|
|
10103
|
+
};
|
|
10104
|
+
function parseHandOffFrames(saved) {
|
|
10105
|
+
if (!Array.isArray(saved)) return [];
|
|
10106
|
+
return saved.filter((value) => {
|
|
10107
|
+
const f = value;
|
|
10108
|
+
return !!f && typeof f === "object" && typeof f.id === "string" && typeof f.caller === "string" && typeof f.target === "string" && typeof f.returnTo === "boolean" && typeof f.message === "string" && typeof f.startedAt === "string" && (f.outcome === void 0 || isOutcome(f.outcome));
|
|
10109
|
+
});
|
|
10110
|
+
}
|
|
10111
|
+
|
|
9784
10112
|
// src/components/orchestrating-driver/orchestrating-driver.ts
|
|
9785
10113
|
var DEFAULT_MAX_HANDOFFS = 3;
|
|
10114
|
+
var DEFAULT_MAX_HAND_OFF_DEPTH = 1;
|
|
9786
10115
|
var DEFAULT_CLASSIFIER_HISTORY_LENGTH = 4;
|
|
9787
10116
|
var DEFAULT_CLASSIFIER_RETRIES = 2;
|
|
9788
10117
|
var CLASSIFIER_MESSAGE_CHARS = 600;
|
|
9789
10118
|
var CLASSIFIER_TRANSCRIPT_CHARS = 3e3;
|
|
9790
|
-
var REQUEST_CONTINUATION_DEFINITION = {
|
|
9791
|
-
name: REQUEST_CONTINUATION_TOOL,
|
|
9792
|
-
description: "Call this when you have completed your part of the task but fulfilling the user's full request requires capabilities outside your domain. Do not call this if you can complete the request yourself. Pass a plain-language description of what still needs to be done \u2014 another specialist will be selected automatically.",
|
|
9793
|
-
parameters: {
|
|
9794
|
-
type: "object",
|
|
9795
|
-
required: ["summary", "remaining_task"],
|
|
9796
|
-
properties: {
|
|
9797
|
-
summary: {
|
|
9798
|
-
type: "string",
|
|
9799
|
-
description: "What you found or did \u2014 passed as context to the next specialist."
|
|
9800
|
-
},
|
|
9801
|
-
remaining_task: {
|
|
9802
|
-
type: "string",
|
|
9803
|
-
description: "What still needs to be done, in plain language. Used to route to the right specialist."
|
|
9804
|
-
}
|
|
9805
|
-
}
|
|
9806
|
-
}
|
|
9807
|
-
};
|
|
9808
10119
|
function isSpecialist(agent) {
|
|
9809
10120
|
return !agent.fallback;
|
|
9810
10121
|
}
|
|
9811
10122
|
function isFallback(agent) {
|
|
9812
10123
|
return agent.fallback === true;
|
|
9813
10124
|
}
|
|
10125
|
+
function isStateful(agent) {
|
|
10126
|
+
return !!(agent.onActivate || agent.onDeactivate);
|
|
10127
|
+
}
|
|
10128
|
+
async function callHandOffHook(agent, hook, run) {
|
|
10129
|
+
try {
|
|
10130
|
+
await run();
|
|
10131
|
+
} catch (e) {
|
|
10132
|
+
logger2.warn(`OrchestratingDriver: ${hook}("${agent}") threw:`, e);
|
|
10133
|
+
}
|
|
10134
|
+
}
|
|
9814
10135
|
function normalizeContextReserve(input) {
|
|
9815
10136
|
if (input == null) return void 0;
|
|
9816
10137
|
const positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
@@ -9843,37 +10164,40 @@ Politely let the user know what you can help with and invite them to rephrase th
|
|
|
9843
10164
|
|
|
9844
10165
|
${UNROUTABLE_INSTRUCTION}`;
|
|
9845
10166
|
}
|
|
9846
|
-
function handoffTailNote(agent, remainingTask) {
|
|
9847
|
-
if (isFallback(agent)) {
|
|
9848
|
-
return "[Hand-off]: no specialist here can take on what remains. Reply to the user now, as the routing note says.";
|
|
9849
|
-
}
|
|
9850
|
-
const work = remainingTask?.trim();
|
|
9851
|
-
return work ? `[Hand-off]: the previous agent passed the rest of this request to you. What remains: ${work}` : "[Hand-off]: the previous agent passed the rest of this request to you without saying what remains. Carry on with the user's request above.";
|
|
9852
|
-
}
|
|
9853
10167
|
var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator {
|
|
9854
10168
|
constructor(providerRegistry, agents, options = {}) {
|
|
9855
10169
|
super(_OrchestratingDriver.createInner(providerRegistry, agents, options));
|
|
9856
10170
|
this.providerRegistry = providerRegistry;
|
|
9857
10171
|
this.agents = agents;
|
|
9858
10172
|
/**
|
|
9859
|
-
* Set by `cancel()` for the duration of a
|
|
9860
|
-
*
|
|
9861
|
-
* user stops. Reset at the start of each
|
|
10173
|
+
* Set by `cancel()` for the duration of a run. Stops it before the next agent
|
|
10174
|
+
* runs — a hand-off's target or a hand-back's caller — so nothing more is asked
|
|
10175
|
+
* of a provider after the user stops. Reset at the start of each run.
|
|
9862
10176
|
*/
|
|
9863
10177
|
this.cancelled = false;
|
|
9864
10178
|
/**
|
|
9865
10179
|
* Whether the CURRENT turn's user message has reached the inner driver's
|
|
9866
10180
|
* history. False through the pre-first-turn classify (where the only echo of
|
|
9867
10181
|
* the message is an optimistic `history-updated` dispatch), true from the
|
|
9868
|
-
* moment `chatDriver.sendMessage` is entered
|
|
9869
|
-
*
|
|
10182
|
+
* moment `chatDriver.sendMessage` is entered. Read by the budget-wall catch in
|
|
10183
|
+
* `sendMessage` to decide
|
|
9870
10184
|
* whether `reportBudgetExhausted` must append the message itself: appending
|
|
9871
10185
|
* it when already appended duplicated it; not appending it when unappended
|
|
9872
10186
|
* made it vanish. Reset at the top of each `runOrchestratedTurn`.
|
|
9873
10187
|
*/
|
|
9874
10188
|
this.userMessageAppended = false;
|
|
10189
|
+
/**
|
|
10190
|
+
* The hand-offs in progress, oldest first (GENC-1737). The top frame's target is the active agent
|
|
10191
|
+
* and holds the flow lock; each caller waits, suspended, for the target above it. Saved with the
|
|
10192
|
+
* session through {@link OrchestratingDriver.getHandOffStack}.
|
|
10193
|
+
*/
|
|
10194
|
+
this.handOffStack = [];
|
|
10195
|
+
/** Hand-offs and hand-backs this user turn, against `maxHandoffs`. */
|
|
10196
|
+
this.handOffsThisTurn = 0;
|
|
9875
10197
|
this.sessionKey = options.sessionKey ?? "";
|
|
9876
10198
|
this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS;
|
|
10199
|
+
this.maxHandOffDepth = options.maxHandOffDepth ?? DEFAULT_MAX_HAND_OFF_DEPTH;
|
|
10200
|
+
this.chatDriver.setHandOffCheck((request, caller) => this.checkHandOff(request, caller));
|
|
9877
10201
|
this.classifierHistoryLength = options.classifierHistoryLength ?? DEFAULT_CLASSIFIER_HISTORY_LENGTH;
|
|
9878
10202
|
this.classifierRetries = options.classifierRetries ?? DEFAULT_CLASSIFIER_RETRIES;
|
|
9879
10203
|
this.specialists = agents.filter(isSpecialist).filter((a) => !a.excludeFromClassifier);
|
|
@@ -9926,7 +10250,7 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
9926
10250
|
activityBus: options.activityBus,
|
|
9927
10251
|
budgetExhaustedMessage: options.budgetExhaustedMessage,
|
|
9928
10252
|
aiServiceErrorMessages: options.aiServiceErrorMessages,
|
|
9929
|
-
// A release
|
|
10253
|
+
// A release can hand back to a waiting caller, so this driver acts on the flag itself.
|
|
9930
10254
|
deferAgentRelease: true
|
|
9931
10255
|
});
|
|
9932
10256
|
}
|
|
@@ -9966,6 +10290,8 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
9966
10290
|
async runOrchestratedTurn(input, attachments, options, delegated) {
|
|
9967
10291
|
this.cancelled = false;
|
|
9968
10292
|
this.userMessageAppended = false;
|
|
10293
|
+
this.handOffsThisTurn = 0;
|
|
10294
|
+
if (this.pendingHandBack?.resumed) this.pendingHandBack = void 0;
|
|
9969
10295
|
const history = this.chatDriver.getHistory();
|
|
9970
10296
|
this.dispatchEvent(
|
|
9971
10297
|
new CustomEvent("history-updated", {
|
|
@@ -9983,66 +10309,16 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
9983
10309
|
} else {
|
|
9984
10310
|
routed = await this.classify(routingInput, history, this.activeAgent);
|
|
9985
10311
|
}
|
|
9986
|
-
let currentAgent = routed.agent;
|
|
9987
|
-
let isHandoff = false;
|
|
9988
|
-
let handoffs = 0;
|
|
9989
|
-
let handoffSummary = "";
|
|
9990
|
-
let remainingTask = "";
|
|
9991
|
-
const attempted = [];
|
|
9992
10312
|
let lastResult;
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
await this.installAgent(
|
|
9996
|
-
if (this.cancelled)
|
|
9997
|
-
let result;
|
|
9998
|
-
if (isHandoff) {
|
|
9999
|
-
const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
|
|
10000
|
-
if (isFallback(currentAgent)) {
|
|
10001
|
-
const work = remainingTask?.trim() || handoffSummary?.trim();
|
|
10002
|
-
contextPrimer.push({
|
|
10003
|
-
role: "user",
|
|
10004
|
-
content: (work ? `[Routing note]: no specialist is available for the remaining work: ${work}
|
|
10005
|
-
` : "[Routing note]: no specialist is available for the work that remains.\n") + "Tell the user plainly what was established, that this part falls outside what any specialist here can do, and what they could do next. Do not claim you will do it yourself, and do not return an empty response."
|
|
10006
|
-
});
|
|
10007
|
-
}
|
|
10008
|
-
result = await this.chatDriver.continueFromHistory(contextPrimer, {
|
|
10009
|
-
tailNote: handoffTailNote(currentAgent, remainingTask)
|
|
10010
|
-
});
|
|
10011
|
-
} else {
|
|
10313
|
+
if (!this.cancelled) {
|
|
10314
|
+
await this.endOrphanedHandOffs(routed.agent);
|
|
10315
|
+
await this.installAgent(routed.agent);
|
|
10316
|
+
if (!this.cancelled) {
|
|
10012
10317
|
this.userMessageAppended = true;
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
lastResult = result;
|
|
10016
|
-
if (this.chatDriver.getAgentReleaseRequested()) {
|
|
10017
|
-
await this.releaseActiveAgent();
|
|
10018
|
-
break;
|
|
10019
|
-
}
|
|
10020
|
-
if (result.reason !== "agent-handoff" || isFallback(currentAgent) || pinned) {
|
|
10021
|
-
break;
|
|
10022
|
-
}
|
|
10023
|
-
if (this.cancelled) break;
|
|
10024
|
-
handoffs += 1;
|
|
10025
|
-
if (handoffs > this.maxHandoffs) {
|
|
10026
|
-
this.appendInlineMessage(
|
|
10027
|
-
`I wasn't able to fully complete your request \u2014 the task required more hand-offs between specialists than allowed. Please try breaking your request into smaller steps.`
|
|
10318
|
+
lastResult = await this.carryOn(
|
|
10319
|
+
await this.chatDriver.sendMessage(input, attachments, options)
|
|
10028
10320
|
);
|
|
10029
|
-
break;
|
|
10030
10321
|
}
|
|
10031
|
-
handoffSummary = result.summary;
|
|
10032
|
-
remainingTask = result.remainingTask;
|
|
10033
|
-
isHandoff = true;
|
|
10034
|
-
if (isSpecialist(currentAgent) && !attempted.includes(currentAgent.name)) {
|
|
10035
|
-
attempted.push(currentAgent.name);
|
|
10036
|
-
}
|
|
10037
|
-
const updatedHistory = this.chatDriver.getHistory();
|
|
10038
|
-
this.dispatchEvent(new CustomEvent("orchestrating-start"));
|
|
10039
|
-
routed = await this.classify(
|
|
10040
|
-
remainingTask?.trim() || routingInput,
|
|
10041
|
-
updatedHistory,
|
|
10042
|
-
void 0,
|
|
10043
|
-
attempted
|
|
10044
|
-
);
|
|
10045
|
-
currentAgent = routed.agent;
|
|
10046
10322
|
}
|
|
10047
10323
|
if (this.cancelled && !this.userMessageAppended) {
|
|
10048
10324
|
this.chatDriver.reportStoppedBeforeTurn({
|
|
@@ -10052,7 +10328,74 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
10052
10328
|
...delegated
|
|
10053
10329
|
});
|
|
10054
10330
|
}
|
|
10055
|
-
return lastResult
|
|
10331
|
+
return lastResult ?? { reason: "done" };
|
|
10332
|
+
}
|
|
10333
|
+
/**
|
|
10334
|
+
* Act on what the agent call that just ended asked for, and run the next agent in the same user
|
|
10335
|
+
* turn while there is one (GENC-1737): a `handOff` runs its target with the brief, and a target
|
|
10336
|
+
* that releases hands back to its caller. Returns the last call's result.
|
|
10337
|
+
*
|
|
10338
|
+
* A failed call ends the turn there, as a Stop does: what it asked for still happens, and the
|
|
10339
|
+
* next agent runs on the next message. Running it now would meet the same wall (a budget, a
|
|
10340
|
+
* refused account, a full context window), or build on a reply that never came.
|
|
10341
|
+
*/
|
|
10342
|
+
async carryOn(result) {
|
|
10343
|
+
let last = result;
|
|
10344
|
+
let tailNote = await this.afterAgentCall();
|
|
10345
|
+
while (tailNote !== void 0 && !this.cancelled && !last.failureReason) {
|
|
10346
|
+
last = await this.chatDriver.continueFromHistory(void 0, { tailNote });
|
|
10347
|
+
tailNote = await this.afterAgentCall();
|
|
10348
|
+
}
|
|
10349
|
+
return last;
|
|
10350
|
+
}
|
|
10351
|
+
/**
|
|
10352
|
+
* After one agent call: start the hand-off it asked for, or release it and, when it was a
|
|
10353
|
+
* hand-off's target, hand back. Returns the tail note for the agent to run next, or `undefined`
|
|
10354
|
+
* when the turn ends here.
|
|
10355
|
+
*
|
|
10356
|
+
* A plain target hands back when its call ends, released or not, as a plain agent is done once
|
|
10357
|
+
* it has answered. A stateful one holds the hand-off until it releases, as it holds its flow.
|
|
10358
|
+
*/
|
|
10359
|
+
async afterAgentCall() {
|
|
10360
|
+
const request = this.chatDriver.getHandOffRequest();
|
|
10361
|
+
if (request) return this.startHandOff(request);
|
|
10362
|
+
const active = this.agentSession.getActive();
|
|
10363
|
+
if (!active) return void 0;
|
|
10364
|
+
const frame = this.topFrameFor(active.name);
|
|
10365
|
+
const released = this.chatDriver.getAgentReleaseRequested();
|
|
10366
|
+
if (!released && !(frame && !isStateful(active))) {
|
|
10367
|
+
if (!isStateful(active)) this.endPlainTurn(active);
|
|
10368
|
+
return void 0;
|
|
10369
|
+
}
|
|
10370
|
+
if (!frame) {
|
|
10371
|
+
await this.releaseActiveAgent();
|
|
10372
|
+
return void 0;
|
|
10373
|
+
}
|
|
10374
|
+
frame.outcome = this.outcomeOf(frame);
|
|
10375
|
+
await this.releaseActiveAgent();
|
|
10376
|
+
return this.handBack(frame, frame.outcome);
|
|
10377
|
+
}
|
|
10378
|
+
/** How the target of `frame` finished: with `completeHandOff`, or a release and its last reply. */
|
|
10379
|
+
outcomeOf(frame) {
|
|
10380
|
+
const completion = this.chatDriver.getHandOffCompletion();
|
|
10381
|
+
if (completion) {
|
|
10382
|
+
return {
|
|
10383
|
+
kind: "completed",
|
|
10384
|
+
message: completion.message,
|
|
10385
|
+
...completion.data !== void 0 && { data: completion.data }
|
|
10386
|
+
};
|
|
10387
|
+
}
|
|
10388
|
+
const finalReply = this.lastReply(frame);
|
|
10389
|
+
return { kind: "released", ...finalReply ? { message: finalReply } : {} };
|
|
10390
|
+
}
|
|
10391
|
+
/**
|
|
10392
|
+
* A plain agent is done once it has answered (GENC-1737): the data a one-way hand-off gave it ends
|
|
10393
|
+
* with its turn, and so does a flow lock no hand-off holds, such as one a restore left on a
|
|
10394
|
+
* target whose hand-off it could not resume.
|
|
10395
|
+
*/
|
|
10396
|
+
endPlainTurn(agent) {
|
|
10397
|
+
this.agentSession.setHandOffData(agent.name, void 0);
|
|
10398
|
+
if (this.agentSession.getFlowOwner() === agent.name) this.agentSession.dropFlowLock();
|
|
10056
10399
|
}
|
|
10057
10400
|
/** {@inheritDoc AiDriver.canRestoreFlowOwner} */
|
|
10058
10401
|
canRestoreFlowOwner(agentName) {
|
|
@@ -10076,15 +10419,33 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
10076
10419
|
* undecorated.
|
|
10077
10420
|
*/
|
|
10078
10421
|
async resumeRestoredFlow(resumeNote) {
|
|
10422
|
+
if (this.pendingHandBack) return this.resumeHandBack(resumeNote);
|
|
10079
10423
|
const owner = this.agentSession.getFlowOwner();
|
|
10080
10424
|
const agent = owner ? this.agents.find((a) => a.name === owner) : void 0;
|
|
10081
10425
|
if (!agent) return void 0;
|
|
10082
10426
|
this.cancelled = false;
|
|
10427
|
+
this.handOffsThisTurn = 0;
|
|
10083
10428
|
await this.installAgent(agent);
|
|
10084
10429
|
if (!resumeNote || agent.resumeOnRestore === false || this.cancelled) return void 0;
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10430
|
+
return this.carryOn(
|
|
10431
|
+
await this.chatDriver.continueFromHistory(void 0, { tailNote: resumeNote })
|
|
10432
|
+
);
|
|
10433
|
+
}
|
|
10434
|
+
/**
|
|
10435
|
+
* The hand-back a restore found due (GENC-1737). The first call, inside the host's restore gate,
|
|
10436
|
+
* makes the caller active again and tells it the outcome; the call with the note runs its turn.
|
|
10437
|
+
*/
|
|
10438
|
+
async resumeHandBack(note) {
|
|
10439
|
+
const pending = this.pendingHandBack;
|
|
10440
|
+
this.cancelled = false;
|
|
10441
|
+
if (!pending.resumed) {
|
|
10442
|
+
pending.resumed = true;
|
|
10443
|
+
await this.handBack(pending.frame, pending.frame.outcome);
|
|
10444
|
+
}
|
|
10445
|
+
if (!note || this.cancelled) return void 0;
|
|
10446
|
+
this.pendingHandBack = void 0;
|
|
10447
|
+
this.handOffsThisTurn = 0;
|
|
10448
|
+
return this.carryOn(await this.chatDriver.continueFromHistory(void 0, { tailNote: note }));
|
|
10088
10449
|
}
|
|
10089
10450
|
/**
|
|
10090
10451
|
* A continuation outside a routed turn — a decorator carrying the agent on, say — can finish the
|
|
@@ -10092,15 +10453,9 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
10092
10453
|
* (GENC-1660), only `sendMessage` checked, and a flow finished by a continuation stayed locked.
|
|
10093
10454
|
*/
|
|
10094
10455
|
async continueFromHistory(transientPrimer, options) {
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
return
|
|
10098
|
-
}
|
|
10099
|
-
/** Release the flow if the turn that just ended asked to, and an agent is still active to release. */
|
|
10100
|
-
async releaseIfRequested() {
|
|
10101
|
-
if (this.chatDriver.getAgentReleaseRequested() && this.agentSession.getActive()) {
|
|
10102
|
-
await this.releaseActiveAgent();
|
|
10103
|
-
}
|
|
10456
|
+
this.cancelled = false;
|
|
10457
|
+
this.handOffsThisTurn = 0;
|
|
10458
|
+
return this.carryOn(await this.chatDriver.continueFromHistory(transientPrimer, options));
|
|
10104
10459
|
}
|
|
10105
10460
|
/**
|
|
10106
10461
|
* {@inheritDoc AiDriver.getContextReserve}
|
|
@@ -10128,25 +10483,18 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
10128
10483
|
}
|
|
10129
10484
|
return winner ? { ...winner, agentName, basis: "reachable-max" } : void 0;
|
|
10130
10485
|
}
|
|
10486
|
+
/** Make `agent` active through the session, firing its hooks, and install it. */
|
|
10487
|
+
async installAgent(agent) {
|
|
10488
|
+
const { previousAgent } = await this.agentSession.activate(agent);
|
|
10489
|
+
this.applyAgent(agent, previousAgent?.name);
|
|
10490
|
+
}
|
|
10131
10491
|
/**
|
|
10132
|
-
*
|
|
10133
|
-
*
|
|
10492
|
+
* Install an agent the session has already made active on the inner driver: the transcript's
|
|
10493
|
+
* divider when another agent spoke before it, and its own view of the history. What is left here
|
|
10494
|
+
* is this driver's own job, the transcript; the session owns the lifecycle.
|
|
10134
10495
|
*/
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
const isTerminal = isFallback(agent) || (pin === void 0 ? this.agentSession.getPinned() : pin) !== null || this.agentSession.getFlowOwner() !== null || !!(agent.onActivate || agent.onDeactivate);
|
|
10138
|
-
let agentToApply = agent;
|
|
10139
|
-
if (!isTerminal) {
|
|
10140
|
-
const declaredTools = agent.toolDefinitions;
|
|
10141
|
-
agentToApply = {
|
|
10142
|
-
...agent,
|
|
10143
|
-
toolDefinitions: typeof declaredTools === "function" ? async (ctx) => [
|
|
10144
|
-
...await declaredTools(ctx),
|
|
10145
|
-
REQUEST_CONTINUATION_DEFINITION
|
|
10146
|
-
] : [...declaredTools ?? [], REQUEST_CONTINUATION_DEFINITION]
|
|
10147
|
-
};
|
|
10148
|
-
}
|
|
10149
|
-
if (previousAgent && previousAgent.name !== agent.name) {
|
|
10496
|
+
applyAgent(agent, previousName) {
|
|
10497
|
+
if (previousName && previousName !== agent.name) {
|
|
10150
10498
|
const rawHistory = this.chatDriver.getHistory();
|
|
10151
10499
|
this.chatDriver.loadHistory([
|
|
10152
10500
|
...rawHistory,
|
|
@@ -10154,7 +10502,7 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
10154
10502
|
]);
|
|
10155
10503
|
}
|
|
10156
10504
|
this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
|
|
10157
|
-
this.chatDriver.installAgent(
|
|
10505
|
+
this.chatDriver.installAgent(agent);
|
|
10158
10506
|
this.dispatchEvent(new CustomEvent("orchestrating-stop"));
|
|
10159
10507
|
}
|
|
10160
10508
|
/**
|
|
@@ -10171,10 +10519,250 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
10171
10519
|
async releaseActiveAgent() {
|
|
10172
10520
|
await this.agentSession.release();
|
|
10173
10521
|
}
|
|
10522
|
+
/** The agent named `name` as this driver installs it: the fallback with its prompt built. */
|
|
10523
|
+
agentNamed(name) {
|
|
10524
|
+
if (this.fallback?.name === name) return this.fallback;
|
|
10525
|
+
return this.agents.find((a) => a.name === name);
|
|
10526
|
+
}
|
|
10527
|
+
/**
|
|
10528
|
+
* End the hand-offs when a turn runs an agent other than the top target (GENC-1737): the lock
|
|
10529
|
+
* that kept turns on the target was taken away, by a host clearing `flowOwnerAgentName` say,
|
|
10530
|
+
* and its caller would otherwise wait for good.
|
|
10531
|
+
*/
|
|
10532
|
+
async endOrphanedHandOffs(agent) {
|
|
10533
|
+
const top = this.handOffStack[this.handOffStack.length - 1];
|
|
10534
|
+
if (!top || top.outcome || top.target === agent.name) return;
|
|
10535
|
+
await this.unwindHandOffs("cleared");
|
|
10536
|
+
await this.agentSession.endSuspended();
|
|
10537
|
+
}
|
|
10538
|
+
/**
|
|
10539
|
+
* Check a handler's `handOff` when it is made (GENC-1737), so a refusal reaches its model. Checked
|
|
10540
|
+
* against the agents and the stack as they are now; the hand-off itself starts after the call.
|
|
10541
|
+
*/
|
|
10542
|
+
checkHandOff(request, caller) {
|
|
10543
|
+
const target = this.agentNamed(request.to);
|
|
10544
|
+
if (!target) return { ok: false, reason: "unknown-agent" };
|
|
10545
|
+
const allowed = this.agents.find((a) => a.name === caller)?.handOffTargets;
|
|
10546
|
+
if (allowed && !allowed.includes(target.name)) return { ok: false, reason: "not-allowed" };
|
|
10547
|
+
if (target.name === caller || this.handOffStack.some((f) => f.caller === target.name || f.target === target.name)) {
|
|
10548
|
+
return { ok: false, reason: "re-entry" };
|
|
10549
|
+
}
|
|
10550
|
+
if (request.returnTo !== false && this.handOffStack.length >= this.maxHandOffDepth) {
|
|
10551
|
+
return { ok: false, reason: "depth" };
|
|
10552
|
+
}
|
|
10553
|
+
if (this.handOffsThisTurn >= this.maxHandoffs) return { ok: false, reason: "limit" };
|
|
10554
|
+
return { ok: true };
|
|
10555
|
+
}
|
|
10556
|
+
/**
|
|
10557
|
+
* Start the hand-off the agent call just made, and return the target's tail note (GENC-1737).
|
|
10558
|
+
*
|
|
10559
|
+
* - `returnTo` (the default): push a frame. The caller waits without being deactivated, its
|
|
10560
|
+
* `agentEnd` payloads suspended, and the target takes the flow lock.
|
|
10561
|
+
* - One-way from a hand-off's target: that frame's caller now waits for the new target instead.
|
|
10562
|
+
* - One-way with nobody waiting: a plain switch, the caller deactivated as on any swap.
|
|
10563
|
+
*
|
|
10564
|
+
* Either way the brief is recorded as a message the caller sent on the user's behalf.
|
|
10565
|
+
*/
|
|
10566
|
+
async startHandOff(request) {
|
|
10567
|
+
const caller = this.agentSession.getActive();
|
|
10568
|
+
const target = this.agentNamed(request.to);
|
|
10569
|
+
if (!caller || !target) return void 0;
|
|
10570
|
+
this.handOffsThisTurn += 1;
|
|
10571
|
+
const frame = {
|
|
10572
|
+
id: crypto.randomUUID(),
|
|
10573
|
+
caller: caller.name,
|
|
10574
|
+
target: target.name,
|
|
10575
|
+
returnTo: request.returnTo !== false,
|
|
10576
|
+
message: request.message,
|
|
10577
|
+
...request.data !== void 0 && { data: request.data },
|
|
10578
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10579
|
+
};
|
|
10580
|
+
const top = this.handOffStack[this.handOffStack.length - 1];
|
|
10581
|
+
if (frame.returnTo) {
|
|
10582
|
+
this.handOffStack.push(frame);
|
|
10583
|
+
await callHandOffHook(
|
|
10584
|
+
caller.name,
|
|
10585
|
+
"onHandOffStart",
|
|
10586
|
+
() => caller.onHandOffStart?.(frameCopy(frame))
|
|
10587
|
+
);
|
|
10588
|
+
this.chatDriver.suspendActivation(caller.name);
|
|
10589
|
+
await this.agentSession.handOff(target, { suspendCaller: true });
|
|
10590
|
+
} else if (top?.target === caller.name) {
|
|
10591
|
+
top.target = target.name;
|
|
10592
|
+
await this.agentSession.handOff(target, { suspendCaller: false });
|
|
10593
|
+
} else {
|
|
10594
|
+
await this.agentSession.activate(target);
|
|
10595
|
+
}
|
|
10596
|
+
this.applyAgent(this.agentSession.getActive() ?? target, caller.name);
|
|
10597
|
+
this.agentSession.setHandOffData(target.name, frameCopy(frame).data);
|
|
10598
|
+
this.recordDelegated(caller.name, frame.message, `Handed over by ${caller.name}`);
|
|
10599
|
+
this.dispatchEvent(new CustomEvent("handoff-started", { detail: frameCopy(frame) }));
|
|
10600
|
+
return handOffNote(caller.name);
|
|
10601
|
+
}
|
|
10602
|
+
/**
|
|
10603
|
+
* Hand back to `frame`'s caller with `outcome`, and return its tail note (GENC-1737). The outcome
|
|
10604
|
+
* is kept on the frame until the caller is active again and its `onHandOffReturn` has run, so a
|
|
10605
|
+
* reload in between still hands back; the frame then goes, before the caller's model call.
|
|
10606
|
+
*/
|
|
10607
|
+
async handBack(frame, outcome) {
|
|
10608
|
+
frame.outcome = outcome;
|
|
10609
|
+
this.handOffsThisTurn += 1;
|
|
10610
|
+
const remaining = this.handOffStack.filter((f) => f !== frame);
|
|
10611
|
+
const caller = this.agentNamed(frame.caller);
|
|
10612
|
+
if (!caller) {
|
|
10613
|
+
this.handOffStack = remaining;
|
|
10614
|
+
this.unwound(frame, "agent-removed");
|
|
10615
|
+
return void 0;
|
|
10616
|
+
}
|
|
10617
|
+
await this.agentSession.resume(caller, remaining.length > 0 || isStateful(caller));
|
|
10618
|
+
const live2 = this.agentSession.getActive() ?? caller;
|
|
10619
|
+
this.applyAgent(live2, frame.target);
|
|
10620
|
+
if (outcome.kind === "completed" && outcome.message) {
|
|
10621
|
+
this.recordDelegated(frame.target, outcome.message, `Handed back by ${frame.target}`);
|
|
10622
|
+
}
|
|
10623
|
+
await callHandOffHook(
|
|
10624
|
+
live2.name,
|
|
10625
|
+
"onHandOffReturn",
|
|
10626
|
+
() => live2.onHandOffReturn?.(structuredClone(outcome), frameCopy(frame))
|
|
10627
|
+
);
|
|
10628
|
+
this.handOffStack = remaining;
|
|
10629
|
+
this.dispatchEvent(new CustomEvent("handoff-returned", { detail: frameCopy(frame) }));
|
|
10630
|
+
return handBackNote(frame, outcome);
|
|
10631
|
+
}
|
|
10632
|
+
/** The top frame, when `agent` is its target. */
|
|
10633
|
+
topFrameFor(agent) {
|
|
10634
|
+
const top = this.handOffStack[this.handOffStack.length - 1];
|
|
10635
|
+
return top?.target === agent ? top : void 0;
|
|
10636
|
+
}
|
|
10637
|
+
/** The target's last reply since its brief, for a target that released without a summary. */
|
|
10638
|
+
lastReply(frame) {
|
|
10639
|
+
const history = this.chatDriver.getHistory();
|
|
10640
|
+
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
10641
|
+
const m = history[i];
|
|
10642
|
+
if (m.role === "user" && m.delegate === frame.caller) return void 0;
|
|
10643
|
+
if (m.role === "assistant" && m.agentName === frame.target && reachesProvider(m) && m.content?.trim()) {
|
|
10644
|
+
return m.content;
|
|
10645
|
+
}
|
|
10646
|
+
}
|
|
10647
|
+
return void 0;
|
|
10648
|
+
}
|
|
10649
|
+
/** The brief, in the transcript as a message the caller sent on the user's behalf. */
|
|
10650
|
+
recordDelegated(from, message, framing) {
|
|
10651
|
+
const history = this.chatDriver.getHistory();
|
|
10652
|
+
this.chatDriver.loadHistory([
|
|
10653
|
+
...history,
|
|
10654
|
+
{
|
|
10655
|
+
role: "user",
|
|
10656
|
+
content: message,
|
|
10657
|
+
delegate: from,
|
|
10658
|
+
delegateFraming: framing,
|
|
10659
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10660
|
+
}
|
|
10661
|
+
]);
|
|
10662
|
+
this.dispatchEvent(
|
|
10663
|
+
new CustomEvent("history-updated", { detail: this.chatDriver.getHistory() })
|
|
10664
|
+
);
|
|
10665
|
+
}
|
|
10666
|
+
/** End `frame` with `kind` and no hand-back, for the host. */
|
|
10667
|
+
unwound(frame, kind) {
|
|
10668
|
+
frame.outcome = { kind };
|
|
10669
|
+
this.dispatchEvent(new CustomEvent("handoff-unwound", { detail: frameCopy(frame) }));
|
|
10670
|
+
}
|
|
10671
|
+
/** {@inheritDoc AiDriver.getHandOffStack} */
|
|
10672
|
+
getHandOffStack() {
|
|
10673
|
+
return this.handOffStack.map(frameCopy);
|
|
10674
|
+
}
|
|
10675
|
+
/** {@inheritDoc AiDriver.restoreHandOffStack} */
|
|
10676
|
+
restoreHandOffStack(saved, savedStates, interrupted) {
|
|
10677
|
+
this.handOffStack = [];
|
|
10678
|
+
this.pendingHandBack = void 0;
|
|
10679
|
+
const kept = [];
|
|
10680
|
+
const unwoundTargets = /* @__PURE__ */ new Set();
|
|
10681
|
+
for (const frame of parseHandOffFrames(saved)) {
|
|
10682
|
+
const caller = this.agents.find((a) => a.name === frame.caller);
|
|
10683
|
+
const outcome = frame.outcome;
|
|
10684
|
+
if (!caller) {
|
|
10685
|
+
this.unwound(frame, "agent-removed");
|
|
10686
|
+
} else if (interrupted || caller.serialize && savedStates?.[caller.name] === void 0) {
|
|
10687
|
+
this.unwound(frame, "state-lost");
|
|
10688
|
+
} else {
|
|
10689
|
+
kept.push(frame);
|
|
10690
|
+
continue;
|
|
10691
|
+
}
|
|
10692
|
+
this.agentSession.setHandOffData(frame.caller, void 0);
|
|
10693
|
+
unwoundTargets.add(frame.target);
|
|
10694
|
+
const below = kept[kept.length - 1];
|
|
10695
|
+
if (below?.target === frame.caller) {
|
|
10696
|
+
below.target = frame.target;
|
|
10697
|
+
if (outcome) below.outcome = outcome;
|
|
10698
|
+
}
|
|
10699
|
+
}
|
|
10700
|
+
const top = kept[kept.length - 1];
|
|
10701
|
+
if (top && !top.outcome && !this.agents.some((a) => a.name === top.target)) {
|
|
10702
|
+
top.outcome = { kind: "agent-removed" };
|
|
10703
|
+
}
|
|
10704
|
+
this.handOffStack = kept;
|
|
10705
|
+
this.chatDriver.suspendRestoredActivations(kept.map((f) => f.caller));
|
|
10706
|
+
const droppedLocks = [...unwoundTargets].filter((name) => {
|
|
10707
|
+
const target = this.agents.find((a) => a.name === name);
|
|
10708
|
+
return !!target && !isStateful(target) && !kept.some((f) => f.target === name);
|
|
10709
|
+
});
|
|
10710
|
+
const dropped = droppedLocks.length ? { droppedLocks } : {};
|
|
10711
|
+
if (!top?.outcome) return dropped;
|
|
10712
|
+
this.pendingHandBack = { frame: top, resumed: false };
|
|
10713
|
+
return { ...dropped, resume: { agent: top.caller, note: handBackNote(top, top.outcome) } };
|
|
10714
|
+
}
|
|
10715
|
+
/** {@inheritDoc AiDriver.unwindHandOffs} */
|
|
10716
|
+
async unwindHandOffs(kind) {
|
|
10717
|
+
const frames = [...this.handOffStack].reverse();
|
|
10718
|
+
this.handOffStack = [];
|
|
10719
|
+
this.pendingHandBack = void 0;
|
|
10720
|
+
this.chatDriver.endSuspendedActivations();
|
|
10721
|
+
for (const frame of frames) {
|
|
10722
|
+
this.unwound(frame, kind);
|
|
10723
|
+
const live2 = this.agentSession.getSuspended().find((a) => a.name === frame.caller);
|
|
10724
|
+
if (kind === "cleared" && live2) {
|
|
10725
|
+
await callHandOffHook(
|
|
10726
|
+
live2.name,
|
|
10727
|
+
"onHandOffReturn",
|
|
10728
|
+
() => live2.onHandOffReturn?.(structuredClone(frame.outcome), frameCopy(frame))
|
|
10729
|
+
);
|
|
10730
|
+
}
|
|
10731
|
+
}
|
|
10732
|
+
}
|
|
10733
|
+
/** {@inheritDoc AiDriver.getHandOffData} */
|
|
10734
|
+
getHandOffData() {
|
|
10735
|
+
const live2 = [this.agentSession.getFlowOwner(), ...this.handOffStack.map((f) => f.caller)];
|
|
10736
|
+
const saved = {};
|
|
10737
|
+
for (const name of live2) {
|
|
10738
|
+
const data = name ? this.agentSession.getHandOffData(name) : void 0;
|
|
10739
|
+
if (name && data !== void 0) saved[name] = data;
|
|
10740
|
+
}
|
|
10741
|
+
return saved;
|
|
10742
|
+
}
|
|
10743
|
+
/** {@inheritDoc AiDriver.restoreHandOffData} */
|
|
10744
|
+
restoreHandOffData(saved, interrupted) {
|
|
10745
|
+
if (interrupted || !saved || typeof saved !== "object" || Array.isArray(saved)) return;
|
|
10746
|
+
for (const [name, data] of Object.entries(saved)) this.agentSession.setHandOffData(name, data);
|
|
10747
|
+
}
|
|
10748
|
+
/** {@inheritDoc AiDriver.serializeWaitingCallers} */
|
|
10749
|
+
serializeWaitingCallers() {
|
|
10750
|
+
const states = {};
|
|
10751
|
+
for (const caller of this.agentSession.getSuspended()) {
|
|
10752
|
+
if (!caller.serialize) continue;
|
|
10753
|
+
try {
|
|
10754
|
+
const state = caller.serialize();
|
|
10755
|
+
if (state !== void 0) states[caller.name] = state;
|
|
10756
|
+
} catch (e) {
|
|
10757
|
+
logger2.error(`OrchestratingDriver: serialize("${caller.name}") threw:`, e);
|
|
10758
|
+
}
|
|
10759
|
+
}
|
|
10760
|
+
return states;
|
|
10761
|
+
}
|
|
10174
10762
|
/**
|
|
10175
10763
|
* Stop the current turn (user "stop" button). Cancels the inner chat driver's
|
|
10176
|
-
* in-flight request and
|
|
10177
|
-
*
|
|
10764
|
+
* in-flight request, and no further agent runs this turn, not even a hand-off's
|
|
10765
|
+
* target or a hand-back. The driver stays usable for the next message.
|
|
10178
10766
|
*/
|
|
10179
10767
|
cancel() {
|
|
10180
10768
|
this.cancelled = true;
|
|
@@ -10211,12 +10799,8 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
10211
10799
|
decision,
|
|
10212
10800
|
to: agent.name,
|
|
10213
10801
|
fallback: isFallback(agent),
|
|
10214
|
-
// The
|
|
10215
|
-
//
|
|
10216
|
-
// `candidates[index]` resolve to the wrong agent whenever `excluded` was non-empty — and the
|
|
10217
|
-
// README sentence invited exactly that join. `this.specialists` stays recoverable as
|
|
10218
|
-
// `candidates ∪ excluded`. Absent where nothing was put to a vote, which is more truthful
|
|
10219
|
-
// than a full list. Reviewer catch on #2496.
|
|
10802
|
+
// The ballot, so `candidates[index]` is the agent chosen. Absent where nothing was put to a
|
|
10803
|
+
// vote, which is more truthful than a full list.
|
|
10220
10804
|
...ballot ? { candidates: ballot } : {},
|
|
10221
10805
|
...rest,
|
|
10222
10806
|
...usage ? { usage } : {},
|
|
@@ -10239,7 +10823,7 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
10239
10823
|
}
|
|
10240
10824
|
return lines.join("\n");
|
|
10241
10825
|
}
|
|
10242
|
-
async classify(input, history, contextAgent
|
|
10826
|
+
async classify(input, history, contextAgent) {
|
|
10243
10827
|
const classifyStartedAt = Date.now();
|
|
10244
10828
|
if (this.specialists.length === 1 && !this.fallback) {
|
|
10245
10829
|
const only = this.specialists[0];
|
|
@@ -10262,27 +10846,16 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
|
|
|
10262
10846
|
this.recordRouting("no-specialists", leaf, { durationMs: Date.now() - classifyStartedAt });
|
|
10263
10847
|
return { agent: leaf, decision: "no-specialists" };
|
|
10264
10848
|
}
|
|
10265
|
-
const ballot =
|
|
10266
|
-
if (ballot.length === 0) {
|
|
10267
|
-
const leaf = this.fallback ?? this.specialists[0];
|
|
10268
|
-
this.recordRouting("ballot-exhausted", leaf, {
|
|
10269
|
-
excluded: excludeAgents,
|
|
10270
|
-
durationMs: Date.now() - classifyStartedAt
|
|
10271
|
-
});
|
|
10272
|
-
return { agent: leaf, decision: "ballot-exhausted" };
|
|
10273
|
-
}
|
|
10849
|
+
const ballot = this.specialists;
|
|
10274
10850
|
const agentList = ballot.map((a, i) => `${i}: ${a.name} \u2014 ${a.description}`).join("\n");
|
|
10275
10851
|
const recentMessages = this.classifierTranscript(history);
|
|
10276
|
-
const continuationHint = contextAgent && isSpecialist(contextAgent)
|
|
10852
|
+
const continuationHint = contextAgent && isSpecialist(contextAgent) ? `
|
|
10277
10853
|
|
|
10278
10854
|
The previous turn was handled by "${contextAgent.name}". If the new message is a refinement, correction, or continuation of that work, prefer "${contextAgent.name}" \u2014 unless the topic has clearly shifted to another specialist's domain.` : "";
|
|
10279
|
-
const
|
|
10280
|
-
|
|
10281
|
-
Already attempted this turn and handed the work on, so not listed above: ${excludeAgents.join(", ")}. Do not expect another agent to repeat their part.` : "";
|
|
10282
|
-
const classifierPrompt = `You are a routing classifier. Call select_agent with the index of the best matching agent for the user message. Use -1 only if the request is entirely unrelated to every agent listed \u2014 prefer the closest match when in doubt. If the request spans multiple agents, pick the one that should act first; I will re-classify and route again once that agent is done.
|
|
10855
|
+
const classifierPrompt = `You are a routing classifier. Call select_agent with the index of the best matching agent for the user message. Use -1 only if the request is entirely unrelated to every agent listed \u2014 prefer the closest match when in doubt. If the request spans multiple agents, pick the one that should act first.
|
|
10283
10856
|
|
|
10284
10857
|
Agents:
|
|
10285
|
-
${agentList}${continuationHint}${
|
|
10858
|
+
${agentList}${continuationHint}${recentMessages ? `
|
|
10286
10859
|
|
|
10287
10860
|
Recent conversation:
|
|
10288
10861
|
${recentMessages}` : ""}`;
|
|
@@ -10330,8 +10903,7 @@ ${recentMessages}` : ""}`;
|
|
|
10330
10903
|
attempts,
|
|
10331
10904
|
durationMs: Date.now() - classifyStartedAt,
|
|
10332
10905
|
response,
|
|
10333
|
-
ballot: ballot.map((a) => a.name)
|
|
10334
|
-
...excludeAgents.length ? { excluded: excludeAgents } : {}
|
|
10906
|
+
ballot: ballot.map((a) => a.name)
|
|
10335
10907
|
});
|
|
10336
10908
|
return { agent: chosen, decision: "classified" };
|
|
10337
10909
|
}
|
|
@@ -10352,8 +10924,7 @@ ${recentMessages}` : ""}`;
|
|
|
10352
10924
|
durationMs: Date.now() - classifyStartedAt,
|
|
10353
10925
|
ballot: ballot.map((a) => a.name),
|
|
10354
10926
|
...lastError ? { error: lastError } : {},
|
|
10355
|
-
...lastResponse ? { response: lastResponse } : {}
|
|
10356
|
-
...excludeAgents.length ? { excluded: excludeAgents } : {}
|
|
10927
|
+
...lastResponse ? { response: lastResponse } : {}
|
|
10357
10928
|
};
|
|
10358
10929
|
if (this.fallback) {
|
|
10359
10930
|
this.recordRouting(decision, this.fallback, routingDetail);
|
|
@@ -10391,7 +10962,7 @@ ${recentMessages}` : ""}`;
|
|
|
10391
10962
|
return match;
|
|
10392
10963
|
}
|
|
10393
10964
|
/**
|
|
10394
|
-
* Append prose the ORCHESTRATOR wrote — the
|
|
10965
|
+
* Append prose the ORCHESTRATOR wrote — the no-specialist apology. Flagged
|
|
10395
10966
|
* `excludeFromProvider` for the same reason the driver's own notices are: it is not something a
|
|
10396
10967
|
* model said, and feeding it back invites the model to echo it. See
|
|
10397
10968
|
* `ChatMessage.excludeFromProvider`.
|
|
@@ -10900,7 +11471,6 @@ function withFreshMetaSnapshot(stored, fresh) {
|
|
|
10900
11471
|
OrchestratingDriver,
|
|
10901
11472
|
PROVIDER_REFUSED_CODE,
|
|
10902
11473
|
ProviderRefusedError,
|
|
10903
|
-
REQUEST_CONTINUATION_TOOL,
|
|
10904
11474
|
SUPPORTED_ANTHROPIC_MODEL_IDS,
|
|
10905
11475
|
SUPPORTED_GEMINI_MODEL_IDS,
|
|
10906
11476
|
UNROUTABLE_INSTRUCTION,
|