@genesislcap/ai-assistant 15.84.3 → 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.
Files changed (50) hide show
  1. package/dist/ai-assistant.api.json +2175 -138
  2. package/dist/ai-assistant.d.ts +420 -43
  3. package/dist/chat-driver.cjs +780 -210
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +780 -209
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +3251 -2161
  8. package/dist/dts/components/agent-session/agent-session.d.ts +50 -2
  9. package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -1
  10. package/dist/dts/components/ai-driver/ai-driver.d.ts +59 -1
  11. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  12. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +13 -1
  13. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
  14. package/dist/dts/components/chat-driver/chat-driver.constants.d.ts +0 -8
  15. package/dist/dts/components/chat-driver/chat-driver.constants.d.ts.map +1 -1
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts +74 -15
  17. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  18. package/dist/dts/components/orchestrating-driver/hand-off.d.ts +20 -0
  19. package/dist/dts/components/orchestrating-driver/hand-off.d.ts.map +1 -0
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +121 -22
  21. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  22. package/dist/dts/config/config.d.ts +66 -0
  23. package/dist/dts/config/config.d.ts.map +1 -1
  24. package/dist/dts/main/main.d.ts +3 -0
  25. package/dist/dts/main/main.d.ts.map +1 -1
  26. package/dist/dts/main/main.types.d.ts +6 -0
  27. package/dist/dts/main/main.types.d.ts.map +1 -1
  28. package/dist/dts/react.d.ts +4 -1
  29. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  30. package/dist/dts/state/persistence/session-snapshot.d.ts +33 -1
  31. package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
  32. package/dist/dts/utils/condense-history.d.ts +10 -3
  33. package/dist/dts/utils/condense-history.d.ts.map +1 -1
  34. package/dist/esm/components/agent-session/agent-session.js +128 -7
  35. package/dist/esm/components/ai-driver/ai-driver.js +3 -0
  36. package/dist/esm/components/ai-driver/base-driver-decorator.js +24 -0
  37. package/dist/esm/components/chat-driver/chat-driver.constants.js +0 -8
  38. package/dist/esm/components/chat-driver/chat-driver.js +247 -74
  39. package/dist/esm/components/orchestrating-driver/hand-off.js +59 -0
  40. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +480 -299
  41. package/dist/esm/main/main.js +32 -6
  42. package/dist/esm/main/main.types.js +6 -0
  43. package/dist/esm/state/debug-event-log.js +1 -1
  44. package/dist/esm/state/persistence/session-persister.js +55 -8
  45. package/dist/esm/state/persistence/session-snapshot.js +12 -6
  46. package/dist/esm/utils/condense-history.js +13 -4
  47. package/dist/react.cjs +16 -1
  48. package/dist/react.mjs +16 -1
  49. package/docs/sub_agent.md +1 -1
  50. package/package.json +17 -17
@@ -4410,7 +4410,7 @@ function toolCallIdsIn(history) {
4410
4410
  }
4411
4411
  return ids;
4412
4412
  }
4413
- function snapshotCondensePolicies(policies, clocks, history) {
4413
+ function snapshotCondensePolicies(policies, clocks, history, suspended = /* @__PURE__ */ new Map()) {
4414
4414
  if (policies.size === 0) return [];
4415
4415
  const present = toolCallIdsIn(history);
4416
4416
  const out = [];
@@ -4425,8 +4425,11 @@ function snapshotCondensePolicies(policies, clocks, history) {
4425
4425
  // the clamp only stops a malformed entry from being written out as "the future".
4426
4426
  modelCalls: Math.max(0, clocks.modelCall - entry.iteration),
4427
4427
  turns: Math.max(0, clocks.turn - entry.turn),
4428
- activations: Math.max(0, clocks.activation - entry.activation),
4429
- phases: Math.max(0, clocks.phaseEpoch - entry.phaseEpoch)
4428
+ activations: suspended.has(entry.activation) ? 0 : Math.max(0, clocks.activation - entry.activation),
4429
+ phases: Math.max(
4430
+ 0,
4431
+ (suspended.get(entry.activation) ?? clocks.phaseEpoch) - entry.phaseEpoch
4432
+ )
4430
4433
  }
4431
4434
  });
4432
4435
  }
@@ -4546,7 +4549,7 @@ function applyCondensation(history, policies, ctx, onCondensed) {
4546
4549
  // backstop, when the whole activation has ended, so a phase boundary that
4547
4550
  // is never declared still collapses at `agentEnd` rather than never.
4548
4551
  case "phaseEnd":
4549
- return ctx.phaseEnded(entry.phaseEpoch) || ctx.activationEnded(entry.activation);
4552
+ return ctx.phaseEnded(entry.phaseEpoch, entry.activation) || ctx.activationEnded(entry.activation);
4550
4553
  default:
4551
4554
  return latestByKey.get(trig.by) !== toolCallId;
4552
4555
  }
@@ -4921,6 +4924,17 @@ var AgentSession = class {
4921
4924
  this.restoredStates = {};
4922
4925
  /** The tail of the activation queue — see {@link AgentSession.activate}. */
4923
4926
  this.activation = Promise.resolve();
4927
+ /**
4928
+ * Callers waiting in a hand-off (GENC-1737), oldest first: still live, so never deactivated by
4929
+ * the hand-off, and resumed without `onActivate`. Empty after a reload, when a caller has no live
4930
+ * state and is activated again from its saved state instead.
4931
+ */
4932
+ this.suspended = [];
4933
+ /**
4934
+ * The `data` of the hand-off that activated each agent (GENC-1737), for its tool handlers: kept
4935
+ * while it is active or waits as a caller, and dropped when its activation ends.
4936
+ */
4937
+ this.handOffData = /* @__PURE__ */ new Map();
4924
4938
  }
4925
4939
  /** The active agent, or `undefined` before the first activation and after a release. */
4926
4940
  getActive() {
@@ -4962,6 +4976,84 @@ var AgentSession = class {
4962
4976
  hasRestoredState(agentName) {
4963
4977
  return this.restoredStates[agentName] !== void 0;
4964
4978
  }
4979
+ /** The `data` of the hand-off that activated `agentName`, if one did and carried any. */
4980
+ getHandOffData(agentName) {
4981
+ return this.handOffData.get(agentName);
4982
+ }
4983
+ /** Record the `data` a hand-off activated `agentName` with, or restore it; `undefined` drops it. */
4984
+ setHandOffData(agentName, data) {
4985
+ if (data === void 0) this.handOffData.delete(agentName);
4986
+ else this.handOffData.set(agentName, data);
4987
+ }
4988
+ /**
4989
+ * Drop the flow lock and leave the active agent active (GENC-1737): a plain agent holding it with
4990
+ * no hand-off behind it is done once its turn ends.
4991
+ */
4992
+ dropFlowLock() {
4993
+ if (this.flowOwner === null) return;
4994
+ this.flowOwner = null;
4995
+ this.reportTransition({
4996
+ previousAgent: this.active,
4997
+ switched: false,
4998
+ flowOwnerChangedTo: null
4999
+ });
5000
+ }
5001
+ /** The callers waiting in a hand-off whose state is live, oldest first. */
5002
+ getSuspended() {
5003
+ return this.suspended;
5004
+ }
5005
+ /**
5006
+ * Make `target` active for a hand-off (GENC-1737), holding the flow lock whatever kind of agent
5007
+ * it is, since a waiting caller depends on it. With `suspendCaller` the active agent waits:
5008
+ * it is not deactivated and keeps its state. Without it the active agent is deactivated as on
5009
+ * any swap: a one-way hand-off from a target, whose waiting caller then waits for the new
5010
+ * target. Queued with {@link AgentSession.activate}.
5011
+ */
5012
+ handOff(target, options) {
5013
+ const run = this.activation.then(
5014
+ () => this.activateNow(target, { lock: true, suspendPrevious: options.suspendCaller })
5015
+ );
5016
+ this.activation = run.catch(() => void 0);
5017
+ return run;
5018
+ }
5019
+ /**
5020
+ * Make a waiting caller active again when its hand-off ends (GENC-1737). One whose state is live
5021
+ * resumes without `onActivate`, since it was never deactivated; after a reload it is activated
5022
+ * from its saved state like any other agent. `lock` keeps the flow lock on it: it held it before,
5023
+ * or another caller is still waiting further down.
5024
+ */
5025
+ resume(caller, lock) {
5026
+ const run = this.activation.then(async () => {
5027
+ const index = this.suspended.findIndex((a) => a.name === caller.name);
5028
+ if (index === -1) return this.activateNow(caller, { lock });
5029
+ const [live2] = this.suspended.splice(index, 1);
5030
+ const transition = { previousAgent: this.active, switched: true };
5031
+ recordMetaEvent(this.deps.sessionKey(), "agent.handoff", {
5032
+ from: this.active?.name ?? null,
5033
+ to: live2.name,
5034
+ restored: false
5035
+ });
5036
+ this.active = live2;
5037
+ if (lock && this.flowOwner !== caller.name) {
5038
+ this.flowOwner = caller.name;
5039
+ transition.flowOwnerChangedTo = caller.name;
5040
+ }
5041
+ this.reportTransition(transition);
5042
+ return transition;
5043
+ });
5044
+ this.activation = run.catch(() => void 0);
5045
+ return run;
5046
+ }
5047
+ /**
5048
+ * Deactivate the callers left waiting by hand-offs that ended without a hand-back, newest first,
5049
+ * when no teardown follows to do it (GENC-1737).
5050
+ */
5051
+ async endSuspended() {
5052
+ for (const caller of this.suspended.splice(0).reverse()) {
5053
+ this.handOffData.delete(caller.name);
5054
+ await this.fireDeactivate(caller, "release");
5055
+ }
5056
+ }
4965
5057
  /**
4966
5058
  * Make `agent` active, firing the lifecycle hooks around the swap: outgoing `onDeactivate`
4967
5059
  * first, then incoming `onActivate`. Both are awaited so heavy activation work (a machine
@@ -4982,7 +5074,11 @@ var AgentSession = class {
4982
5074
  this.activation = run.catch(() => void 0);
4983
5075
  return run;
4984
5076
  }
4985
- async activateNow(agent) {
5077
+ /**
5078
+ * `lock` takes the flow lock whatever kind of agent it is, and `suspendPrevious` leaves the
5079
+ * outgoing agent waiting instead of deactivating it, as a hand-off does (GENC-1737).
5080
+ */
5081
+ async activateNow(agent, { lock = false, suspendPrevious = false } = {}) {
4986
5082
  const previousAgent = this.active;
4987
5083
  const switched = !previousAgent || previousAgent.name !== agent.name;
4988
5084
  const transition = { previousAgent, switched };
@@ -4994,7 +5090,14 @@ var AgentSession = class {
4994
5090
  restored: this.hasRestoredState(agent.name)
4995
5091
  });
4996
5092
  }
4997
- if (switched && previousAgent) await this.fireDeactivate(previousAgent, "switch");
5093
+ if (switched && previousAgent) {
5094
+ if (suspendPrevious) {
5095
+ this.suspended.push(previousAgent);
5096
+ } else {
5097
+ this.handOffData.delete(previousAgent.name);
5098
+ await this.fireDeactivate(previousAgent, "switch");
5099
+ }
5100
+ }
4998
5101
  if (switched && agent.onActivate) {
4999
5102
  const restored = this.restoredStates[agent.name];
5000
5103
  if (restored !== void 0) delete this.restoredStates[agent.name];
@@ -5011,9 +5114,12 @@ var AgentSession = class {
5011
5114
  }
5012
5115
  }
5013
5116
  const hasLifecycleHooks = !!(agent.onActivate || agent.onDeactivate);
5014
- if (switched && hasLifecycleHooks && this.flowOwner !== agent.name) {
5117
+ if (switched && (hasLifecycleHooks || lock) && this.flowOwner !== agent.name) {
5015
5118
  this.flowOwner = agent.name;
5016
5119
  transition.flowOwnerChangedTo = agent.name;
5120
+ } else if (switched && !suspendPrevious && this.flowOwner === previousAgent?.name) {
5121
+ this.flowOwner = null;
5122
+ transition.flowOwnerChangedTo = null;
5017
5123
  }
5018
5124
  this.active = agent;
5019
5125
  if (switched) this.reportTransition(transition);
@@ -5031,6 +5137,7 @@ var AgentSession = class {
5031
5137
  async release() {
5032
5138
  const agent = this.active;
5033
5139
  if (!agent) return void 0;
5140
+ this.handOffData.delete(agent.name);
5034
5141
  await this.fireDeactivate(agent, "release");
5035
5142
  this.active = void 0;
5036
5143
  const transition = { previousAgent: agent, switched: true };
@@ -5068,6 +5175,11 @@ var AgentSession = class {
5068
5175
  async dispose() {
5069
5176
  const agent = this.active;
5070
5177
  if (agent) await this.fireDeactivate(agent, "dispose");
5178
+ for (const caller of [...this.suspended].reverse()) {
5179
+ await this.fireDeactivate(caller, "dispose");
5180
+ }
5181
+ this.suspended = [];
5182
+ this.handOffData.clear();
5071
5183
  this.lifecycleAbortController.abort();
5072
5184
  this.active = void 0;
5073
5185
  }
@@ -5107,8 +5219,6 @@ var HARVESTED_SUBAGENT_EVENTS = /* @__PURE__ */ new Set([
5107
5219
  "tool.failed",
5108
5220
  "tool.unresolved"
5109
5221
  ]);
5110
- var REQUEST_CONTINUATION_TOOL = "request_continuation";
5111
- var HANDOFF_TOOL_RESULT_PLACEHOLDER = "Handoff to another specialist \u2014 routing continues on the next turn.";
5112
5222
 
5113
5223
  // src/utils/tool-fold.ts
5114
5224
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
@@ -6736,6 +6846,16 @@ var SERVICE_WIDE_REFUSALS = /* @__PURE__ */ new Set([
6736
6846
  "ACCESS_DENIED",
6737
6847
  "VENDOR_UNREACHABLE"
6738
6848
  ]);
6849
+ 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.`;
6850
+ 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.";
6851
+ var jsonCopy2 = (value) => {
6852
+ try {
6853
+ const text = JSON.stringify(value);
6854
+ return text === void 0 ? void 0 : JSON.parse(text);
6855
+ } catch {
6856
+ return void 0;
6857
+ }
6858
+ };
6739
6859
  var toolResultImages = (result) => {
6740
6860
  if (!result || typeof result !== "object") return void 0;
6741
6861
  const candidate = result.attachments;
@@ -6925,6 +7045,17 @@ var ChatDriver = class _ChatDriver extends EventTarget {
6925
7045
  * doesn't leak forward.
6926
7046
  */
6927
7047
  this.agentReleaseRequested = false;
7048
+ /**
7049
+ * The `agentEnd` activation of each caller waiting in a hand-off, by agent name, with the phase
7050
+ * epoch it was suspended at (GENC-1737). A waiting caller is suspended, not ended, so neither its
7051
+ * `agentEnd` payloads nor its `phaseEnd` ones (to another agent's `endPhase()`) collapse while the
7052
+ * target runs; when it is installed again its entries move to the new activation and, while
7053
+ * their phase had not ended, to the current epoch. A caller restored from a saved session gets
7054
+ * an activation placeholder below zero, which no live activation uses.
7055
+ */
7056
+ this.suspendedActivations = /* @__PURE__ */ new Map();
7057
+ /** The last placeholder handed out for a restored caller; see `suspendedActivations`. */
7058
+ this.restoredSuspensionSeq = 0;
6928
7059
  /**
6929
7060
  * Aborted by `dispose()` on driver teardown (e.g. an agent-config swap).
6930
7061
  * Threaded into every provider call as `ChatRequestOptions.signal`, so a
@@ -6973,8 +7104,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
6973
7104
  * The stateful-agent contract for this driver: active agent, user pin, flow lock, restored
6974
7105
  * state, and the `onActivate` / `onDeactivate` hooks (§7.1).
6975
7106
  *
6976
- * **Public because `OrchestratingDriver` reaches through to it.** The orchestrator decorates a
6977
- * config before installing it (the cross-agent continuation tool), so it cannot route through
7107
+ * **Public because `OrchestratingDriver` reaches through to it.** The orchestrator builds a
7108
+ * config before installing it (the fallback's prompt), so it cannot route through
6978
7109
  * {@link ChatDriver.activateAgent} and must drive the session itself — and one shared session is
6979
7110
  * the point: two would let `getActiveAgent()` disagree between the outer and inner driver.
6980
7111
  * `@internal` rather than private so that reach is legal without six delegating one-liners.
@@ -7381,19 +7512,15 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7381
7512
  this.aiServiceNoticesShown.add(code);
7382
7513
  return false;
7383
7514
  }
7384
- /**
7385
- * Whether a sub-agent hit a wall this turn that stops this driver too. A handoff goes on past an
7386
- * AI-service wall: the next agent may use another vendor, and if it uses this one its own call
7387
- * meets the wall and says so.
7388
- */
7389
- subAgentWallThisTurn(handingOff = false) {
7390
- return this.providerRefusedThisTurn || this.budgetExhaustedThisTurn || this.aiServiceErrorThisTurn && !handingOff;
7515
+ /** Whether a sub-agent hit a wall this turn that stops this driver too. */
7516
+ subAgentWallThisTurn() {
7517
+ return this.providerRefusedThisTurn || this.budgetExhaustedThisTurn || this.aiServiceErrorThisTurn;
7391
7518
  }
7392
7519
  /**
7393
7520
  * Ends the turn when a sub-agent hit a wall this turn that stops this driver too: the provider
7394
7521
  * refusing the account, the AI service (GENC-1611), or the budget. Checked before each model call,
7395
- * and again right after a tool batch, before a handoff or the iteration cap can report the turn as
7396
- * something else.
7522
+ * and again right after a tool batch, before the iteration cap can report the turn as something
7523
+ * else.
7397
7524
  */
7398
7525
  endTurnOnSubAgentWall() {
7399
7526
  if (!this.subAgentWallThisTurn()) return void 0;
@@ -7601,6 +7728,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7601
7728
  installAgent(config) {
7602
7729
  if (config.name !== this.activeAgentName) {
7603
7730
  this.currentActivation += 1;
7731
+ this.resumeSuspendedActivation(config.name);
7604
7732
  this.resumeRestoredCondensePolicies(config.name);
7605
7733
  }
7606
7734
  this.systemPrompt = config.systemPrompt;
@@ -7802,6 +7930,109 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7802
7930
  getAgentReleaseRequested() {
7803
7931
  return this.agentReleaseRequested;
7804
7932
  }
7933
+ /**
7934
+ * Offer `handOff` and `completeHandOff` to tool handlers, checking each `handOff` with `check`
7935
+ * (GENC-1737). Set by `OrchestratingDriver`, which knows the agents and the hand-off stack and acts
7936
+ * on the request after the call; a bare `ChatDriver` has no other agent to hand to.
7937
+ *
7938
+ * @internal
7939
+ */
7940
+ setHandOffCheck(check) {
7941
+ this.checkHandOff = check;
7942
+ }
7943
+ /** The `handOff` the most recent call accepted, if any (GENC-1737). */
7944
+ getHandOffRequest() {
7945
+ return this.handOffRequested;
7946
+ }
7947
+ /** What `completeHandOff` handed back in the most recent call, if it was called. */
7948
+ getHandOffCompletion() {
7949
+ return this.handOffCompletion;
7950
+ }
7951
+ /**
7952
+ * A handler's `handOff`: refused once this call has handed off or released, or when `data` is
7953
+ * not JSON, and otherwise checked by the orchestrator. `data` is kept as a JSON copy, since it is
7954
+ * saved with the session and the handler may change its own object afterwards.
7955
+ */
7956
+ requestHandOff(request) {
7957
+ if (this.handOffRequested) return { ok: false, reason: "already-requested" };
7958
+ if (this.agentReleaseRequested) return { ok: false, reason: "released" };
7959
+ let data;
7960
+ if (request?.data !== void 0) {
7961
+ data = jsonCopy2(request.data);
7962
+ if (data === void 0) return { ok: false, reason: "not-json" };
7963
+ }
7964
+ const accepted = {
7965
+ to: request.to,
7966
+ message: String(request.message ?? ""),
7967
+ ...data !== void 0 && { data },
7968
+ ...request.returnTo === false && { returnTo: false }
7969
+ };
7970
+ const result = this.checkHandOff(accepted, this.activeAgentName);
7971
+ if (result.ok) this.handOffRequested = accepted;
7972
+ return result;
7973
+ }
7974
+ /**
7975
+ * The note of the closing call a hand-off asks for (GENC-1737), or `undefined` when none: once a
7976
+ * handler's `handOff` is accepted, or `completeHandOff` is called, the rest of that batch runs,
7977
+ * then one model call with no tools, so the agent can say so but not carry on, which a model told
7978
+ * so in a tool result does anyway. Then the turn ends.
7979
+ */
7980
+ closingCallNote() {
7981
+ if (this.handOffRequested) return handOffClosingNote(this.handOffRequested.to);
7982
+ return this.handOffCompletion ? HAND_BACK_CLOSING_NOTE : void 0;
7983
+ }
7984
+ /**
7985
+ * Suspend the active agent's activation for a hand-off it made (GENC-1737): installing the target
7986
+ * advances the `agentEnd` clock as any swap does, but this caller's payloads stay live.
7987
+ */
7988
+ suspendActivation(agentName) {
7989
+ if (agentName === this.activeAgentName) {
7990
+ this.suspendedActivations.set(agentName, {
7991
+ activation: this.currentActivation,
7992
+ phaseEpoch: this.currentPhaseEpoch
7993
+ });
7994
+ }
7995
+ }
7996
+ /**
7997
+ * Suspend restored callers before their condense policies are restored, so entries still live
7998
+ * at save time stay live instead of waiting for "the next activation", which is the target's.
7999
+ */
8000
+ suspendRestoredActivations(agentNames) {
8001
+ for (const name of agentNames) {
8002
+ this.restoredSuspensionSeq -= 1;
8003
+ this.suspendedActivations.set(name, {
8004
+ activation: this.restoredSuspensionSeq,
8005
+ phaseEpoch: this.currentPhaseEpoch
8006
+ });
8007
+ }
8008
+ }
8009
+ /** End every suspension: the hand-offs were unwound, so their callers' activations are over. */
8010
+ endSuspendedActivations() {
8011
+ this.suspendedActivations.clear();
8012
+ }
8013
+ /**
8014
+ * A caller installed again takes its suspended entries into the activation it resumes in, and
8015
+ * the ones whose phase had not ended into the current phase epoch.
8016
+ */
8017
+ resumeSuspendedActivation(agentName) {
8018
+ const suspended = this.suspendedActivations.get(agentName);
8019
+ if (suspended === void 0) return;
8020
+ this.suspendedActivations.delete(agentName);
8021
+ for (const entry of this.condensePolicies.values()) {
8022
+ if (entry.activation !== suspended.activation) continue;
8023
+ entry.activation = this.currentActivation;
8024
+ if (entry.phaseEpoch >= suspended.phaseEpoch) entry.phaseEpoch = this.currentPhaseEpoch;
8025
+ }
8026
+ }
8027
+ /**
8028
+ * The suspended activations, each with the phase epoch it was suspended at, for the `agentEnd`
8029
+ * and `phaseEnd` checks and for persistence.
8030
+ */
8031
+ suspendedClocks() {
8032
+ return new Map(
8033
+ [...this.suspendedActivations.values()].map((s) => [s.activation, s.phaseEpoch])
8034
+ );
8035
+ }
7805
8036
  /**
7806
8037
  * Act on a `releaseAgent` call from a tool handler: fire `onDeactivate` and drop the flow lock,
7807
8038
  * so the next turn starts clean and the persister does not serialize a flow that is over.
@@ -7940,7 +8171,12 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7940
8171
  * @beta
7941
8172
  */
7942
8173
  getCondensePoliciesForPersistence(messages) {
7943
- return snapshotCondensePolicies(this.condensePolicies, this.condenseClocks(), messages);
8174
+ return snapshotCondensePolicies(
8175
+ this.condensePolicies,
8176
+ this.condenseClocks(),
8177
+ messages,
8178
+ this.suspendedClocks()
8179
+ );
7944
8180
  }
7945
8181
  /**
7946
8182
  * Re-register policies saved by {@link ChatDriver.getCondensePoliciesForPersistence} against
@@ -7989,9 +8225,13 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7989
8225
  const entry = parsePersistedCondensePolicy(raw);
7990
8226
  if (!entry || !present.has(entry.toolCallId)) continue;
7991
8227
  if (this.condensePolicies.has(entry.toolCallId)) continue;
7992
- this.condensePolicies.set(entry.toolCallId, rebaseRestoredCondensePolicy(entry, clocks));
8228
+ const registered = rebaseRestoredCondensePolicy(entry, clocks);
8229
+ this.condensePolicies.set(entry.toolCallId, registered);
7993
8230
  restored += 1;
7994
- if (entry.elapsed.activations === 0 && entry.agent !== this.activeAgentName) {
8231
+ const suspended = entry.elapsed.activations === 0 && entry.agent !== void 0 ? this.suspendedActivations.get(entry.agent) : void 0;
8232
+ if (suspended !== void 0) {
8233
+ registered.activation = suspended.activation;
8234
+ } else if (entry.elapsed.activations === 0 && entry.agent !== this.activeAgentName) {
7995
8235
  this.condenseAwaitingReactivation.add(entry.toolCallId);
7996
8236
  awaitingReactivation += 1;
7997
8237
  }
@@ -8348,6 +8588,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
8348
8588
  this.subAgentCompletion = void 0;
8349
8589
  this.subAgentFailure = void 0;
8350
8590
  this.agentReleaseRequested = false;
8591
+ this.handOffRequested = void 0;
8592
+ this.handOffCompletion = void 0;
8351
8593
  this.budgetExhaustedThisTurn = false;
8352
8594
  this.budgetWallDetail = void 0;
8353
8595
  this.budgetWallViaSubAgent = false;
@@ -8481,6 +8723,12 @@ var ChatDriver = class _ChatDriver extends EventTarget {
8481
8723
  this.currentActivation += 1;
8482
8724
  },
8483
8725
  releaseAgent: () => {
8726
+ if (this.handOffRequested) {
8727
+ logger2.warn(
8728
+ `ChatDriver(${this.activeAgentName ?? "unknown"}): releaseAgent called after handOff \u2014 ignoring; the hand-off ends this agent's turn`
8729
+ );
8730
+ return;
8731
+ }
8484
8732
  if (this.agentReleaseRequested) {
8485
8733
  logger2.warn(
8486
8734
  `ChatDriver(${this.activeAgentName ?? "unknown"}): releaseAgent called more than once \u2014 ignoring`
@@ -8490,6 +8738,34 @@ var ChatDriver = class _ChatDriver extends EventTarget {
8490
8738
  this.agentReleaseRequested = true;
8491
8739
  this.currentActivation += 1;
8492
8740
  },
8741
+ ...this.checkHandOff && {
8742
+ handOff: (request) => this.requestHandOff(request),
8743
+ // A copy, so a handler cannot change what is saved.
8744
+ handOffData: () => jsonCopy2(this.agentSession.getHandOffData(this.activeAgentName ?? "")),
8745
+ completeHandOff: (completion) => {
8746
+ if (this.agentReleaseRequested || this.handOffRequested) {
8747
+ logger2.warn(
8748
+ `ChatDriver(${this.activeAgentName ?? "unknown"}): completeHandOff called after a release or a hand-off \u2014 ignoring`
8749
+ );
8750
+ return;
8751
+ }
8752
+ let data;
8753
+ if (completion?.data !== void 0) {
8754
+ data = jsonCopy2(completion.data);
8755
+ if (data === void 0) {
8756
+ logger2.warn(
8757
+ `ChatDriver(${this.activeAgentName ?? "unknown"}): completeHandOff data cannot be saved as JSON \u2014 handing back without it`
8758
+ );
8759
+ }
8760
+ }
8761
+ this.handOffCompletion = {
8762
+ message: String(completion?.message ?? ""),
8763
+ ...data !== void 0 && { data }
8764
+ };
8765
+ this.agentReleaseRequested = true;
8766
+ this.currentActivation += 1;
8767
+ }
8768
+ },
8493
8769
  endPhase: () => {
8494
8770
  this.currentPhaseEpoch += 1;
8495
8771
  },
@@ -8692,6 +8968,9 @@ var ChatDriver = class _ChatDriver extends EventTarget {
8692
8968
  this.beginTurn();
8693
8969
  this.subAgentCompletion = void 0;
8694
8970
  this.subAgentFailure = void 0;
8971
+ this.agentReleaseRequested = false;
8972
+ this.handOffRequested = void 0;
8973
+ this.handOffCompletion = void 0;
8695
8974
  this.budgetExhaustedThisTurn = false;
8696
8975
  this.budgetWallDetail = void 0;
8697
8976
  this.budgetWallViaSubAgent = false;
@@ -8759,7 +9038,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
8759
9038
  let firstLlmCall = !!currentInput;
8760
9039
  let pinnedThinkingPolicy;
8761
9040
  let thinkingPolicyPinned = false;
8762
- while (iterations < this.maxToolIterations) {
9041
+ let closingCallMade = false;
9042
+ while (iterations < this.maxToolIterations || this.closingCallNote() && !closingCallMade) {
8763
9043
  iterations += 1;
8764
9044
  this.modelCallSeq += 1;
8765
9045
  if (this.turnController.signal.aborted) {
@@ -8767,6 +9047,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
8767
9047
  }
8768
9048
  const wall = this.endTurnOnSubAgentWall();
8769
9049
  if (wall) return wall;
9050
+ if (closingCallMade) return turnDone();
9051
+ const closing = this.closingCallNote();
8770
9052
  const promptCtx = {
8771
9053
  agentName: this.activeAgentName ?? "",
8772
9054
  history: this.history,
@@ -8808,13 +9090,17 @@ ${EMPTY_RESPONSE_NUDGE}` : "";
8808
9090
  const firstCallNote = tailNote && iterations === 1 ? `
8809
9091
 
8810
9092
  ${tailNote}` : "";
8811
- const frameworkSystemSuffix = `${foldSuffix}${firstCallNote}${retryNudge}`;
9093
+ const closingNote = closing ? `
9094
+
9095
+ ${closing}` : "";
9096
+ const frameworkSystemSuffix = `${foldSuffix}${firstCallNote}${closingNote}${retryNudge}`;
8812
9097
  const primer = normalizeForProvider([
8813
9098
  ...this.primerHistory ?? [],
8814
9099
  ...transientPrimer ?? []
8815
9100
  ]);
8816
9101
  const baseHistory = firstLlmCall ? this.history.slice(0, -1) : this.history;
8817
9102
  firstLlmCall = false;
9103
+ const suspended = this.suspendedClocks();
8818
9104
  const condensedHistory = applyCondensation(
8819
9105
  [...baseHistory],
8820
9106
  this.condensePolicies,
@@ -8823,10 +9109,11 @@ ${tailNote}` : "";
8823
9109
  turn: this.turnSeq,
8824
9110
  // A call's activation has ended once a later activation is current — a
8825
9111
  // swap or release/complete both advance the counter.
8826
- activationEnded: (activation) => activation < this.currentActivation,
9112
+ // A caller waiting in a hand-off is suspended, not ended (GENC-1737).
9113
+ activationEnded: (activation) => activation < this.currentActivation && !suspended.has(activation),
8827
9114
  // A call's phase has ended once the app declared a later phase epoch
8828
- // current via `endPhase()`.
8829
- phaseEnded: (phaseEpoch) => phaseEpoch < this.currentPhaseEpoch,
9115
+ // current via `endPhase()`; a waiting caller's, only by one before it waited.
9116
+ phaseEnded: (phaseEpoch, activation) => phaseEpoch < (suspended.get(activation) ?? this.currentPhaseEpoch),
8830
9117
  batchCalls: this.condenseBatchCalls
8831
9118
  },
8832
9119
  (detail) => recordMetaEvent(this.sessionKey, "context.condensed", detail)
@@ -8866,7 +9153,7 @@ ${tailNote}` : "";
8866
9153
  const tailContext = tailBody ? `<system-reminder>
8867
9154
  ${tailBody}
8868
9155
  </system-reminder>` : void 0;
8869
- const effectiveToolChoice = resolvedToolChoice ?? (this.isSubAgent ? "required" : void 0);
9156
+ const effectiveToolChoice = closing ? "none" : resolvedToolChoice ?? (this.isSubAgent ? "required" : void 0);
8870
9157
  const turnSnapshot = this.turnRecorder.record({
8871
9158
  agentName: this.activeAgentName,
8872
9159
  agentLabel: this.activeAgentLabel,
@@ -8938,6 +9225,7 @@ ${tailBody}
8938
9225
  });
8939
9226
  return this.endTurnWithFailure(TURN_FAILURE_EXITS.contextGuard);
8940
9227
  }
9228
+ if (closing) closingCallMade = true;
8941
9229
  let response;
8942
9230
  try {
8943
9231
  response = await activeProvider.chat(historyForCall, userInputForCall, options);
@@ -9071,6 +9359,10 @@ ${tailBody}
9071
9359
  }
9072
9360
  turnSnapshot.usage = messageUsage(response);
9073
9361
  if (response.model !== void 0) turnSnapshot.model = response.model;
9362
+ if (closing) {
9363
+ if (response.toolCalls?.length) response = { ...response, toolCalls: void 0 };
9364
+ if (!response.content?.trim()) return turnDone();
9365
+ }
9074
9366
  const isThinkingStep = response.content && response.toolCalls?.length;
9075
9367
  const isEmptyResponse = !response.content?.trim() && !response.toolCalls?.length;
9076
9368
  const isRefusal = response.responseMeta?.finishReason === "refusal";
@@ -9148,17 +9440,11 @@ ${tailBody}
9148
9440
  emptyResponseAttempts = 0;
9149
9441
  malformedAttempts = 0;
9150
9442
  setupTransportAttempts = 0;
9443
+ if (closing) return turnDone();
9151
9444
  if (!response.toolCalls?.length) {
9152
9445
  break;
9153
9446
  }
9154
- const [toolCalls, systemCalls] = response.toolCalls.reduce(
9155
- (acc, tc) => {
9156
- if (tc.name === REQUEST_CONTINUATION_TOOL) acc[1].push(tc);
9157
- else acc[0].push(tc);
9158
- return acc;
9159
- },
9160
- [[], []]
9161
- );
9447
+ const toolCalls = response.toolCalls;
9162
9448
  const executedById = /* @__PURE__ */ new Map();
9163
9449
  const unknownToolIds = /* @__PURE__ */ new Set();
9164
9450
  const staleToolIds = /* @__PURE__ */ new Set();
@@ -9336,28 +9622,20 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
9336
9622
  }
9337
9623
  const foldPath = this.folds.activeNames();
9338
9624
  for (const tc of response.toolCalls) {
9339
- if (tc.name === REQUEST_CONTINUATION_TOOL) {
9625
+ const r = executedById.get(tc.id);
9626
+ if (r) {
9340
9627
  this.appendToHistory({
9341
9628
  role: "tool",
9342
9629
  content: "",
9343
- toolResult: { toolCallId: tc.id, content: HANDOFF_TOOL_RESULT_PLACEHOLDER }
9630
+ toolResult: {
9631
+ toolCallId: r.toolCallId,
9632
+ content: r.content,
9633
+ // Images DO persist, unlike the sub-agent trace above: they are the payload
9634
+ // the model must see on every replay of this turn, not a one-off UI artefact.
9635
+ // `condenseWhen` is what sheds them once they are spent — see condense-history.
9636
+ ...r.attachments ? { attachments: r.attachments } : {}
9637
+ }
9344
9638
  });
9345
- } else {
9346
- const r = executedById.get(tc.id);
9347
- if (r) {
9348
- this.appendToHistory({
9349
- role: "tool",
9350
- content: "",
9351
- toolResult: {
9352
- toolCallId: r.toolCallId,
9353
- content: r.content,
9354
- // Images DO persist, unlike the sub-agent trace above: they are the payload
9355
- // the model must see on every replay of this turn, not a one-off UI artefact.
9356
- // `condenseWhen` is what sheds them once they are spent — see condense-history.
9357
- ...r.attachments ? { attachments: r.attachments } : {}
9358
- }
9359
- });
9360
- }
9361
9639
  }
9362
9640
  }
9363
9641
  let tcMsgIdx = -1;
@@ -9421,8 +9699,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
9421
9699
  });
9422
9700
  return this.endTurnWithFailure(TURN_FAILURE_EXITS.unknownToolLimit);
9423
9701
  }
9424
- const firstContinuation = systemCalls[0];
9425
- if (!this.subAgentCompletion && this.subAgentWallThisTurn(!!firstContinuation)) {
9702
+ if (!this.subAgentCompletion && this.subAgentWallThisTurn()) {
9426
9703
  if (this.turnController.signal.aborted) return this.completeAbortedTurn();
9427
9704
  const wallAfterBatch = this.endTurnOnSubAgentWall();
9428
9705
  if (wallAfterBatch) return wallAfterBatch;
@@ -9431,10 +9708,6 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
9431
9708
  this.noticeAIServiceRefusal(this.aiServiceWall.code);
9432
9709
  this.flushAIServiceNotices();
9433
9710
  }
9434
- if (firstContinuation) {
9435
- const { summary, remaining_task: remainingTask } = firstContinuation.args;
9436
- return { reason: "agent-handoff", summary, remainingTask };
9437
- }
9438
9711
  if (this.subAgentCompletion) {
9439
9712
  return turnDone();
9440
9713
  }
@@ -9510,6 +9783,9 @@ var AI_DRIVER_EVENT_TYPES = Object.keys({
9510
9783
  "agent-changed": true,
9511
9784
  "agent-released": true,
9512
9785
  "flow-owner-changed": true,
9786
+ "handoff-started": true,
9787
+ "handoff-returned": true,
9788
+ "handoff-unwound": true,
9513
9789
  "orchestrating-start": true,
9514
9790
  "orchestrating-stop": true
9515
9791
  });
@@ -9590,6 +9866,24 @@ var BaseDriverDecorator = class _BaseDriverDecorator extends EventTarget {
9590
9866
  restoreCondensePolicies(entries) {
9591
9867
  return this.inner.restoreCondensePolicies?.(entries) ?? { restored: 0, awaitingReactivation: 0 };
9592
9868
  }
9869
+ getHandOffStack() {
9870
+ return this.inner.getHandOffStack?.() ?? [];
9871
+ }
9872
+ restoreHandOffStack(saved, savedStates, interrupted) {
9873
+ return this.inner.restoreHandOffStack?.(saved, savedStates, interrupted) ?? {};
9874
+ }
9875
+ unwindHandOffs(kind) {
9876
+ return this.inner.unwindHandOffs?.(kind) ?? Promise.resolve();
9877
+ }
9878
+ serializeWaitingCallers() {
9879
+ return this.inner.serializeWaitingCallers?.() ?? {};
9880
+ }
9881
+ getHandOffData() {
9882
+ return this.inner.getHandOffData?.() ?? {};
9883
+ }
9884
+ restoreHandOffData(saved, interrupted) {
9885
+ this.inner.restoreHandOffData?.(saved, interrupted);
9886
+ }
9593
9887
  getActiveAgent() {
9594
9888
  return this.inner.getActiveAgent();
9595
9889
  }
@@ -9698,36 +9992,64 @@ var BaseDriverDecorator = class _BaseDriverDecorator extends EventTarget {
9698
9992
  }
9699
9993
  };
9700
9994
 
9995
+ // src/components/orchestrating-driver/hand-off.ts
9996
+ var OUTCOME_KINDS = [
9997
+ "completed",
9998
+ "released",
9999
+ "cleared",
10000
+ "agent-removed",
10001
+ "state-lost"
10002
+ ];
10003
+ function handOffNote(caller) {
10004
+ 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.`;
10005
+ }
10006
+ function handBackNote(frame, outcome) {
10007
+ const how = {
10008
+ completed: `${frame.target} has finished the request you handed it.`,
10009
+ released: `${frame.target} has finished the request you handed it, without a summary. Its last reply is above.`,
10010
+ cleared: "The hand-off was cleared.",
10011
+ "agent-removed": `${frame.target} is no longer available, so the request you handed it was not finished.`,
10012
+ "state-lost": `The hand-off to ${frame.target} could not be resumed.`
10013
+ };
10014
+ const summary = outcome.kind === "completed" && outcome.message ? " Its summary is the message above." : "";
10015
+ return `[Hand-back]: ${how[outcome.kind]}${summary} Carry on with the user from here.`;
10016
+ }
10017
+ var frameCopy = (frame) => structuredClone(frame);
10018
+ var isOutcome = (value) => {
10019
+ const o = value;
10020
+ return !!o && typeof o === "object" && OUTCOME_KINDS.includes(o.kind) && (o.message === void 0 || typeof o.message === "string");
10021
+ };
10022
+ function parseHandOffFrames(saved) {
10023
+ if (!Array.isArray(saved)) return [];
10024
+ return saved.filter((value) => {
10025
+ const f = value;
10026
+ 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));
10027
+ });
10028
+ }
10029
+
9701
10030
  // src/components/orchestrating-driver/orchestrating-driver.ts
9702
10031
  var DEFAULT_MAX_HANDOFFS = 3;
10032
+ var DEFAULT_MAX_HAND_OFF_DEPTH = 1;
9703
10033
  var DEFAULT_CLASSIFIER_HISTORY_LENGTH = 4;
9704
10034
  var DEFAULT_CLASSIFIER_RETRIES = 2;
9705
10035
  var CLASSIFIER_MESSAGE_CHARS = 600;
9706
10036
  var CLASSIFIER_TRANSCRIPT_CHARS = 3e3;
9707
- var REQUEST_CONTINUATION_DEFINITION = {
9708
- name: REQUEST_CONTINUATION_TOOL,
9709
- 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.",
9710
- parameters: {
9711
- type: "object",
9712
- required: ["summary", "remaining_task"],
9713
- properties: {
9714
- summary: {
9715
- type: "string",
9716
- description: "What you found or did \u2014 passed as context to the next specialist."
9717
- },
9718
- remaining_task: {
9719
- type: "string",
9720
- description: "What still needs to be done, in plain language. Used to route to the right specialist."
9721
- }
9722
- }
9723
- }
9724
- };
9725
10037
  function isSpecialist(agent) {
9726
10038
  return !agent.fallback;
9727
10039
  }
9728
10040
  function isFallback(agent) {
9729
10041
  return agent.fallback === true;
9730
10042
  }
10043
+ function isStateful(agent) {
10044
+ return !!(agent.onActivate || agent.onDeactivate);
10045
+ }
10046
+ async function callHandOffHook(agent, hook, run) {
10047
+ try {
10048
+ await run();
10049
+ } catch (e) {
10050
+ logger2.warn(`OrchestratingDriver: ${hook}("${agent}") threw:`, e);
10051
+ }
10052
+ }
9731
10053
  function normalizeContextReserve(input) {
9732
10054
  if (input == null) return void 0;
9733
10055
  const positive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
@@ -9760,37 +10082,40 @@ Politely let the user know what you can help with and invite them to rephrase th
9760
10082
 
9761
10083
  ${UNROUTABLE_INSTRUCTION}`;
9762
10084
  }
9763
- function handoffTailNote(agent, remainingTask) {
9764
- if (isFallback(agent)) {
9765
- return "[Hand-off]: no specialist here can take on what remains. Reply to the user now, as the routing note says.";
9766
- }
9767
- const work = remainingTask?.trim();
9768
- 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.";
9769
- }
9770
10085
  var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator {
9771
10086
  constructor(providerRegistry, agents, options = {}) {
9772
10087
  super(_OrchestratingDriver.createInner(providerRegistry, agents, options));
9773
10088
  this.providerRegistry = providerRegistry;
9774
10089
  this.agents = agents;
9775
10090
  /**
9776
- * Set by `cancel()` for the duration of a `sendMessage` run. Breaks the
9777
- * handoff loop so we don't classify or start another agent turn after the
9778
- * user stops. Reset at the start of each `sendMessage`.
10091
+ * Set by `cancel()` for the duration of a run. Stops it before the next agent
10092
+ * runs — a hand-off's target or a hand-back's caller — so nothing more is asked
10093
+ * of a provider after the user stops. Reset at the start of each run.
9779
10094
  */
9780
10095
  this.cancelled = false;
9781
10096
  /**
9782
10097
  * Whether the CURRENT turn's user message has reached the inner driver's
9783
10098
  * history. False through the pre-first-turn classify (where the only echo of
9784
10099
  * the message is an optimistic `history-updated` dispatch), true from the
9785
- * moment `chatDriver.sendMessage` is entered — including through every later
9786
- * handoff classify. Read by the budget-wall catch in `sendMessage` to decide
10100
+ * moment `chatDriver.sendMessage` is entered. Read by the budget-wall catch in
10101
+ * `sendMessage` to decide
9787
10102
  * whether `reportBudgetExhausted` must append the message itself: appending
9788
10103
  * it when already appended duplicated it; not appending it when unappended
9789
10104
  * made it vanish. Reset at the top of each `runOrchestratedTurn`.
9790
10105
  */
9791
10106
  this.userMessageAppended = false;
10107
+ /**
10108
+ * The hand-offs in progress, oldest first (GENC-1737). The top frame's target is the active agent
10109
+ * and holds the flow lock; each caller waits, suspended, for the target above it. Saved with the
10110
+ * session through {@link OrchestratingDriver.getHandOffStack}.
10111
+ */
10112
+ this.handOffStack = [];
10113
+ /** Hand-offs and hand-backs this user turn, against `maxHandoffs`. */
10114
+ this.handOffsThisTurn = 0;
9792
10115
  this.sessionKey = options.sessionKey ?? "";
9793
10116
  this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS;
10117
+ this.maxHandOffDepth = options.maxHandOffDepth ?? DEFAULT_MAX_HAND_OFF_DEPTH;
10118
+ this.chatDriver.setHandOffCheck((request, caller) => this.checkHandOff(request, caller));
9794
10119
  this.classifierHistoryLength = options.classifierHistoryLength ?? DEFAULT_CLASSIFIER_HISTORY_LENGTH;
9795
10120
  this.classifierRetries = options.classifierRetries ?? DEFAULT_CLASSIFIER_RETRIES;
9796
10121
  this.specialists = agents.filter(isSpecialist).filter((a) => !a.excludeFromClassifier);
@@ -9843,7 +10168,7 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
9843
10168
  activityBus: options.activityBus,
9844
10169
  budgetExhaustedMessage: options.budgetExhaustedMessage,
9845
10170
  aiServiceErrorMessages: options.aiServiceErrorMessages,
9846
- // A release ends this driver's handoff loop, so it acts on the flag itself; see the option.
10171
+ // A release can hand back to a waiting caller, so this driver acts on the flag itself.
9847
10172
  deferAgentRelease: true
9848
10173
  });
9849
10174
  }
@@ -9883,6 +10208,8 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
9883
10208
  async runOrchestratedTurn(input, attachments, options, delegated) {
9884
10209
  this.cancelled = false;
9885
10210
  this.userMessageAppended = false;
10211
+ this.handOffsThisTurn = 0;
10212
+ if (this.pendingHandBack?.resumed) this.pendingHandBack = void 0;
9886
10213
  const history = this.chatDriver.getHistory();
9887
10214
  this.dispatchEvent(
9888
10215
  new CustomEvent("history-updated", {
@@ -9900,66 +10227,16 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
9900
10227
  } else {
9901
10228
  routed = await this.classify(routingInput, history, this.activeAgent);
9902
10229
  }
9903
- let currentAgent = routed.agent;
9904
- let isHandoff = false;
9905
- let handoffs = 0;
9906
- let handoffSummary = "";
9907
- let remainingTask = "";
9908
- const attempted = [];
9909
10230
  let lastResult;
9910
- while (true) {
9911
- if (this.cancelled) break;
9912
- await this.installAgent(currentAgent, pin);
9913
- if (this.cancelled) break;
9914
- let result;
9915
- if (isHandoff) {
9916
- const contextPrimer = handoffSummary ? [{ role: "user", content: `[Context from previous agent]: ${handoffSummary}` }] : [];
9917
- if (isFallback(currentAgent)) {
9918
- const work = remainingTask?.trim() || handoffSummary?.trim();
9919
- contextPrimer.push({
9920
- role: "user",
9921
- content: (work ? `[Routing note]: no specialist is available for the remaining work: ${work}
9922
- ` : "[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."
9923
- });
9924
- }
9925
- result = await this.chatDriver.continueFromHistory(contextPrimer, {
9926
- tailNote: handoffTailNote(currentAgent, remainingTask)
9927
- });
9928
- } else {
10231
+ if (!this.cancelled) {
10232
+ await this.endOrphanedHandOffs(routed.agent);
10233
+ await this.installAgent(routed.agent);
10234
+ if (!this.cancelled) {
9929
10235
  this.userMessageAppended = true;
9930
- result = await this.chatDriver.sendMessage(input, attachments, options);
9931
- }
9932
- lastResult = result;
9933
- if (this.chatDriver.getAgentReleaseRequested()) {
9934
- await this.releaseActiveAgent();
9935
- break;
9936
- }
9937
- if (result.reason !== "agent-handoff" || isFallback(currentAgent) || pinned) {
9938
- break;
9939
- }
9940
- if (this.cancelled) break;
9941
- handoffs += 1;
9942
- if (handoffs > this.maxHandoffs) {
9943
- this.appendInlineMessage(
9944
- `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.`
10236
+ lastResult = await this.carryOn(
10237
+ await this.chatDriver.sendMessage(input, attachments, options)
9945
10238
  );
9946
- break;
9947
10239
  }
9948
- handoffSummary = result.summary;
9949
- remainingTask = result.remainingTask;
9950
- isHandoff = true;
9951
- if (isSpecialist(currentAgent) && !attempted.includes(currentAgent.name)) {
9952
- attempted.push(currentAgent.name);
9953
- }
9954
- const updatedHistory = this.chatDriver.getHistory();
9955
- this.dispatchEvent(new CustomEvent("orchestrating-start"));
9956
- routed = await this.classify(
9957
- remainingTask?.trim() || routingInput,
9958
- updatedHistory,
9959
- void 0,
9960
- attempted
9961
- );
9962
- currentAgent = routed.agent;
9963
10240
  }
9964
10241
  if (this.cancelled && !this.userMessageAppended) {
9965
10242
  this.chatDriver.reportStoppedBeforeTurn({
@@ -9969,7 +10246,74 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
9969
10246
  ...delegated
9970
10247
  });
9971
10248
  }
9972
- return lastResult?.reason === "done" ? lastResult : { reason: "done" };
10249
+ return lastResult ?? { reason: "done" };
10250
+ }
10251
+ /**
10252
+ * Act on what the agent call that just ended asked for, and run the next agent in the same user
10253
+ * turn while there is one (GENC-1737): a `handOff` runs its target with the brief, and a target
10254
+ * that releases hands back to its caller. Returns the last call's result.
10255
+ *
10256
+ * A failed call ends the turn there, as a Stop does: what it asked for still happens, and the
10257
+ * next agent runs on the next message. Running it now would meet the same wall (a budget, a
10258
+ * refused account, a full context window), or build on a reply that never came.
10259
+ */
10260
+ async carryOn(result) {
10261
+ let last = result;
10262
+ let tailNote = await this.afterAgentCall();
10263
+ while (tailNote !== void 0 && !this.cancelled && !last.failureReason) {
10264
+ last = await this.chatDriver.continueFromHistory(void 0, { tailNote });
10265
+ tailNote = await this.afterAgentCall();
10266
+ }
10267
+ return last;
10268
+ }
10269
+ /**
10270
+ * After one agent call: start the hand-off it asked for, or release it and, when it was a
10271
+ * hand-off's target, hand back. Returns the tail note for the agent to run next, or `undefined`
10272
+ * when the turn ends here.
10273
+ *
10274
+ * A plain target hands back when its call ends, released or not, as a plain agent is done once
10275
+ * it has answered. A stateful one holds the hand-off until it releases, as it holds its flow.
10276
+ */
10277
+ async afterAgentCall() {
10278
+ const request = this.chatDriver.getHandOffRequest();
10279
+ if (request) return this.startHandOff(request);
10280
+ const active = this.agentSession.getActive();
10281
+ if (!active) return void 0;
10282
+ const frame = this.topFrameFor(active.name);
10283
+ const released = this.chatDriver.getAgentReleaseRequested();
10284
+ if (!released && !(frame && !isStateful(active))) {
10285
+ if (!isStateful(active)) this.endPlainTurn(active);
10286
+ return void 0;
10287
+ }
10288
+ if (!frame) {
10289
+ await this.releaseActiveAgent();
10290
+ return void 0;
10291
+ }
10292
+ frame.outcome = this.outcomeOf(frame);
10293
+ await this.releaseActiveAgent();
10294
+ return this.handBack(frame, frame.outcome);
10295
+ }
10296
+ /** How the target of `frame` finished: with `completeHandOff`, or a release and its last reply. */
10297
+ outcomeOf(frame) {
10298
+ const completion = this.chatDriver.getHandOffCompletion();
10299
+ if (completion) {
10300
+ return {
10301
+ kind: "completed",
10302
+ message: completion.message,
10303
+ ...completion.data !== void 0 && { data: completion.data }
10304
+ };
10305
+ }
10306
+ const finalReply = this.lastReply(frame);
10307
+ return { kind: "released", ...finalReply ? { message: finalReply } : {} };
10308
+ }
10309
+ /**
10310
+ * A plain agent is done once it has answered (GENC-1737): the data a one-way hand-off gave it ends
10311
+ * with its turn, and so does a flow lock no hand-off holds, such as one a restore left on a
10312
+ * target whose hand-off it could not resume.
10313
+ */
10314
+ endPlainTurn(agent) {
10315
+ this.agentSession.setHandOffData(agent.name, void 0);
10316
+ if (this.agentSession.getFlowOwner() === agent.name) this.agentSession.dropFlowLock();
9973
10317
  }
9974
10318
  /** {@inheritDoc AiDriver.canRestoreFlowOwner} */
9975
10319
  canRestoreFlowOwner(agentName) {
@@ -9993,15 +10337,33 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
9993
10337
  * undecorated.
9994
10338
  */
9995
10339
  async resumeRestoredFlow(resumeNote) {
10340
+ if (this.pendingHandBack) return this.resumeHandBack(resumeNote);
9996
10341
  const owner = this.agentSession.getFlowOwner();
9997
10342
  const agent = owner ? this.agents.find((a) => a.name === owner) : void 0;
9998
10343
  if (!agent) return void 0;
9999
10344
  this.cancelled = false;
10345
+ this.handOffsThisTurn = 0;
10000
10346
  await this.installAgent(agent);
10001
10347
  if (!resumeNote || agent.resumeOnRestore === false || this.cancelled) return void 0;
10002
- const result = await this.chatDriver.continueFromHistory(void 0, { tailNote: resumeNote });
10003
- await this.releaseIfRequested();
10004
- return result;
10348
+ return this.carryOn(
10349
+ await this.chatDriver.continueFromHistory(void 0, { tailNote: resumeNote })
10350
+ );
10351
+ }
10352
+ /**
10353
+ * The hand-back a restore found due (GENC-1737). The first call, inside the host's restore gate,
10354
+ * makes the caller active again and tells it the outcome; the call with the note runs its turn.
10355
+ */
10356
+ async resumeHandBack(note) {
10357
+ const pending = this.pendingHandBack;
10358
+ this.cancelled = false;
10359
+ if (!pending.resumed) {
10360
+ pending.resumed = true;
10361
+ await this.handBack(pending.frame, pending.frame.outcome);
10362
+ }
10363
+ if (!note || this.cancelled) return void 0;
10364
+ this.pendingHandBack = void 0;
10365
+ this.handOffsThisTurn = 0;
10366
+ return this.carryOn(await this.chatDriver.continueFromHistory(void 0, { tailNote: note }));
10005
10367
  }
10006
10368
  /**
10007
10369
  * A continuation outside a routed turn — a decorator carrying the agent on, say — can finish the
@@ -10009,15 +10371,9 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
10009
10371
  * (GENC-1660), only `sendMessage` checked, and a flow finished by a continuation stayed locked.
10010
10372
  */
10011
10373
  async continueFromHistory(transientPrimer, options) {
10012
- const result = await this.chatDriver.continueFromHistory(transientPrimer, options);
10013
- await this.releaseIfRequested();
10014
- return result;
10015
- }
10016
- /** Release the flow if the turn that just ended asked to, and an agent is still active to release. */
10017
- async releaseIfRequested() {
10018
- if (this.chatDriver.getAgentReleaseRequested() && this.agentSession.getActive()) {
10019
- await this.releaseActiveAgent();
10020
- }
10374
+ this.cancelled = false;
10375
+ this.handOffsThisTurn = 0;
10376
+ return this.carryOn(await this.chatDriver.continueFromHistory(transientPrimer, options));
10021
10377
  }
10022
10378
  /**
10023
10379
  * {@inheritDoc AiDriver.getContextReserve}
@@ -10045,25 +10401,18 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
10045
10401
  }
10046
10402
  return winner ? { ...winner, agentName, basis: "reachable-max" } : void 0;
10047
10403
  }
10404
+ /** Make `agent` active through the session, firing its hooks, and install it. */
10405
+ async installAgent(agent) {
10406
+ const { previousAgent } = await this.agentSession.activate(agent);
10407
+ this.applyAgent(agent, previousAgent?.name);
10408
+ }
10048
10409
  /**
10049
- * `pin` stands in for the user-pin when given, as for
10050
- * {@link OrchestratingDriver.resolvePinnedAgent}.
10410
+ * Install an agent the session has already made active on the inner driver: the transcript's
10411
+ * divider when another agent spoke before it, and its own view of the history. What is left here
10412
+ * is this driver's own job, the transcript; the session owns the lifecycle.
10051
10413
  */
10052
- async installAgent(agent, pin) {
10053
- const { previousAgent } = await this.agentSession.activate(agent);
10054
- const isTerminal = isFallback(agent) || (pin === void 0 ? this.agentSession.getPinned() : pin) !== null || this.agentSession.getFlowOwner() !== null || !!(agent.onActivate || agent.onDeactivate);
10055
- let agentToApply = agent;
10056
- if (!isTerminal) {
10057
- const declaredTools = agent.toolDefinitions;
10058
- agentToApply = {
10059
- ...agent,
10060
- toolDefinitions: typeof declaredTools === "function" ? async (ctx) => [
10061
- ...await declaredTools(ctx),
10062
- REQUEST_CONTINUATION_DEFINITION
10063
- ] : [...declaredTools ?? [], REQUEST_CONTINUATION_DEFINITION]
10064
- };
10065
- }
10066
- if (previousAgent && previousAgent.name !== agent.name) {
10414
+ applyAgent(agent, previousName) {
10415
+ if (previousName && previousName !== agent.name) {
10067
10416
  const rawHistory = this.chatDriver.getHistory();
10068
10417
  this.chatDriver.loadHistory([
10069
10418
  ...rawHistory,
@@ -10071,7 +10420,7 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
10071
10420
  ]);
10072
10421
  }
10073
10422
  this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
10074
- this.chatDriver.installAgent(agentToApply);
10423
+ this.chatDriver.installAgent(agent);
10075
10424
  this.dispatchEvent(new CustomEvent("orchestrating-stop"));
10076
10425
  }
10077
10426
  /**
@@ -10088,10 +10437,250 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
10088
10437
  async releaseActiveAgent() {
10089
10438
  await this.agentSession.release();
10090
10439
  }
10440
+ /** The agent named `name` as this driver installs it: the fallback with its prompt built. */
10441
+ agentNamed(name) {
10442
+ if (this.fallback?.name === name) return this.fallback;
10443
+ return this.agents.find((a) => a.name === name);
10444
+ }
10445
+ /**
10446
+ * End the hand-offs when a turn runs an agent other than the top target (GENC-1737): the lock
10447
+ * that kept turns on the target was taken away, by a host clearing `flowOwnerAgentName` say,
10448
+ * and its caller would otherwise wait for good.
10449
+ */
10450
+ async endOrphanedHandOffs(agent) {
10451
+ const top = this.handOffStack[this.handOffStack.length - 1];
10452
+ if (!top || top.outcome || top.target === agent.name) return;
10453
+ await this.unwindHandOffs("cleared");
10454
+ await this.agentSession.endSuspended();
10455
+ }
10456
+ /**
10457
+ * Check a handler's `handOff` when it is made (GENC-1737), so a refusal reaches its model. Checked
10458
+ * against the agents and the stack as they are now; the hand-off itself starts after the call.
10459
+ */
10460
+ checkHandOff(request, caller) {
10461
+ const target = this.agentNamed(request.to);
10462
+ if (!target) return { ok: false, reason: "unknown-agent" };
10463
+ const allowed = this.agents.find((a) => a.name === caller)?.handOffTargets;
10464
+ if (allowed && !allowed.includes(target.name)) return { ok: false, reason: "not-allowed" };
10465
+ if (target.name === caller || this.handOffStack.some((f) => f.caller === target.name || f.target === target.name)) {
10466
+ return { ok: false, reason: "re-entry" };
10467
+ }
10468
+ if (request.returnTo !== false && this.handOffStack.length >= this.maxHandOffDepth) {
10469
+ return { ok: false, reason: "depth" };
10470
+ }
10471
+ if (this.handOffsThisTurn >= this.maxHandoffs) return { ok: false, reason: "limit" };
10472
+ return { ok: true };
10473
+ }
10474
+ /**
10475
+ * Start the hand-off the agent call just made, and return the target's tail note (GENC-1737).
10476
+ *
10477
+ * - `returnTo` (the default): push a frame. The caller waits without being deactivated, its
10478
+ * `agentEnd` payloads suspended, and the target takes the flow lock.
10479
+ * - One-way from a hand-off's target: that frame's caller now waits for the new target instead.
10480
+ * - One-way with nobody waiting: a plain switch, the caller deactivated as on any swap.
10481
+ *
10482
+ * Either way the brief is recorded as a message the caller sent on the user's behalf.
10483
+ */
10484
+ async startHandOff(request) {
10485
+ const caller = this.agentSession.getActive();
10486
+ const target = this.agentNamed(request.to);
10487
+ if (!caller || !target) return void 0;
10488
+ this.handOffsThisTurn += 1;
10489
+ const frame = {
10490
+ id: crypto.randomUUID(),
10491
+ caller: caller.name,
10492
+ target: target.name,
10493
+ returnTo: request.returnTo !== false,
10494
+ message: request.message,
10495
+ ...request.data !== void 0 && { data: request.data },
10496
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
10497
+ };
10498
+ const top = this.handOffStack[this.handOffStack.length - 1];
10499
+ if (frame.returnTo) {
10500
+ this.handOffStack.push(frame);
10501
+ await callHandOffHook(
10502
+ caller.name,
10503
+ "onHandOffStart",
10504
+ () => caller.onHandOffStart?.(frameCopy(frame))
10505
+ );
10506
+ this.chatDriver.suspendActivation(caller.name);
10507
+ await this.agentSession.handOff(target, { suspendCaller: true });
10508
+ } else if (top?.target === caller.name) {
10509
+ top.target = target.name;
10510
+ await this.agentSession.handOff(target, { suspendCaller: false });
10511
+ } else {
10512
+ await this.agentSession.activate(target);
10513
+ }
10514
+ this.applyAgent(this.agentSession.getActive() ?? target, caller.name);
10515
+ this.agentSession.setHandOffData(target.name, frameCopy(frame).data);
10516
+ this.recordDelegated(caller.name, frame.message, `Handed over by ${caller.name}`);
10517
+ this.dispatchEvent(new CustomEvent("handoff-started", { detail: frameCopy(frame) }));
10518
+ return handOffNote(caller.name);
10519
+ }
10520
+ /**
10521
+ * Hand back to `frame`'s caller with `outcome`, and return its tail note (GENC-1737). The outcome
10522
+ * is kept on the frame until the caller is active again and its `onHandOffReturn` has run, so a
10523
+ * reload in between still hands back; the frame then goes, before the caller's model call.
10524
+ */
10525
+ async handBack(frame, outcome) {
10526
+ frame.outcome = outcome;
10527
+ this.handOffsThisTurn += 1;
10528
+ const remaining = this.handOffStack.filter((f) => f !== frame);
10529
+ const caller = this.agentNamed(frame.caller);
10530
+ if (!caller) {
10531
+ this.handOffStack = remaining;
10532
+ this.unwound(frame, "agent-removed");
10533
+ return void 0;
10534
+ }
10535
+ await this.agentSession.resume(caller, remaining.length > 0 || isStateful(caller));
10536
+ const live2 = this.agentSession.getActive() ?? caller;
10537
+ this.applyAgent(live2, frame.target);
10538
+ if (outcome.kind === "completed" && outcome.message) {
10539
+ this.recordDelegated(frame.target, outcome.message, `Handed back by ${frame.target}`);
10540
+ }
10541
+ await callHandOffHook(
10542
+ live2.name,
10543
+ "onHandOffReturn",
10544
+ () => live2.onHandOffReturn?.(structuredClone(outcome), frameCopy(frame))
10545
+ );
10546
+ this.handOffStack = remaining;
10547
+ this.dispatchEvent(new CustomEvent("handoff-returned", { detail: frameCopy(frame) }));
10548
+ return handBackNote(frame, outcome);
10549
+ }
10550
+ /** The top frame, when `agent` is its target. */
10551
+ topFrameFor(agent) {
10552
+ const top = this.handOffStack[this.handOffStack.length - 1];
10553
+ return top?.target === agent ? top : void 0;
10554
+ }
10555
+ /** The target's last reply since its brief, for a target that released without a summary. */
10556
+ lastReply(frame) {
10557
+ const history = this.chatDriver.getHistory();
10558
+ for (let i = history.length - 1; i >= 0; i -= 1) {
10559
+ const m = history[i];
10560
+ if (m.role === "user" && m.delegate === frame.caller) return void 0;
10561
+ if (m.role === "assistant" && m.agentName === frame.target && reachesProvider(m) && m.content?.trim()) {
10562
+ return m.content;
10563
+ }
10564
+ }
10565
+ return void 0;
10566
+ }
10567
+ /** The brief, in the transcript as a message the caller sent on the user's behalf. */
10568
+ recordDelegated(from, message, framing) {
10569
+ const history = this.chatDriver.getHistory();
10570
+ this.chatDriver.loadHistory([
10571
+ ...history,
10572
+ {
10573
+ role: "user",
10574
+ content: message,
10575
+ delegate: from,
10576
+ delegateFraming: framing,
10577
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
10578
+ }
10579
+ ]);
10580
+ this.dispatchEvent(
10581
+ new CustomEvent("history-updated", { detail: this.chatDriver.getHistory() })
10582
+ );
10583
+ }
10584
+ /** End `frame` with `kind` and no hand-back, for the host. */
10585
+ unwound(frame, kind) {
10586
+ frame.outcome = { kind };
10587
+ this.dispatchEvent(new CustomEvent("handoff-unwound", { detail: frameCopy(frame) }));
10588
+ }
10589
+ /** {@inheritDoc AiDriver.getHandOffStack} */
10590
+ getHandOffStack() {
10591
+ return this.handOffStack.map(frameCopy);
10592
+ }
10593
+ /** {@inheritDoc AiDriver.restoreHandOffStack} */
10594
+ restoreHandOffStack(saved, savedStates, interrupted) {
10595
+ this.handOffStack = [];
10596
+ this.pendingHandBack = void 0;
10597
+ const kept = [];
10598
+ const unwoundTargets = /* @__PURE__ */ new Set();
10599
+ for (const frame of parseHandOffFrames(saved)) {
10600
+ const caller = this.agents.find((a) => a.name === frame.caller);
10601
+ const outcome = frame.outcome;
10602
+ if (!caller) {
10603
+ this.unwound(frame, "agent-removed");
10604
+ } else if (interrupted || caller.serialize && savedStates?.[caller.name] === void 0) {
10605
+ this.unwound(frame, "state-lost");
10606
+ } else {
10607
+ kept.push(frame);
10608
+ continue;
10609
+ }
10610
+ this.agentSession.setHandOffData(frame.caller, void 0);
10611
+ unwoundTargets.add(frame.target);
10612
+ const below = kept[kept.length - 1];
10613
+ if (below?.target === frame.caller) {
10614
+ below.target = frame.target;
10615
+ if (outcome) below.outcome = outcome;
10616
+ }
10617
+ }
10618
+ const top = kept[kept.length - 1];
10619
+ if (top && !top.outcome && !this.agents.some((a) => a.name === top.target)) {
10620
+ top.outcome = { kind: "agent-removed" };
10621
+ }
10622
+ this.handOffStack = kept;
10623
+ this.chatDriver.suspendRestoredActivations(kept.map((f) => f.caller));
10624
+ const droppedLocks = [...unwoundTargets].filter((name) => {
10625
+ const target = this.agents.find((a) => a.name === name);
10626
+ return !!target && !isStateful(target) && !kept.some((f) => f.target === name);
10627
+ });
10628
+ const dropped = droppedLocks.length ? { droppedLocks } : {};
10629
+ if (!top?.outcome) return dropped;
10630
+ this.pendingHandBack = { frame: top, resumed: false };
10631
+ return { ...dropped, resume: { agent: top.caller, note: handBackNote(top, top.outcome) } };
10632
+ }
10633
+ /** {@inheritDoc AiDriver.unwindHandOffs} */
10634
+ async unwindHandOffs(kind) {
10635
+ const frames = [...this.handOffStack].reverse();
10636
+ this.handOffStack = [];
10637
+ this.pendingHandBack = void 0;
10638
+ this.chatDriver.endSuspendedActivations();
10639
+ for (const frame of frames) {
10640
+ this.unwound(frame, kind);
10641
+ const live2 = this.agentSession.getSuspended().find((a) => a.name === frame.caller);
10642
+ if (kind === "cleared" && live2) {
10643
+ await callHandOffHook(
10644
+ live2.name,
10645
+ "onHandOffReturn",
10646
+ () => live2.onHandOffReturn?.(structuredClone(frame.outcome), frameCopy(frame))
10647
+ );
10648
+ }
10649
+ }
10650
+ }
10651
+ /** {@inheritDoc AiDriver.getHandOffData} */
10652
+ getHandOffData() {
10653
+ const live2 = [this.agentSession.getFlowOwner(), ...this.handOffStack.map((f) => f.caller)];
10654
+ const saved = {};
10655
+ for (const name of live2) {
10656
+ const data = name ? this.agentSession.getHandOffData(name) : void 0;
10657
+ if (name && data !== void 0) saved[name] = data;
10658
+ }
10659
+ return saved;
10660
+ }
10661
+ /** {@inheritDoc AiDriver.restoreHandOffData} */
10662
+ restoreHandOffData(saved, interrupted) {
10663
+ if (interrupted || !saved || typeof saved !== "object" || Array.isArray(saved)) return;
10664
+ for (const [name, data] of Object.entries(saved)) this.agentSession.setHandOffData(name, data);
10665
+ }
10666
+ /** {@inheritDoc AiDriver.serializeWaitingCallers} */
10667
+ serializeWaitingCallers() {
10668
+ const states = {};
10669
+ for (const caller of this.agentSession.getSuspended()) {
10670
+ if (!caller.serialize) continue;
10671
+ try {
10672
+ const state = caller.serialize();
10673
+ if (state !== void 0) states[caller.name] = state;
10674
+ } catch (e) {
10675
+ logger2.error(`OrchestratingDriver: serialize("${caller.name}") threw:`, e);
10676
+ }
10677
+ }
10678
+ return states;
10679
+ }
10091
10680
  /**
10092
10681
  * Stop the current turn (user "stop" button). Cancels the inner chat driver's
10093
- * in-flight request and breaks the handoff loop so no further agent turn or
10094
- * classify runs. The driver stays usable for the next message.
10682
+ * in-flight request, and no further agent runs this turn, not even a hand-off's
10683
+ * target or a hand-back. The driver stays usable for the next message.
10095
10684
  */
10096
10685
  cancel() {
10097
10686
  this.cancelled = true;
@@ -10128,12 +10717,8 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
10128
10717
  decision,
10129
10718
  to: agent.name,
10130
10719
  fallback: isFallback(agent),
10131
- // The BALLOT, not `this.specialists`. `index` is a position in the ballot (see its
10132
- // construction in `classify`), so publishing the unfiltered specialist list beside it made
10133
- // `candidates[index]` resolve to the wrong agent whenever `excluded` was non-empty — and the
10134
- // README sentence invited exactly that join. `this.specialists` stays recoverable as
10135
- // `candidates ∪ excluded`. Absent where nothing was put to a vote, which is more truthful
10136
- // than a full list. Reviewer catch on #2496.
10720
+ // The ballot, so `candidates[index]` is the agent chosen. Absent where nothing was put to a
10721
+ // vote, which is more truthful than a full list.
10137
10722
  ...ballot ? { candidates: ballot } : {},
10138
10723
  ...rest,
10139
10724
  ...usage ? { usage } : {},
@@ -10156,7 +10741,7 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
10156
10741
  }
10157
10742
  return lines.join("\n");
10158
10743
  }
10159
- async classify(input, history, contextAgent, excludeAgents = []) {
10744
+ async classify(input, history, contextAgent) {
10160
10745
  const classifyStartedAt = Date.now();
10161
10746
  if (this.specialists.length === 1 && !this.fallback) {
10162
10747
  const only = this.specialists[0];
@@ -10179,27 +10764,16 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
10179
10764
  this.recordRouting("no-specialists", leaf, { durationMs: Date.now() - classifyStartedAt });
10180
10765
  return { agent: leaf, decision: "no-specialists" };
10181
10766
  }
10182
- const ballot = excludeAgents.length ? this.specialists.filter((a) => !excludeAgents.includes(a.name)) : this.specialists;
10183
- if (ballot.length === 0) {
10184
- const leaf = this.fallback ?? this.specialists[0];
10185
- this.recordRouting("ballot-exhausted", leaf, {
10186
- excluded: excludeAgents,
10187
- durationMs: Date.now() - classifyStartedAt
10188
- });
10189
- return { agent: leaf, decision: "ballot-exhausted" };
10190
- }
10767
+ const ballot = this.specialists;
10191
10768
  const agentList = ballot.map((a, i) => `${i}: ${a.name} \u2014 ${a.description}`).join("\n");
10192
10769
  const recentMessages = this.classifierTranscript(history);
10193
- const continuationHint = contextAgent && isSpecialist(contextAgent) && !excludeAgents.includes(contextAgent.name) ? `
10770
+ const continuationHint = contextAgent && isSpecialist(contextAgent) ? `
10194
10771
 
10195
10772
  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.` : "";
10196
- const attemptedHint = excludeAgents.length ? `
10197
-
10198
- Already attempted this turn and handed the work on, so not listed above: ${excludeAgents.join(", ")}. Do not expect another agent to repeat their part.` : "";
10199
- 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.
10773
+ 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.
10200
10774
 
10201
10775
  Agents:
10202
- ${agentList}${continuationHint}${attemptedHint}${recentMessages ? `
10776
+ ${agentList}${continuationHint}${recentMessages ? `
10203
10777
 
10204
10778
  Recent conversation:
10205
10779
  ${recentMessages}` : ""}`;
@@ -10247,8 +10821,7 @@ ${recentMessages}` : ""}`;
10247
10821
  attempts,
10248
10822
  durationMs: Date.now() - classifyStartedAt,
10249
10823
  response,
10250
- ballot: ballot.map((a) => a.name),
10251
- ...excludeAgents.length ? { excluded: excludeAgents } : {}
10824
+ ballot: ballot.map((a) => a.name)
10252
10825
  });
10253
10826
  return { agent: chosen, decision: "classified" };
10254
10827
  }
@@ -10269,8 +10842,7 @@ ${recentMessages}` : ""}`;
10269
10842
  durationMs: Date.now() - classifyStartedAt,
10270
10843
  ballot: ballot.map((a) => a.name),
10271
10844
  ...lastError ? { error: lastError } : {},
10272
- ...lastResponse ? { response: lastResponse } : {},
10273
- ...excludeAgents.length ? { excluded: excludeAgents } : {}
10845
+ ...lastResponse ? { response: lastResponse } : {}
10274
10846
  };
10275
10847
  if (this.fallback) {
10276
10848
  this.recordRouting(decision, this.fallback, routingDetail);
@@ -10308,7 +10880,7 @@ ${recentMessages}` : ""}`;
10308
10880
  return match;
10309
10881
  }
10310
10882
  /**
10311
- * Append prose the ORCHESTRATOR wrote — the handoff cap, the no-specialist apology. Flagged
10883
+ * Append prose the ORCHESTRATOR wrote — the no-specialist apology. Flagged
10312
10884
  * `excludeFromProvider` for the same reason the driver's own notices are: it is not something a
10313
10885
  * model said, and feeding it back invites the model to echo it. See
10314
10886
  * `ChatMessage.excludeFromProvider`.
@@ -10816,7 +11388,6 @@ export {
10816
11388
  OrchestratingDriver,
10817
11389
  PROVIDER_REFUSED_CODE,
10818
11390
  ProviderRefusedError,
10819
- REQUEST_CONTINUATION_TOOL,
10820
11391
  SUPPORTED_ANTHROPIC_MODEL_IDS,
10821
11392
  SUPPORTED_GEMINI_MODEL_IDS,
10822
11393
  UNROUTABLE_INSTRUCTION,