@genesislcap/ai-assistant 15.24.2 → 15.25.1

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 (47) hide show
  1. package/dist/ai-assistant.api.json +1065 -33
  2. package/dist/ai-assistant.d.ts +288 -102
  3. package/dist/chat-driver.cjs +630 -378
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +630 -378
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +1198 -388
  8. package/dist/dts/components/agent-session/agent-session.d.ts +134 -0
  9. package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -0
  10. package/dist/dts/components/agent-session/index.d.ts +2 -0
  11. package/dist/dts/components/agent-session/index.d.ts.map +1 -0
  12. package/dist/dts/components/ai-driver/ai-driver.d.ts +31 -4
  13. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  14. package/dist/dts/components/chat-driver/chat-driver.constants.d.ts +52 -0
  15. package/dist/dts/components/chat-driver/chat-driver.constants.d.ts.map +1 -0
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts +100 -82
  17. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  18. package/dist/dts/components/chat-driver/fold-controller.d.ts +82 -0
  19. package/dist/dts/components/chat-driver/fold-controller.d.ts.map +1 -0
  20. package/dist/dts/components/chat-driver/turn-failures.d.ts +20 -1
  21. package/dist/dts/components/chat-driver/turn-failures.d.ts.map +1 -1
  22. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +14 -24
  23. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  24. package/dist/dts/index.d.ts +1 -0
  25. package/dist/dts/index.d.ts.map +1 -1
  26. package/dist/dts/main/main.d.ts +6 -16
  27. package/dist/dts/main/main.d.ts.map +1 -1
  28. package/dist/dts/suggestions/generate-suggestions.d.ts +35 -0
  29. package/dist/dts/suggestions/generate-suggestions.d.ts.map +1 -0
  30. package/dist/dts/utils/agents-key.d.ts +10 -0
  31. package/dist/dts/utils/agents-key.d.ts.map +1 -0
  32. package/dist/dts/utils/resolve-turn-input.d.ts +9 -0
  33. package/dist/dts/utils/resolve-turn-input.d.ts.map +1 -0
  34. package/dist/esm/components/agent-session/agent-session.js +208 -0
  35. package/dist/esm/components/agent-session/index.js +1 -0
  36. package/dist/esm/components/chat-driver/chat-driver.constants.js +72 -0
  37. package/dist/esm/components/chat-driver/chat-driver.js +190 -395
  38. package/dist/esm/components/chat-driver/fold-controller.js +184 -0
  39. package/dist/esm/components/chat-driver/turn-failures.js +14 -0
  40. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +48 -131
  41. package/dist/esm/index.js +1 -0
  42. package/dist/esm/main/main.js +11 -38
  43. package/dist/esm/suggestions/generate-suggestions.js +123 -0
  44. package/dist/esm/utils/agents-key.js +24 -0
  45. package/dist/esm/utils/resolve-turn-input.js +16 -0
  46. package/docs/migration-GENC-1580.md +44 -36
  47. package/package.json +16 -16
@@ -3854,6 +3854,77 @@ function createInteractionContext(interactionId) {
3854
3854
  };
3855
3855
  }
3856
3856
 
3857
+ // src/suggestions/generate-suggestions.ts
3858
+ var HISTORY_WINDOW = 8;
3859
+ function capabilities(ctx, allAgentInfo) {
3860
+ let agentContext = "";
3861
+ let toolContext = "";
3862
+ if (allAgentInfo?.length) {
3863
+ const agentDescriptions = allAgentInfo.map((agent) => {
3864
+ const tools = agent.tools.map((t) => t.name).join(", ");
3865
+ return `- ${agent.name} (${agent.description}): ${tools ? `(Tools: ${tools})` : "No tools"}`;
3866
+ }).join("\n");
3867
+ agentContext = `The assistant has the following capabilities:
3868
+ ${agentDescriptions}`;
3869
+ const allToolNames = [
3870
+ ...new Set(allAgentInfo.flatMap((agent) => agent.tools.map((t) => t.name)))
3871
+ ].join(", ");
3872
+ toolContext = allToolNames ? `You have access to the following tools across all agents: ${allToolNames}.` : "";
3873
+ } else if (ctx.activeAgentName) {
3874
+ const toolNames = ctx.toolDefinitions.map((tool) => tool.name).join(", ");
3875
+ agentContext = `You are currently acting as the "${ctx.activeAgentName}" agent.`;
3876
+ toolContext = toolNames ? `You have access to the following tools: ${toolNames}.` : "";
3877
+ }
3878
+ return agentContext || toolContext ? `
3879
+
3880
+ <capabilities>
3881
+ ${[agentContext, toolContext].filter(Boolean).join(
3882
+ "\n"
3883
+ )}
3884
+ </capabilities>
3885
+ Suggestions must only cover what the agent is capable of. Do not suggest anything outside these capabilities.` : "";
3886
+ }
3887
+ var OUTPUT_FORMAT = (count) => `
3888
+
3889
+ Output format (strict):
3890
+ - Exactly ${count} lines.
3891
+ - One suggestion per line.
3892
+ - No numbering, bullets, markdown, emojis, code blocks, or quotes around the suggestion.
3893
+ - No preamble, headings, summary, or commentary before or after the list.`;
3894
+ var SUGGESTIONS_SYSTEM_PROMPT = "You generate suggested prompts that a user could send to an AI assistant. You never continue the conversation or roleplay as the assistant.";
3895
+ function buildSuggestionsPrompt(ctx, history, prompt2, count, allAgentInfo) {
3896
+ const capabilitiesBlock = capabilities(ctx, allAgentInfo);
3897
+ const guidanceBlock = prompt2 ? `
3898
+
3899
+ Additional guidance: "${prompt2}"` : "";
3900
+ if (history.length === 0) {
3901
+ return `Generate exactly ${count} brief starter prompts to show a user what an AI assistant can do. Phrase each one as the user would write it. Keep them short and generic \u2014 do not invent specific names, IDs, or data (e.g. prefer "Search for a trade" over "Find all trades with Client A").` + capabilitiesBlock + guidanceBlock + OUTPUT_FORMAT(count);
3902
+ }
3903
+ const conversationContext = history.filter(
3904
+ (m) => (m.role === "user" || m.role === "assistant") && !m.toolCalls?.length && !m.thinking && !!m.content?.trim()
3905
+ ).slice(-HISTORY_WINDOW).map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
3906
+ return `<conversation_history>
3907
+ ${conversationContext}
3908
+ </conversation_history>
3909
+
3910
+ The conversation above has ended. Do not continue it or respond as the assistant. Generate exactly ${count} follow-up prompts the *user* might send next, phrased in first person as the user would write them. The first ${Math.max(0, count - 1)} should be natural follow-ups to the last turn. Do not invent specific names, IDs, or data values that do not appear in <conversation_history>.` + capabilitiesBlock + guidanceBlock + OUTPUT_FORMAT(count);
3911
+ }
3912
+ function parseSuggestions(text, count) {
3913
+ return text.split("\n").map((s) => s.trim()).map((s) => s.replace(/^([-*•]|\d+[.)])\s+/, "")).map((s) => s.replace(/^[*_]{1,3}|[*_]{1,3}$/g, "").trim()).filter((s) => s.length > 0).filter((s) => !/^[-=]{2,}$/.test(s)).filter((s) => !/^here (are|is) /i.test(s)).filter((s) => !s.endsWith(":")).slice(0, count);
3914
+ }
3915
+ async function generateSuggestions(ctx, history, prompt2, count, allAgentInfo) {
3916
+ const defaultProvider = ctx.registry.default();
3917
+ if (!defaultProvider.prompt) {
3918
+ logger2.warn("ChatDriver: default AI provider does not implement prompt()");
3919
+ return [];
3920
+ }
3921
+ const userMessage = buildSuggestionsPrompt(ctx, history, prompt2, count, allAgentInfo);
3922
+ const text = await defaultProvider.prompt(userMessage, {
3923
+ systemPrompt: SUGGESTIONS_SYSTEM_PROMPT
3924
+ });
3925
+ return parseSuggestions(text, count);
3926
+ }
3927
+
3857
3928
  // src/utils/context-tokens.ts
3858
3929
  var APPROX_CHARS_PER_TOKEN = 4;
3859
3930
  var IMAGE_TOKEN_ESTIMATE = 1600;
@@ -4327,6 +4398,12 @@ The above summarizes earlier parts of this conversation that were compacted. Con
4327
4398
  });
4328
4399
  }
4329
4400
 
4401
+ // src/utils/resolve-turn-input.ts
4402
+ async function resolveTurnInput(input, ctx) {
4403
+ if (input === void 0) return void 0;
4404
+ return typeof input === "function" ? input(ctx) : input;
4405
+ }
4406
+
4330
4407
  // src/utils/sum-usage.ts
4331
4408
  function emptyUsage() {
4332
4409
  return {
@@ -4397,9 +4474,358 @@ function accumulate(messages, into) {
4397
4474
  }
4398
4475
  }
4399
4476
 
4477
+ // src/components/agent-session/agent-session.ts
4478
+ var WARN_PREFIX = "AgentSession";
4479
+ var AgentSession = class {
4480
+ constructor(deps) {
4481
+ this.deps = deps;
4482
+ /**
4483
+ * Aborted on disposal. Threaded into `AgentLifecycleContext.signal` so long-running
4484
+ * `onActivate` work can bail if the session disconnects.
4485
+ */
4486
+ this.lifecycleAbortController = new AbortController();
4487
+ /**
4488
+ * Sticky user pick from the picker (or the host's `setAgent` API). Only changes on explicit
4489
+ * user action. Survives flow completion: when a stateful agent releases, the user-pin stays put
4490
+ * and routing falls back to it on the next turn.
4491
+ */
4492
+ this.pinned = null;
4493
+ /**
4494
+ * Transient flow lock — set when a stateful agent activates, cleared on release. While
4495
+ * non-null, routing precedence goes to this agent and the picker is locked. Independent of
4496
+ * the user pin so completing a flow doesn't wipe the user's sticky choice.
4497
+ */
4498
+ this.flowOwner = null;
4499
+ /**
4500
+ * Per-agent restored state for a rehydrated session, keyed by agent name. Primed by the host on
4501
+ * restore; each entry is passed to that agent's `onActivate` as `ctx.restored` on its next
4502
+ * activation, then deleted so a later re-activation starts fresh.
4503
+ */
4504
+ this.restoredStates = {};
4505
+ }
4506
+ /** The active agent, or `undefined` before the first activation and after a release. */
4507
+ getActive() {
4508
+ return this.active;
4509
+ }
4510
+ /** The sticky user pin, or `null` for automatic routing. */
4511
+ getPinned() {
4512
+ return this.pinned;
4513
+ }
4514
+ /** The transient flow lock, or `null` when no flow holds routing. */
4515
+ getFlowOwner() {
4516
+ return this.flowOwner;
4517
+ }
4518
+ /**
4519
+ * The name routing is locked to, flow lock first. A flow in progress outranks the user's
4520
+ * sticky pick for the duration of the flow; neither being set means automatic routing.
4521
+ */
4522
+ getLockedName() {
4523
+ return this.flowOwner ?? this.pinned;
4524
+ }
4525
+ /** Set the sticky user pin. `null` returns to automatic routing. */
4526
+ setPinned(name) {
4527
+ this.pinned = name;
4528
+ }
4529
+ /**
4530
+ * Set the flow-owner lock directly — used to restore state on driver reconstruction
4531
+ * (agents-array changes, pop-in/pop-out), not to steer routing. Writes without firing hooks or
4532
+ * reporting a transition, because the flow it names is already running.
4533
+ */
4534
+ setFlowOwner(name) {
4535
+ this.flowOwner = name;
4536
+ }
4537
+ /** Prime per-agent restored state for a rehydrated session. Replaces any pending entries. */
4538
+ primeRestoredStates(snapshots) {
4539
+ this.restoredStates = { ...snapshots };
4540
+ }
4541
+ /** Whether a restored snapshot is waiting for this agent's next activation. */
4542
+ hasRestoredState(agentName) {
4543
+ return this.restoredStates[agentName] !== void 0;
4544
+ }
4545
+ /**
4546
+ * Make `agent` active, firing the lifecycle hooks around the swap: outgoing `onDeactivate`
4547
+ * first, then incoming `onActivate`. Both are awaited so heavy activation work (a machine
4548
+ * restore, say) completes before the agent's first turn runs, and a throw from either is logged
4549
+ * rather than propagated — an agent whose hook fails still activates.
4550
+ *
4551
+ * Re-activating the same agent by name is not a switch: no hooks fire, no lock moves, and no
4552
+ * handoff is recorded. That guard matters because a per-turn re-resolve calls through here.
4553
+ */
4554
+ async activate(agent) {
4555
+ const previousAgent = this.active;
4556
+ const switched = !previousAgent || previousAgent.name !== agent.name;
4557
+ const transition = { previousAgent, switched };
4558
+ if (switched) {
4559
+ recordMetaEvent(this.deps.sessionKey(), "agent.handoff", {
4560
+ from: previousAgent?.name ?? null,
4561
+ to: agent.name,
4562
+ // GENC-1351: was a restored snapshot waiting for this agent at activation?
4563
+ restored: this.hasRestoredState(agent.name)
4564
+ });
4565
+ }
4566
+ if (switched && previousAgent) await this.fireDeactivate(previousAgent, "switch");
4567
+ if (switched && agent.onActivate) {
4568
+ const restored = this.restoredStates[agent.name];
4569
+ if (restored !== void 0) delete this.restoredStates[agent.name];
4570
+ try {
4571
+ await agent.onActivate({
4572
+ agentName: agent.name,
4573
+ sessionKey: this.deps.sessionKey(),
4574
+ previousAgentName: previousAgent?.name,
4575
+ signal: this.lifecycleAbortController.signal,
4576
+ restored
4577
+ });
4578
+ } catch (e) {
4579
+ logger2.warn(`${WARN_PREFIX}: onActivate("${agent.name}") threw:`, e);
4580
+ }
4581
+ }
4582
+ const hasLifecycleHooks = !!(agent.onActivate || agent.onDeactivate);
4583
+ if (switched && hasLifecycleHooks && this.flowOwner !== agent.name) {
4584
+ this.flowOwner = agent.name;
4585
+ transition.flowOwnerChangedTo = agent.name;
4586
+ }
4587
+ this.active = agent;
4588
+ return transition;
4589
+ }
4590
+ /**
4591
+ * Release the active agent: fire `onDeactivate` and drop the flow lock.
4592
+ *
4593
+ * The user pin is intentionally **not** cleared — someone who explicitly picked this agent
4594
+ * should stay on it after the flow ends, so the next message starts a fresh flow on the same
4595
+ * agent rather than routing through the classifier.
4596
+ *
4597
+ * Returns `undefined` when there was no active agent, so a caller can skip its events.
4598
+ */
4599
+ async release() {
4600
+ const agent = this.active;
4601
+ if (!agent) return void 0;
4602
+ await this.fireDeactivate(agent, "release");
4603
+ this.active = void 0;
4604
+ const transition = { previousAgent: agent, switched: true };
4605
+ if (this.flowOwner !== null) {
4606
+ this.flowOwner = null;
4607
+ transition.flowOwnerChangedTo = null;
4608
+ }
4609
+ return transition;
4610
+ }
4611
+ /**
4612
+ * Fire `onDeactivate` on the active agent and abort pending lifecycle work, for session
4613
+ * teardown, so machines release resources cleanly. The session is not reusable afterwards: the
4614
+ * abort signal stays aborted, so a later `activate` would hand its hook a dead signal.
4615
+ *
4616
+ * `onDeactivate` runs **before** the abort, deliberately: teardown that needs to flush state
4617
+ * would otherwise be handed an already-aborted signal and have to skip its own work. The cost is
4618
+ * that an `onActivate` still in flight keeps running until `onDeactivate` finishes — both hold
4619
+ * the same signal. Acceptable because the two are sequenced by `activate`, so overlap only
4620
+ * happens if a hook resolves before the work it started; a hook that must stop immediately
4621
+ * should own its own cancellation.
4622
+ */
4623
+ async dispose() {
4624
+ const agent = this.active;
4625
+ if (agent) await this.fireDeactivate(agent, "dispose");
4626
+ this.lifecycleAbortController.abort();
4627
+ this.active = void 0;
4628
+ }
4629
+ /**
4630
+ * The one place `onDeactivate` is invoked. `reason` only reaches the warning text, which is what
4631
+ * tells a log reader which path fired.
4632
+ */
4633
+ async fireDeactivate(agent, reason) {
4634
+ if (!agent.onDeactivate) return;
4635
+ const label = reason === "switch" ? "" : `${reason} `;
4636
+ try {
4637
+ await agent.onDeactivate({
4638
+ agentName: agent.name,
4639
+ sessionKey: this.deps.sessionKey(),
4640
+ signal: this.lifecycleAbortController.signal
4641
+ });
4642
+ } catch (e) {
4643
+ logger2.warn(`${WARN_PREFIX}: ${label}onDeactivate("${agent.name}") threw:`, e);
4644
+ }
4645
+ }
4646
+ };
4647
+
4648
+ // src/components/chat-driver/chat-driver.constants.ts
4649
+ var DEFAULT_MAX_TOOL_ITERATIONS = 50;
4650
+ var DEFAULT_MAX_FOLD_OPERATIONS = 5;
4651
+ var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
4652
+ var DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
4653
+ var MAX_STALE_TOOL_CALLS = DEFAULT_MAX_UNKNOWN_TOOL_CALLS * 2;
4654
+ var MAX_MALFORMED_RETRIES = 5;
4655
+ var MAX_EMPTY_RESPONSE_RETRIES = 3;
4656
+ var MALFORMED_CALL_NUDGE = "IMPORTANT: Use only the structured function-call API to invoke tools. Do not write Python code or use Python-style syntax to call tools.";
4657
+ var EMPTY_RESPONSE_NUDGE = "IMPORTANT: Your previous attempt returned nothing at all, which is never a valid response. Answer the user's last message now, in prose \u2014 even if all you can honestly say is that you cannot help with it and why. If a tool is the right way to answer, call it. Do not return an empty response.";
4658
+ var MAX_SETUP_TRANSPORT_RETRIES = 3;
4659
+ var HARVESTED_SUBAGENT_EVENTS = /* @__PURE__ */ new Set([
4660
+ "turn.retry",
4661
+ "turn.error",
4662
+ "tool.failed",
4663
+ "tool.unresolved"
4664
+ ]);
4665
+ var REQUEST_CONTINUATION_TOOL = "request_continuation";
4666
+ var HANDOFF_TOOL_RESULT_PLACEHOLDER = "Handoff to another specialist \u2014 routing continues on the next turn.";
4667
+
4400
4668
  // src/utils/tool-fold.ts
4401
4669
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
4402
4670
 
4671
+ // src/components/chat-driver/fold-controller.ts
4672
+ var foldOf = (handler) => handler ? handler[TOOL_FOLD_SYMBOL] : void 0;
4673
+ var FoldController = class {
4674
+ constructor(deps) {
4675
+ this.deps = deps;
4676
+ this.stack = [];
4677
+ /**
4678
+ * Consecutive fold opens/closes with no tool call between them. A model that only opens and
4679
+ * closes is making no progress, and the loop breaks out past a ceiling.
4680
+ */
4681
+ this.consecutiveOps = 0;
4682
+ }
4683
+ /** Drop all fold state. Called on an agent swap — each agent starts outside every fold. */
4684
+ reset() {
4685
+ this.stack = [];
4686
+ this.consecutiveOps = 0;
4687
+ }
4688
+ /** Names of the currently open folds, outermost first. */
4689
+ activeNames() {
4690
+ return this.stack.map((f) => f.foldName);
4691
+ }
4692
+ get depth() {
4693
+ return this.stack.length;
4694
+ }
4695
+ /** The innermost open fold's name, or `undefined` when none is open. */
4696
+ get topName() {
4697
+ return this.stack[this.stack.length - 1]?.foldName;
4698
+ }
4699
+ /** Count one fold operation and return the new consecutive total. */
4700
+ noteOp() {
4701
+ this.consecutiveOps += 1;
4702
+ return this.consecutiveOps;
4703
+ }
4704
+ /** Real work happened, so the no-progress streak restarts. */
4705
+ clearOps() {
4706
+ this.consecutiveOps = 0;
4707
+ }
4708
+ /** Fold metadata for a tool name, or `undefined` if it is not a fold facade. */
4709
+ foldFor(toolName) {
4710
+ return foldOf(this.deps.getHandlers()[toolName]);
4711
+ }
4712
+ /**
4713
+ * Which fold to **open** to reach `toolName`, or `null` if no registered fold contains it.
4714
+ *
4715
+ * Returns the outermost *registered* ancestor, not the innermost fold holding the tool — the one
4716
+ * consumer uses the answer to tell the model what to call next, and a nested fold cannot be
4717
+ * opened before its parent. The recursion therefore only descends through closed folds: once a
4718
+ * fold is open its children are top-level in the live handler map and match directly.
4719
+ */
4720
+ findContaining(toolName, handlers) {
4721
+ const source = handlers ?? this.deps.getHandlers();
4722
+ for (const handler of Object.values(source)) {
4723
+ const fold = foldOf(handler);
4724
+ if (!fold) continue;
4725
+ if (fold.handlers[toolName]) return fold.name;
4726
+ if (this.findContaining(toolName, fold.handlers)) return fold.name;
4727
+ }
4728
+ return null;
4729
+ }
4730
+ /**
4731
+ * If an open fold is hiding a previously-available tool, the fold to close to start getting it
4732
+ * back, else `null`.
4733
+ *
4734
+ * Only exclusive folds hide tools — they replace the tool set on open rather than extending it —
4735
+ * so a base tool that was visible beforehand sits in a frame's `previousHandlers` but not in the
4736
+ * live map. Only the top fold's `close_` tool is active, so that is always the actionable step:
4737
+ * closing repeatedly walks back to a tool further down the stack.
4738
+ */
4739
+ hidingTool(toolName) {
4740
+ if (this.stack.length === 0) return null;
4741
+ const hidden = this.stack.some((f) => f.previousHandlers[toolName]);
4742
+ return hidden ? this.topName : null;
4743
+ }
4744
+ /** Open a fold: push a frame, swap the tool set, and return the response for the model. */
4745
+ open(foldName, fold, args, activeToolCallId) {
4746
+ for (const key of Object.keys(args)) {
4747
+ const innerHandler = fold.handlers[key];
4748
+ if (innerHandler) {
4749
+ logger2.debug(`ChatDriver: fold shortcut dispatch "${foldName}" \u2192 "${key}"`);
4750
+ this.pushFrame(foldName);
4751
+ this.applyToolSet(fold, foldName);
4752
+ this.clearOps();
4753
+ const innerArgs = typeof args[key] === "object" && args[key] !== null ? args[key] : {};
4754
+ return innerHandler(innerArgs, this.deps.handlerContext(activeToolCallId)).then(
4755
+ (r) => typeof r === "string" ? r : JSON.stringify(r)
4756
+ );
4757
+ }
4758
+ }
4759
+ this.pushFrame(foldName);
4760
+ this.applyToolSet(fold, foldName);
4761
+ const closeToolName = `close_${foldName}`;
4762
+ const innerToolNames = fold.tools.map((t) => t.name);
4763
+ let message = `Fold opened: ${foldName}. Tools now available: ${[...innerToolNames, closeToolName].join(", ")}.`;
4764
+ if (fold.usageNotes) message += ` Notes: ${fold.usageNotes}`;
4765
+ message += ` Call ${closeToolName} when done to return to the previous tools.`;
4766
+ return Promise.resolve(message);
4767
+ }
4768
+ /** Close the top fold: pop the frame, restore the tool set it captured. */
4769
+ close() {
4770
+ const frame = this.stack.pop();
4771
+ if (!frame) return "No fold is currently open.";
4772
+ this.deps.setDefinitions(frame.previousDefinitions);
4773
+ this.deps.setHandlers(frame.previousHandlers);
4774
+ const toolNames = frame.previousDefinitions.map((t) => t.name);
4775
+ return `Fold closed: ${frame.foldName}. Tools now available: ${toolNames.join(", ")}.`;
4776
+ }
4777
+ /** The fold-awareness suffix appended to the system prompt on each LLM call. */
4778
+ systemPromptSuffix() {
4779
+ const activeFolds = Object.values(this.deps.getHandlers()).map(foldOf).filter((f) => !!f);
4780
+ if (activeFolds.length === 0 && this.stack.length === 0) return "";
4781
+ const parts = [
4782
+ "\n\n--- Tool Folds ---",
4783
+ "Some tools are grouped into folds. You may see tool calls in the conversation history for tools that are not currently available \u2014 they are inside a fold. To access them, invoke the fold tool first."
4784
+ ];
4785
+ if (this.topName) {
4786
+ parts.push(
4787
+ `You are currently inside the "${this.topName}" fold. Call close_${this.topName} when you are done with these tools.`
4788
+ );
4789
+ }
4790
+ for (const fold of activeFolds) parts.push(`\u2022 ${fold.name}: ${fold.description}`);
4791
+ return parts.join("\n");
4792
+ }
4793
+ pushFrame(foldName) {
4794
+ this.stack.push({
4795
+ foldName,
4796
+ previousDefinitions: [...this.deps.getDefinitions()],
4797
+ previousHandlers: { ...this.deps.getHandlers() }
4798
+ });
4799
+ }
4800
+ /**
4801
+ * Install the fold's inner tool set, replacing it (exclusive) or extending it (non-exclusive),
4802
+ * and inject the close tool. Does not touch the stack.
4803
+ */
4804
+ applyToolSet(fold, foldName) {
4805
+ const closeToolName = `close_${foldName}`;
4806
+ const newDefs = [];
4807
+ const newHandlers = {};
4808
+ if (!fold.exclusive) {
4809
+ for (const def of this.deps.getDefinitions()) {
4810
+ if (def.name !== foldName) newDefs.push(def);
4811
+ }
4812
+ for (const [name, handler] of Object.entries(this.deps.getHandlers())) {
4813
+ if (name !== foldName) newHandlers[name] = handler;
4814
+ }
4815
+ }
4816
+ newDefs.push(...fold.tools);
4817
+ Object.assign(newHandlers, fold.handlers);
4818
+ newDefs.push({
4819
+ name: closeToolName,
4820
+ description: `Close the ${foldName} fold and return to the previous set of tools.`,
4821
+ parameters: { type: "object", properties: {} }
4822
+ });
4823
+ newHandlers[closeToolName] = async () => this.close();
4824
+ this.deps.setDefinitions(newDefs);
4825
+ this.deps.setHandlers(newHandlers);
4826
+ }
4827
+ };
4828
+
4403
4829
  // src/components/chat-driver/turn-failures.ts
4404
4830
  var TURN_FAILURE_EXITS = {
4405
4831
  contextGuard: {
@@ -4471,6 +4897,15 @@ var TURN_FAILURE_EXITS = {
4471
4897
  // `TurnFailureReason` and cost the literal types that let `turn-failures.test.ts` pin the
4472
4898
  // two deliberate divergences at compile time instead of at runtime.
4473
4899
  };
4900
+ function turnDone(failureReason, budget, providerRefused) {
4901
+ if (!failureReason) return { reason: "done" };
4902
+ return {
4903
+ reason: "done",
4904
+ failureReason,
4905
+ ...budget ? { budget } : {},
4906
+ ...providerRefused ? { providerRefused } : {}
4907
+ };
4908
+ }
4474
4909
 
4475
4910
  // src/components/chat-driver/chat-driver.ts
4476
4911
  var toolResultImages = (result) => {
@@ -4501,25 +4936,6 @@ var budgetDetailOf = (e) => {
4501
4936
  ...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
4502
4937
  };
4503
4938
  };
4504
- var DEFAULT_MAX_TOOL_ITERATIONS = 50;
4505
- var DEFAULT_MAX_FOLD_OPERATIONS = 5;
4506
- var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
4507
- var DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
4508
- var MAX_STALE_TOOL_CALLS = DEFAULT_MAX_UNKNOWN_TOOL_CALLS * 2;
4509
- var MAX_MALFORMED_RETRIES = 5;
4510
- var MAX_EMPTY_RESPONSE_RETRIES = 3;
4511
- var MALFORMED_CALL_NUDGE = "IMPORTANT: Use only the structured function-call API to invoke tools. Do not write Python code or use Python-style syntax to call tools.";
4512
- var EMPTY_RESPONSE_NUDGE = "IMPORTANT: Your previous attempt returned nothing at all, which is never a valid response. Answer the user's last message now, in prose \u2014 even if all you can honestly say is that you cannot help with it and why. If a tool is the right way to answer, call it. Do not return an empty response.";
4513
- var MAX_SETUP_TRANSPORT_RETRIES = 3;
4514
- var SUGGESTIONS_HISTORY_WINDOW = 8;
4515
- var HARVESTED_SUBAGENT_EVENTS = /* @__PURE__ */ new Set([
4516
- "turn.retry",
4517
- "turn.error",
4518
- "tool.failed",
4519
- "tool.unresolved"
4520
- ]);
4521
- var REQUEST_CONTINUATION_TOOL = "request_continuation";
4522
- var HANDOFF_TOOL_RESULT_PLACEHOLDER = "Handoff to another specialist \u2014 routing continues on the next turn.";
4523
4939
  var ChatDriver = class _ChatDriver extends EventTarget {
4524
4940
  constructor(providerRegistry, config = {}) {
4525
4941
  super();
@@ -4582,10 +4998,18 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4582
4998
  * (`callPhaseEpoch < currentPhaseEpoch`), with an `agentEnd` backstop.
4583
4999
  */
4584
5000
  this.currentPhaseEpoch = 0;
4585
- /** Stack of fold frames — grows when a fold opens, shrinks when it closes. */
4586
- this.foldStack = [];
4587
- /** Consecutive fold open/close ops without a real tool call. Reset on real tool execution. */
4588
- this.consecutiveFoldOps = 0;
5001
+ /** Tool folds: the open-fold stack, the no-progress counter, and the tool-surface swap. */
5002
+ this.folds = new FoldController({
5003
+ getDefinitions: () => this.toolDefinitions,
5004
+ setDefinitions: (defs) => {
5005
+ this.toolDefinitions = defs;
5006
+ },
5007
+ getHandlers: () => this.toolHandlers,
5008
+ setHandlers: (handlers) => {
5009
+ this.toolHandlers = handlers;
5010
+ },
5011
+ handlerContext: (activeToolCallId) => this.buildHandlerContext(activeToolCallId)
5012
+ });
4589
5013
  /** Consecutive unknown-tool calls without a real tool call. Reset on real tool execution. */
4590
5014
  this.consecutiveUnknownToolCalls = 0;
4591
5015
  /**
@@ -4685,6 +5109,19 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4685
5109
  * picked up on the next turn.
4686
5110
  */
4687
5111
  this.resolvedStatusCache = /* @__PURE__ */ new Map();
5112
+ /**
5113
+ * The stateful-agent contract for this driver: active agent, user pin, flow lock, restored
5114
+ * state, and the `onActivate` / `onDeactivate` hooks (§7.1).
5115
+ *
5116
+ * **Public because `OrchestratingDriver` reaches through to it.** The orchestrator decorates a
5117
+ * config before installing it (the cross-agent continuation tool), so it cannot route through
5118
+ * {@link ChatDriver.activateAgent} and must drive the session itself — and one shared session is
5119
+ * the point: two would let `getActiveAgent()` disagree between the outer and inner driver.
5120
+ * `@internal` rather than private so that reach is legal without six delegating one-liners.
5121
+ *
5122
+ * @internal
5123
+ */
5124
+ this.agentSession = new AgentSession({ sessionKey: () => this.sessionKey });
4688
5125
  /**
4689
5126
  * A provider refusal was observed this turn, so the tool loop must not call the model again
4690
5127
  * (GENC-1506).
@@ -4721,6 +5158,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4721
5158
  condenseBatchCalls = 1,
4722
5159
  maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
4723
5160
  sessionKey = "",
5161
+ deferAgentRelease = false,
4724
5162
  activityBus = NOOP_ACTIVITY_BUS,
4725
5163
  budgetExhaustedMessage,
4726
5164
  providerRefusedMessage
@@ -4728,6 +5166,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4728
5166
  this.maxToolIterations = maxToolIterations;
4729
5167
  this.condenseBatchCalls = condenseBatchCalls;
4730
5168
  this.sessionKey = sessionKey;
5169
+ this.deferAgentRelease = deferAgentRelease;
4731
5170
  this.activityBus = activityBus;
4732
5171
  this.budgetExhaustedMessageOverride = budgetExhaustedMessage;
4733
5172
  this.providerRefusedMessageOverride = providerRefusedMessage;
@@ -4775,12 +5214,13 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4775
5214
  * interaction is instead settled by `resolveInteraction` (user response or
4776
5215
  * timeout), which also stamps history and reverts the chat-input override.
4777
5216
  */
4778
- dispose() {
5217
+ async dispose() {
4779
5218
  this.lifecycleController.abort(new DOMException("AI assistant driver disposed", "AbortError"));
4780
5219
  for (const handle of this.interactionContexts.values()) handle.dispose();
4781
5220
  this.interactionContexts.clear();
4782
5221
  this.unsubscribeRegistry?.();
4783
5222
  this.unsubscribeRegistry = void 0;
5223
+ await this.agentSession.dispose();
4784
5224
  }
4785
5225
  /**
4786
5226
  * Stop the current turn (user "stop" button). Aborts the in-flight provider
@@ -4829,23 +5269,6 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4829
5269
  }
4830
5270
  return { reason: "done" };
4831
5271
  }
4832
- /**
4833
- * Build the `done` loop result, carrying the typed failure reason when the turn
4834
- * bailed (PTC-0). The discriminant stays `'done'` either way — the same value a
4835
- * clean turn returns — so consumers matching on `reason === 'done'` are unchanged;
4836
- * `failureReason` is simply present on a failure and absent on success. Omitted
4837
- * (rather than set to `undefined`) so a happy-path result stays byte-identical to
4838
- * the historical `{ reason: 'done' }`.
4839
- */
4840
- turnDone(failureReason, budget, providerRefused) {
4841
- if (!failureReason) return { reason: "done" };
4842
- return {
4843
- reason: "done",
4844
- failureReason,
4845
- ...budget ? { budget } : {},
4846
- ...providerRefused ? { providerRefused } : {}
4847
- };
4848
- }
4849
5272
  /**
4850
5273
  * End the turn on a terminal failure: fail upward when this driver is a sub-agent, speak
4851
5274
  * to the user when it is not, and hand the reason to the caller either way.
@@ -4871,7 +5294,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4871
5294
  } else {
4872
5295
  this.appendDriverNotice(overrides?.copy ?? exit.copy);
4873
5296
  }
4874
- return this.turnDone(exit.resultReason, overrides?.budget, overrides?.providerRefused);
5297
+ return turnDone(exit.resultReason, overrides?.budget, overrides?.providerRefused);
4875
5298
  }
4876
5299
  /**
4877
5300
  * Terminal budget outcome for a wall hit **outside** the tool loop — today,
@@ -4924,7 +5347,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4924
5347
  isSubAgent: this.isSubAgent
4925
5348
  });
4926
5349
  this.appendDriverNotice(this.budgetExhaustedBubble(e));
4927
- return this.turnDone("budget-exhausted", budgetDetailOf(e));
5350
+ return turnDone("budget-exhausted", budgetDetailOf(e));
4928
5351
  }
4929
5352
  /**
4930
5353
  * The sentence shown when the upstream provider refuses (GENC-1506).
@@ -4969,8 +5392,66 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4969
5392
  };
4970
5393
  }
4971
5394
  /**
4972
- * Swap in a new agent's configuration. Called by OrchestratingDriver before
4973
- * each specialist turn so the shared driver runs with the right tools and prompt.
5395
+ * Make `config` the active agent, firing its lifecycle hooks — `onDeactivate` on the outgoing
5396
+ * agent, then `onActivate` on this one — before installing it.
5397
+ *
5398
+ * This is the entry point a host should use. {@link ChatDriver.applyAgent} installs a config
5399
+ * *without* firing hooks, which is right for the two callers that need it (the orchestrator,
5400
+ * which fires them through the shared session, and `invokeSubAgent`, where a child's config is
5401
+ * not an activation) and wrong for everyone else.
5402
+ *
5403
+ * @beta
5404
+ */
5405
+ async activateAgent(config) {
5406
+ await this.agentSession.activate(config);
5407
+ this.applyAgent(config);
5408
+ }
5409
+ /**
5410
+ * {@inheritDoc AiDriver.getActiveAgent}
5411
+ *
5412
+ * @beta
5413
+ */
5414
+ getActiveAgent() {
5415
+ return this.agentSession.getActive();
5416
+ }
5417
+ /** {@inheritDoc AiDriver.primeRestoredAgentState} */
5418
+ primeRestoredAgentState(snapshots) {
5419
+ this.agentSession.primeRestoredStates(snapshots);
5420
+ }
5421
+ /**
5422
+ * Record the sticky agent pin. See {@link AiDriver.setPinnedAgent} for the contract.
5423
+ *
5424
+ * Recorded, not acted on: this driver has one agent and nothing to route between, so a pin
5425
+ * changes no behaviour here. It is still honoured as state, because the session outlives the
5426
+ * question of which driver is holding it — a host that pins before an orchestrator is built
5427
+ * would otherwise lose the pick.
5428
+ *
5429
+ * @beta
5430
+ */
5431
+ setPinnedAgent(name) {
5432
+ this.agentSession.setPinned(name);
5433
+ }
5434
+ /**
5435
+ * Record the transient flow lock. See {@link AiDriver.setFlowOwner} for the contract.
5436
+ *
5437
+ * Recorded, not acted on, for the same reason as {@link ChatDriver.setPinnedAgent}.
5438
+ *
5439
+ * @beta
5440
+ */
5441
+ setFlowOwner(name) {
5442
+ this.agentSession.setFlowOwner(name);
5443
+ }
5444
+ /**
5445
+ * Install `config` as the active agent **without firing lifecycle hooks**.
5446
+ *
5447
+ * Prefer {@link ChatDriver.activateAgent} unless you are deliberately skipping the hooks. Two
5448
+ * callers are: `OrchestratingDriver`, which fires them through the shared
5449
+ * {@link ChatDriver.agentSession} and then installs a decorated config, and `invokeSubAgent`,
5450
+ * where the child driver's config is a tool surface rather than an activation.
5451
+ *
5452
+ * Renamed to `installAgent` in a follow-up (GENC-1580) — see `docs/migration-GENC-1580.md`.
5453
+ * Until then the name says less than it should, so do not add a third caller to this path
5454
+ * without checking which of the two above it resembles.
4974
5455
  */
4975
5456
  applyAgent(config) {
4976
5457
  if (config.name !== this.activeAgentName) this.currentActivation += 1;
@@ -5009,8 +5490,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5009
5490
  if (typeof config.provider === "string") {
5010
5491
  this.resolveProviderByName(config.provider, config.name);
5011
5492
  }
5012
- this.foldStack = [];
5013
- this.consecutiveFoldOps = 0;
5493
+ this.folds.reset();
5014
5494
  this.everSeenToolNames.clear();
5015
5495
  }
5016
5496
  /**
@@ -5127,15 +5607,6 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5127
5607
  this.resolvedStatusCache.set(name, status);
5128
5608
  return status;
5129
5609
  }
5130
- /**
5131
- * Resolve a per-turn config input that is either a static value or a function
5132
- * of the turn context — the value-or-resolver shape shared by `provider`,
5133
- * `temperature`, and `toolChoice`. Returns undefined when the input is unset.
5134
- */
5135
- async resolveTurnInput(input, ctx) {
5136
- if (input === void 0) return void 0;
5137
- return typeof input === "function" ? input(ctx) : input;
5138
- }
5139
5610
  /**
5140
5611
  * Returns the early-stop result set by `completeSubAgent`, if any.
5141
5612
  * Called by a parent `ChatDriver` after running this instance as a sub-agent.
@@ -5182,6 +5653,19 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5182
5653
  getAgentReleaseRequested() {
5183
5654
  return this.agentReleaseRequested;
5184
5655
  }
5656
+ /**
5657
+ * Act on a `releaseAgent` call from a tool handler: fire `onDeactivate` and drop the flow lock,
5658
+ * so the next turn starts clean and the persister does not serialize a flow that is over.
5659
+ *
5660
+ * No-op when the owner handles release ({@link ChatDriverConfig.deferAgentRelease}). The flag is
5661
+ * cleared here rather than only at the next `sendMessage`, so the signal cannot be acted on twice
5662
+ * and release a later agent that never asked.
5663
+ */
5664
+ async releaseIfRequested() {
5665
+ if (this.deferAgentRelease || !this.agentReleaseRequested) return;
5666
+ this.agentReleaseRequested = false;
5667
+ await this.agentSession.release();
5668
+ }
5185
5669
  /**
5186
5670
  * Return the per-turn snapshots captured so far. Used by the host's debug
5187
5671
  * log exporter to show what the LLM saw on each turn — system prompt, tool
@@ -5329,7 +5813,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5329
5813
  }
5330
5814
  /** Returns the current fold stack names for debugging. */
5331
5815
  getActiveFoldNames() {
5332
- return this.foldStack.map((f) => f.foldName);
5816
+ return this.folds.activeNames();
5333
5817
  }
5334
5818
  /**
5335
5819
  * The transcript as it should be **stored**: every tool payload the model has already stopped
@@ -5351,65 +5835,23 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5351
5835
  getHistoryForPersistence() {
5352
5836
  return condenseForPersistence([...this.history], this.condensePolicies);
5353
5837
  }
5838
+ /**
5839
+ * {@inheritDoc AiDriver.getSuggestions}
5840
+ *
5841
+ * @beta
5842
+ */
5354
5843
  async getSuggestions(history, prompt2, count, allAgentInfo) {
5355
- const defaultProvider = this.providerRegistry.default();
5356
- if (!defaultProvider.prompt) {
5357
- logger2.warn("ChatDriver: default AI provider does not implement prompt()");
5358
- return [];
5359
- }
5360
- let agentContext = "";
5361
- let toolContext = "";
5362
- if (allAgentInfo?.length) {
5363
- const agentDescriptions = allAgentInfo.map((agent) => {
5364
- const tools = agent.tools.map((t) => t.name).join(", ");
5365
- return `- ${agent.name} (${agent.description}): ${tools ? `(Tools: ${tools})` : "No tools"}`;
5366
- }).join("\n");
5367
- agentContext = `The assistant has the following capabilities:
5368
- ${agentDescriptions}`;
5369
- const allToolNames = allAgentInfo.flatMap((agent) => agent.tools.map((t) => t.name)).filter((value, index, self) => self.indexOf(value) === index).join(", ");
5370
- toolContext = allToolNames ? `You have access to the following tools across all agents: ${allToolNames}.` : "";
5371
- } else if (this.activeAgentName) {
5372
- const toolNames = this.toolDefinitions.map((tool) => tool.name).join(", ");
5373
- agentContext = `You are currently acting as the "${this.activeAgentName}" agent.`;
5374
- toolContext = toolNames ? `You have access to the following tools: ${toolNames}.` : "";
5375
- }
5376
- const systemPrompt = "You generate suggested prompts that a user could send to an AI assistant. You never continue the conversation or roleplay as the assistant.";
5377
- const capabilitiesBlock = agentContext || toolContext ? `
5378
-
5379
- <capabilities>
5380
- ${[agentContext, toolContext].filter(Boolean).join("\n")}
5381
- </capabilities>
5382
- Suggestions must only cover what the agent is capable of. Do not suggest anything outside these capabilities.` : "";
5383
- const guidanceBlock = prompt2 ? `
5384
-
5385
- Additional guidance: "${prompt2}"` : "";
5386
- let userMessage;
5387
- if (history.length === 0) {
5388
- userMessage = `Generate exactly ${count} brief starter prompts to show a user what an AI assistant can do. Phrase each one as the user would write it. Keep them short and generic \u2014 do not invent specific names, IDs, or data (e.g. prefer "Search for a trade" over "Find all trades with Client A").` + capabilitiesBlock + guidanceBlock + `
5389
-
5390
- Output format (strict):
5391
- - Exactly ${count} lines.
5392
- - One suggestion per line.
5393
- - No numbering, bullets, markdown, emojis, code blocks, or quotes around the suggestion.
5394
- - No preamble, headings, summary, or commentary before or after the list.`;
5395
- } else {
5396
- const conversationContext = history.filter(
5397
- (m) => (m.role === "user" || m.role === "assistant") && !m.toolCalls?.length && !m.thinking && !!m.content?.trim()
5398
- ).slice(-SUGGESTIONS_HISTORY_WINDOW).map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
5399
- userMessage = `<conversation_history>
5400
- ${conversationContext}
5401
- </conversation_history>
5402
-
5403
- The conversation above has ended. Do not continue it or respond as the assistant. Generate exactly ${count} follow-up prompts the *user* might send next, phrased in first person as the user would write them. The first ${Math.max(0, count - 1)} should be natural follow-ups to the last turn. Do not invent specific names, IDs, or data values that do not appear in <conversation_history>.` + capabilitiesBlock + guidanceBlock + `
5404
-
5405
- Output format (strict):
5406
- - Exactly ${count} lines.
5407
- - One suggestion per line.
5408
- - No numbering, bullets, markdown, emojis, code blocks, or quotes around the suggestion.
5409
- - No preamble, headings, summary, or commentary before or after the list.`;
5410
- }
5411
- const text = await defaultProvider.prompt(userMessage, { systemPrompt });
5412
- return text.split("\n").map((s) => s.trim()).map((s) => s.replace(/^([-*•]|\d+[.)])\s+/, "")).map((s) => s.replace(/^[*_]{1,3}|[*_]{1,3}$/g, "").trim()).filter((s) => s.length > 0).filter((s) => !/^[-=]{2,}$/.test(s)).filter((s) => !/^here (are|is) /i.test(s)).filter((s) => !s.endsWith(":")).slice(0, count);
5844
+ return generateSuggestions(
5845
+ {
5846
+ registry: this.providerRegistry,
5847
+ activeAgentName: this.activeAgentName,
5848
+ toolDefinitions: this.toolDefinitions
5849
+ },
5850
+ history,
5851
+ prompt2,
5852
+ count,
5853
+ allAgentInfo
5854
+ );
5413
5855
  }
5414
5856
  /**
5415
5857
  * Whether `compact()` would do anything right now — a summarizable run of older
@@ -5787,7 +6229,7 @@ Output format (strict):
5787
6229
  this.appendDriverNotice(
5788
6230
  "Sorry, something went wrong on my end. Please try again in a moment."
5789
6231
  );
5790
- result = this.turnDone("exception");
6232
+ result = turnDone("exception");
5791
6233
  return result;
5792
6234
  } finally {
5793
6235
  recordMetaEvent(this.sessionKey, "turn.end", {
@@ -5798,6 +6240,7 @@ Output format (strict):
5798
6240
  this.busy = false;
5799
6241
  this.endTurn();
5800
6242
  this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
6243
+ await this.releaseIfRequested();
5801
6244
  }
5802
6245
  }
5803
6246
  /**
@@ -6051,7 +6494,7 @@ Output format (strict):
6051
6494
  this.appendDriverNotice(
6052
6495
  "Sorry, something went wrong on my end. Please try again in a moment."
6053
6496
  );
6054
- result = this.turnDone("exception");
6497
+ result = turnDone("exception");
6055
6498
  return result;
6056
6499
  } finally {
6057
6500
  recordMetaEvent(this.sessionKey, "turn.end", {
@@ -6062,136 +6505,10 @@ Output format (strict):
6062
6505
  this.busy = false;
6063
6506
  this.endTurn();
6064
6507
  this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
6508
+ await this.releaseIfRequested();
6065
6509
  }
6066
6510
  }
6067
6511
  // ---------------------------------------------------------------------------
6068
- // Fold mechanics
6069
- // ---------------------------------------------------------------------------
6070
- /** Extract ToolFold metadata from a handler, or undefined if it isn't a fold facade. */
6071
- getFold(toolName) {
6072
- const handler = this.toolHandlers[toolName];
6073
- return handler ? handler[TOOL_FOLD_SYMBOL] : void 0;
6074
- }
6075
- /**
6076
- * Search all currently registered fold facades (and their nested folds recursively)
6077
- * to find which fold contains a given tool name. Returns the immediate parent fold name.
6078
- */
6079
- findFoldContaining(toolName, handlers) {
6080
- const source = handlers ?? this.toolHandlers;
6081
- for (const [, handler] of Object.entries(source)) {
6082
- const fold = handler[TOOL_FOLD_SYMBOL];
6083
- if (!fold) continue;
6084
- if (fold.handlers[toolName]) return fold.name;
6085
- const nested = this.findFoldContaining(toolName, fold.handlers);
6086
- if (nested) return fold.name;
6087
- }
6088
- return null;
6089
- }
6090
- /**
6091
- * If an open fold is hiding a previously-available tool, return the name of
6092
- * the fold to close to start getting it back. Only exclusive folds hide tools
6093
- * (they replace the tool set on open rather than extending it), so a base tool
6094
- * that was visible before the fold opened now sits in a fold-stack frame's
6095
- * `previousHandlers` but not in the live handler map. Only the top fold's
6096
- * `close_` tool is active, so that's always the actionable next step — even
6097
- * when the tool lives further down the stack, closing repeatedly walks back to
6098
- * it. Returns null when no open fold accounts for the tool.
6099
- */
6100
- foldHidingTool(toolName) {
6101
- if (this.foldStack.length === 0) return null;
6102
- const hidden = this.foldStack.some((f) => f.previousHandlers[toolName]);
6103
- return hidden ? this.foldStack[this.foldStack.length - 1].foldName : null;
6104
- }
6105
- /**
6106
- * Install the fold's inner tool set, replacing (exclusive) or extending (non-exclusive)
6107
- * the current tool set. Also injects the close tool. Does NOT touch the fold stack.
6108
- */
6109
- applyFoldToolSet(fold, foldName) {
6110
- const closeToolName = `close_${foldName}`;
6111
- const newDefs = [];
6112
- const newHandlers = {};
6113
- if (!fold.exclusive) {
6114
- for (const def of this.toolDefinitions) {
6115
- if (def.name !== foldName) newDefs.push(def);
6116
- }
6117
- for (const [name, handler] of Object.entries(this.toolHandlers)) {
6118
- if (name !== foldName) newHandlers[name] = handler;
6119
- }
6120
- }
6121
- newDefs.push(...fold.tools);
6122
- Object.assign(newHandlers, fold.handlers);
6123
- newDefs.push({
6124
- name: closeToolName,
6125
- description: `Close the ${foldName} fold and return to the previous set of tools.`,
6126
- parameters: { type: "object", properties: {} }
6127
- });
6128
- newHandlers[closeToolName] = async () => this.closeFold();
6129
- this.toolDefinitions = newDefs;
6130
- this.toolHandlers = newHandlers;
6131
- }
6132
- /** Open a fold: push a stack frame, swap the tool set, return the response message. */
6133
- openFold(foldName, fold, args, activeToolCallId) {
6134
- for (const key of Object.keys(args)) {
6135
- const innerHandler = fold.handlers[key];
6136
- if (innerHandler) {
6137
- logger2.debug(`ChatDriver: fold shortcut dispatch "${foldName}" \u2192 "${key}"`);
6138
- this.pushFoldFrame(foldName);
6139
- this.applyFoldToolSet(fold, foldName);
6140
- this.consecutiveFoldOps = 0;
6141
- const innerArgs = typeof args[key] === "object" && args[key] !== null ? args[key] : {};
6142
- return innerHandler(innerArgs, this.buildHandlerContext(activeToolCallId)).then(
6143
- (r) => typeof r === "string" ? r : JSON.stringify(r)
6144
- );
6145
- }
6146
- }
6147
- this.pushFoldFrame(foldName);
6148
- this.applyFoldToolSet(fold, foldName);
6149
- const innerToolNames = fold.tools.map((t) => t.name);
6150
- const closeToolName = `close_${foldName}`;
6151
- let message = `Fold opened: ${foldName}. Tools now available: ${[...innerToolNames, closeToolName].join(", ")}.`;
6152
- if (fold.usageNotes) message += ` Notes: ${fold.usageNotes}`;
6153
- message += ` Call ${closeToolName} when done to return to the previous tools.`;
6154
- return Promise.resolve(message);
6155
- }
6156
- pushFoldFrame(foldName) {
6157
- this.foldStack.push({
6158
- foldName,
6159
- previousDefinitions: [...this.toolDefinitions],
6160
- previousHandlers: { ...this.toolHandlers }
6161
- });
6162
- }
6163
- /** Close the top fold: pop the stack frame, restore the previous tool set. */
6164
- closeFold() {
6165
- const frame = this.foldStack.pop();
6166
- if (!frame) return "No fold is currently open.";
6167
- this.toolDefinitions = frame.previousDefinitions;
6168
- this.toolHandlers = frame.previousHandlers;
6169
- const toolNames = this.toolDefinitions.map((t) => t.name);
6170
- return `Fold closed: ${frame.foldName}. Tools now available: ${toolNames.join(", ")}.`;
6171
- }
6172
- /** Build the fold-awareness suffix appended to the system prompt each LLM call. */
6173
- buildFoldSystemPromptSuffix() {
6174
- const activeFolds = [];
6175
- for (const handler of Object.values(this.toolHandlers)) {
6176
- const fold = handler[TOOL_FOLD_SYMBOL];
6177
- if (fold) activeFolds.push(fold);
6178
- }
6179
- if (activeFolds.length === 0 && this.foldStack.length === 0) return "";
6180
- const parts = ["\n\n--- Tool Folds ---"];
6181
- parts.push(
6182
- "Some tools are grouped into folds. You may see tool calls in the conversation history for tools that are not currently available \u2014 they are inside a fold. To access them, invoke the fold tool first."
6183
- );
6184
- if (this.foldStack.length > 0) {
6185
- const current = this.foldStack[this.foldStack.length - 1];
6186
- parts.push(
6187
- `You are currently inside the "${current.foldName}" fold. Call close_${current.foldName} when you are done with these tools.`
6188
- );
6189
- }
6190
- for (const fold of activeFolds) {
6191
- parts.push(`\u2022 ${fold.name}: ${fold.description}`);
6192
- }
6193
- return parts.join("\n");
6194
- }
6195
6512
  // ---------------------------------------------------------------------------
6196
6513
  // Tool loop
6197
6514
  // ---------------------------------------------------------------------------
@@ -6286,7 +6603,7 @@ Output format (strict):
6286
6603
  if (typeof this.displayName === "function") {
6287
6604
  this.activeAgentLabel = await this.displayName(promptCtx);
6288
6605
  }
6289
- const foldSuffix = this.buildFoldSystemPromptSuffix();
6606
+ const foldSuffix = this.folds.systemPromptSuffix();
6290
6607
  const retryNudge = malformedAttempts > 0 ? `
6291
6608
 
6292
6609
  ${MALFORMED_CALL_NUDGE}` : emptyResponseAttempts > 0 ? `
@@ -6328,14 +6645,14 @@ ${EMPTY_RESPONSE_NUDGE}` : "";
6328
6645
  ] = (
6329
6646
  // oxlint-disable-next-line no-await-in-loop
6330
6647
  await Promise.all([
6331
- this.resolveTurnInput(this.activeTemperatureInput, promptCtx),
6332
- this.resolveTurnInput(this.activeToolChoiceInput, promptCtx),
6333
- this.resolveTurnInput(this.activeCachePolicyInput, promptCtx),
6334
- this.resolveTurnInput(this.activeTailContextInput, promptCtx),
6335
- this.resolveTurnInput(this.activeResponseSchemaInput, promptCtx),
6648
+ resolveTurnInput(this.activeTemperatureInput, promptCtx),
6649
+ resolveTurnInput(this.activeToolChoiceInput, promptCtx),
6650
+ resolveTurnInput(this.activeCachePolicyInput, promptCtx),
6651
+ resolveTurnInput(this.activeTailContextInput, promptCtx),
6652
+ resolveTurnInput(this.activeResponseSchemaInput, promptCtx),
6336
6653
  // Only consulted on the first iteration (see `pinnedThinkingPolicy`); resolved
6337
6654
  // alongside the others so a resolver still sees the same turn context.
6338
- thinkingPolicyPinned ? Promise.resolve(void 0) : this.resolveTurnInput(
6655
+ thinkingPolicyPinned ? Promise.resolve(void 0) : resolveTurnInput(
6339
6656
  this.activeThinkingPolicyInput,
6340
6657
  promptCtx
6341
6658
  )
@@ -6511,7 +6828,7 @@ ${tailBody}
6511
6828
  isSubAgent: true
6512
6829
  });
6513
6830
  this.failSubAgent("budget_exhausted", this.budgetWallDetail);
6514
- return this.turnDone("budget-exhausted", this.budgetWallDetail);
6831
+ return turnDone("budget-exhausted", this.budgetWallDetail);
6515
6832
  }
6516
6833
  return this.reportBudgetExhausted(e);
6517
6834
  }
@@ -6644,36 +6961,36 @@ ${tailBody}
6644
6961
  if (toolCalls.length > 0) {
6645
6962
  await Promise.all(
6646
6963
  toolCalls.map(async (tc) => {
6647
- const fold = this.getFold(tc.name);
6964
+ const fold = this.folds.foldFor(tc.name);
6648
6965
  if (fold) {
6649
- this.consecutiveFoldOps += 1;
6650
- if (this.consecutiveFoldOps > this.maxFoldOperations) {
6966
+ const foldOps = this.folds.noteOp();
6967
+ if (foldOps > this.maxFoldOperations) {
6651
6968
  logger2.warn(
6652
6969
  `ChatDriver: fold operation limit (${this.maxFoldOperations}) reached \u2014 injecting guidance`
6653
6970
  );
6654
6971
  executedById.set(tc.id, {
6655
6972
  toolCallId: tc.id,
6656
- content: `You have opened and closed folds ${this.consecutiveFoldOps} times without calling any tools. Please call a specific tool to make progress, or respond to the user.`
6973
+ content: `You have opened and closed folds ${foldOps} times without calling any tools. Please call a specific tool to make progress, or respond to the user.`
6657
6974
  });
6658
6975
  return;
6659
6976
  }
6660
- const content = await this.openFold(tc.name, fold, tc.args, tc.id);
6977
+ const content = await this.folds.open(tc.name, fold, tc.args, tc.id);
6661
6978
  executedById.set(tc.id, { toolCallId: tc.id, content });
6662
6979
  iterations -= 1;
6663
6980
  return;
6664
6981
  }
6665
- if (tc.name.startsWith("close_") && this.foldStack.length > 0) {
6666
- const topFoldName = this.foldStack[this.foldStack.length - 1].foldName;
6982
+ if (tc.name.startsWith("close_") && this.folds.depth > 0) {
6983
+ const topFoldName = this.folds.topName;
6667
6984
  if (tc.name === `close_${topFoldName}`) {
6668
- this.consecutiveFoldOps += 1;
6669
- if (this.consecutiveFoldOps > this.maxFoldOperations) {
6985
+ const foldOps = this.folds.noteOp();
6986
+ if (foldOps > this.maxFoldOperations) {
6670
6987
  executedById.set(tc.id, {
6671
6988
  toolCallId: tc.id,
6672
- content: `You have opened and closed folds ${this.consecutiveFoldOps} times without calling any tools. Please call a specific tool to make progress, or respond to the user.`
6989
+ content: `You have opened and closed folds ${foldOps} times without calling any tools. Please call a specific tool to make progress, or respond to the user.`
6673
6990
  });
6674
6991
  return;
6675
6992
  }
6676
- const content = this.closeFold();
6993
+ const content = this.folds.close();
6677
6994
  executedById.set(tc.id, { toolCallId: tc.id, content });
6678
6995
  iterations -= 1;
6679
6996
  return;
@@ -6681,7 +6998,7 @@ ${tailBody}
6681
6998
  }
6682
6999
  const handler = this.toolHandlers[tc.name];
6683
7000
  if (!handler) {
6684
- const containingFold = this.findFoldContaining(tc.name);
7001
+ const containingFold = this.folds.findContaining(tc.name);
6685
7002
  if (containingFold) {
6686
7003
  logger2.debug(
6687
7004
  `ChatDriver: model called folded tool "${tc.name}" \u2014 guiding to open "${containingFold}"`
@@ -6701,7 +7018,7 @@ ${tailBody}
6701
7018
  }
6702
7019
  if (this.everSeenToolNames.has(tc.name)) {
6703
7020
  this.consecutiveUnknownToolCalls += 1;
6704
- const hidingFold = this.foldHidingTool(tc.name);
7021
+ const hidingFold = this.folds.hidingTool(tc.name);
6705
7022
  let content;
6706
7023
  if (hidingFold) {
6707
7024
  content = `"${tc.name}" is not available while the "${hidingFold}" fold is open. Call close_${hidingFold} to return to the previous set of tools, then call ${tc.name}.`;
@@ -6797,12 +7114,12 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6797
7114
  );
6798
7115
  }
6799
7116
  if (anyRealToolExecuted) {
6800
- this.consecutiveFoldOps = 0;
7117
+ this.folds.clearOps();
6801
7118
  this.consecutiveUnknownToolCalls = 0;
6802
7119
  this.recentUnknownToolNames.clear();
6803
7120
  this.recentStaleToolNames.clear();
6804
7121
  }
6805
- const foldPath = this.foldStack.map((f) => f.foldName);
7122
+ const foldPath = this.folds.activeNames();
6806
7123
  for (const tc of response.toolCalls) {
6807
7124
  if (tc.name === REQUEST_CONTINUATION_TOOL) {
6808
7125
  this.appendToHistory({
@@ -6839,7 +7156,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6839
7156
  const tcMsg = this.history[tcMsgIdx];
6840
7157
  const availableToolNames = Object.keys(this.toolHandlers);
6841
7158
  const annotatedCalls = tcMsg.toolCalls.map((tc) => {
6842
- const isFoldOpen = !!this.getFold(tc.name) || // Was a fold facade at time of the call (now the tool set has changed)
7159
+ const isFoldOpen = !!this.folds.foldFor(tc.name) || // Was a fold facade at time of the call (now the tool set has changed)
6843
7160
  // — detect by checking if the result message indicated a fold open
6844
7161
  executedById.get(tc.id)?.content?.startsWith("Fold opened:");
6845
7162
  const isFoldClose = executedById.get(tc.id)?.content?.startsWith("Fold closed:");
@@ -6898,7 +7215,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6898
7215
  return { reason: "agent-handoff", summary, remainingTask };
6899
7216
  }
6900
7217
  if (this.subAgentCompletion) {
6901
- return this.turnDone();
7218
+ return turnDone();
6902
7219
  }
6903
7220
  }
6904
7221
  if (iterations >= this.maxToolIterations) {
@@ -6912,7 +7229,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6912
7229
  });
6913
7230
  return this.endTurnWithFailure(TURN_FAILURE_EXITS.maxIterations);
6914
7231
  }
6915
- return this.turnDone();
7232
+ return turnDone();
6916
7233
  }
6917
7234
  /**
6918
7235
  * Append prose the DRIVER wrote rather than the model — a failure notice, a limit explanation,
@@ -7020,11 +7337,6 @@ var OrchestratingDriver = class extends EventTarget {
7020
7337
  super();
7021
7338
  this.providerRegistry = providerRegistry;
7022
7339
  this.agents = agents;
7023
- /**
7024
- * Aborted on driver disposal. Threaded into `AgentLifecycleContext.signal`
7025
- * so long-running `onActivate` work can bail if the session disconnects.
7026
- */
7027
- this.lifecycleAbortController = new AbortController();
7028
7340
  /**
7029
7341
  * Set by `cancel()` for the duration of a `sendMessage` run. Breaks the
7030
7342
  * handoff loop so we don't classify or start another agent turn after the
@@ -7042,28 +7354,6 @@ var OrchestratingDriver = class extends EventTarget {
7042
7354
  * made it vanish. Reset at the top of each `runOrchestratedTurn`.
7043
7355
  */
7044
7356
  this.userMessageAppended = false;
7045
- /**
7046
- * Sticky user pick from the picker (or the host's `setAgent` API). Only
7047
- * changes on explicit user action. Survives flow completion: when a stateful
7048
- * agent releases, the user-pin stays put and routing falls back to it on
7049
- * the next turn.
7050
- */
7051
- this.pinnedAgentName = null;
7052
- /**
7053
- * Transient flow lock — set when a stateful agent's `onActivate` fires,
7054
- * cleared by `releaseActiveAgent`. While non-null, routing precedence goes
7055
- * to this agent and the picker is locked. Independent of
7056
- * `pinnedAgentName` so completing a flow doesn't wipe the user's sticky
7057
- * choice.
7058
- */
7059
- this.flowOwnerAgentName = null;
7060
- /**
7061
- * Per-agent restored state for a rehydrated session, keyed by agent name.
7062
- * Primed by the host on restore; each entry is passed to that agent's
7063
- * `onActivate` as `ctx.restored` on its next activation, then deleted so a
7064
- * later re-activation starts fresh.
7065
- */
7066
- this.restoredAgentStates = {};
7067
7357
  this.sessionKey = options.sessionKey ?? "";
7068
7358
  this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS;
7069
7359
  this.classifierHistoryLength = options.classifierHistoryLength ?? DEFAULT_CLASSIFIER_HISTORY_LENGTH;
@@ -7084,7 +7374,9 @@ var OrchestratingDriver = class extends EventTarget {
7084
7374
  maxTurnSnapshots: options.maxTurnSnapshots,
7085
7375
  sessionKey: this.sessionKey,
7086
7376
  activityBus: options.activityBus,
7087
- budgetExhaustedMessage: options.budgetExhaustedMessage
7377
+ budgetExhaustedMessage: options.budgetExhaustedMessage,
7378
+ // This driver releases after the turn so it can fire the host-facing events; see the option.
7379
+ deferAgentRelease: true
7088
7380
  });
7089
7381
  this.chatDriver.addEventListener("history-updated", (e) => {
7090
7382
  this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
@@ -7121,6 +7413,28 @@ var OrchestratingDriver = class extends EventTarget {
7121
7413
  );
7122
7414
  });
7123
7415
  }
7416
+ /**
7417
+ * The stateful-agent contract (§7.1): active agent, user pin, flow lock, restored state, and
7418
+ * the `onActivate` / `onDeactivate` hooks.
7419
+ *
7420
+ * **The inner driver's session, not one of our own.** One session is the invariant: two would
7421
+ * let the outer and inner driver disagree about which agent is active, and the inner one would
7422
+ * never be activated at all, because this driver installs a *decorated* config through
7423
+ * `applyAgent` rather than going via `ChatDriver.activateAgent`. This driver keeps the routing
7424
+ * decisions; the session owns the lifecycle.
7425
+ */
7426
+ get agentSession() {
7427
+ return this.chatDriver.agentSession;
7428
+ }
7429
+ /**
7430
+ * The active agent, or `undefined` before the first activation and after a release.
7431
+ *
7432
+ * Kept as an accessor over `agentSession` rather than removed: it is a public member of an
7433
+ * exported class, so nothing in this package reading it is not the same as nothing reading it.
7434
+ */
7435
+ get activeAgent() {
7436
+ return this.agentSession.getActive();
7437
+ }
7124
7438
  resolveInteraction(interactionId, result) {
7125
7439
  this.chatDriver.resolveInteraction(interactionId, result);
7126
7440
  }
@@ -7146,7 +7460,7 @@ var OrchestratingDriver = class extends EventTarget {
7146
7460
  * return to automatic routing.
7147
7461
  */
7148
7462
  setPinnedAgent(name) {
7149
- this.pinnedAgentName = name;
7463
+ this.agentSession.setPinned(name);
7150
7464
  }
7151
7465
  /**
7152
7466
  * Set the flow-owner lock — used to restore state from Redux on driver
@@ -7155,7 +7469,7 @@ var OrchestratingDriver = class extends EventTarget {
7155
7469
  * for.
7156
7470
  */
7157
7471
  setFlowOwner(name) {
7158
- this.flowOwnerAgentName = name;
7472
+ this.agentSession.setFlowOwner(name);
7159
7473
  }
7160
7474
  loadHistory(messages) {
7161
7475
  this.chatDriver.loadHistory(messages);
@@ -7314,7 +7628,7 @@ var OrchestratingDriver = class extends EventTarget {
7314
7628
  }
7315
7629
  /** {@inheritDoc AiDriver.primeRestoredAgentState} */
7316
7630
  primeRestoredAgentState(snapshots) {
7317
- this.restoredAgentStates = { ...snapshots };
7631
+ this.agentSession.primeRestoredStates(snapshots);
7318
7632
  }
7319
7633
  /**
7320
7634
  * {@inheritDoc AiDriver.getContextReserve}
@@ -7344,51 +7658,14 @@ var OrchestratingDriver = class extends EventTarget {
7344
7658
  }
7345
7659
  /** {@inheritDoc AiDriver.getActiveAgent} */
7346
7660
  getActiveAgent() {
7347
- return this.activeAgent;
7661
+ return this.agentSession.getActive();
7348
7662
  }
7349
7663
  async applyAgent(agent) {
7350
- const previousAgent = this.activeAgent;
7351
- const isSwitch = !previousAgent || previousAgent.name !== agent.name;
7352
- if (isSwitch) {
7353
- recordMetaEvent(this.sessionKey, "agent.handoff", {
7354
- from: previousAgent?.name ?? null,
7355
- to: agent.name,
7356
- // GENC-1351: was a restored snapshot waiting for this agent at activation?
7357
- restored: this.restoredAgentStates[agent.name] !== void 0
7358
- });
7664
+ const { previousAgent, flowOwnerChangedTo } = await this.agentSession.activate(agent);
7665
+ if (flowOwnerChangedTo !== void 0) {
7666
+ this.dispatchEvent(new CustomEvent("flow-owner-changed", { detail: flowOwnerChangedTo }));
7359
7667
  }
7360
- if (isSwitch && previousAgent?.onDeactivate) {
7361
- try {
7362
- await previousAgent.onDeactivate({
7363
- agentName: previousAgent.name,
7364
- sessionKey: this.sessionKey,
7365
- signal: this.lifecycleAbortController.signal
7366
- });
7367
- } catch (e) {
7368
- logger2.warn(`OrchestratingDriver: onDeactivate("${previousAgent.name}") threw:`, e);
7369
- }
7370
- }
7371
- if (isSwitch && agent.onActivate) {
7372
- const restored = this.restoredAgentStates[agent.name];
7373
- if (restored !== void 0) delete this.restoredAgentStates[agent.name];
7374
- try {
7375
- await agent.onActivate({
7376
- agentName: agent.name,
7377
- sessionKey: this.sessionKey,
7378
- previousAgentName: previousAgent?.name,
7379
- signal: this.lifecycleAbortController.signal,
7380
- restored
7381
- });
7382
- } catch (e) {
7383
- logger2.warn(`OrchestratingDriver: onActivate("${agent.name}") threw:`, e);
7384
- }
7385
- }
7386
- const hasLifecycleHooks = !!(agent.onActivate || agent.onDeactivate);
7387
- if (isSwitch && hasLifecycleHooks && this.flowOwnerAgentName !== agent.name) {
7388
- this.flowOwnerAgentName = agent.name;
7389
- this.dispatchEvent(new CustomEvent("flow-owner-changed", { detail: agent.name }));
7390
- }
7391
- const isTerminal = isFallback(agent) || this.pinnedAgentName !== null || this.flowOwnerAgentName !== null || hasLifecycleHooks;
7668
+ const isTerminal = isFallback(agent) || this.agentSession.getPinned() !== null || this.agentSession.getFlowOwner() !== null || !!(agent.onActivate || agent.onDeactivate);
7392
7669
  let agentToApply = agent;
7393
7670
  if (!isTerminal) {
7394
7671
  const declaredTools = agent.toolDefinitions;
@@ -7409,7 +7686,6 @@ var OrchestratingDriver = class extends EventTarget {
7409
7686
  }
7410
7687
  this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
7411
7688
  this.chatDriver.applyAgent(agentToApply);
7412
- this.activeAgent = agent;
7413
7689
  this.dispatchEvent(new CustomEvent("orchestrating-stop"));
7414
7690
  this.dispatchEvent(new CustomEvent("agent-changed", { detail: agent }));
7415
7691
  }
@@ -7425,25 +7701,14 @@ var OrchestratingDriver = class extends EventTarget {
7425
7701
  * routing through the classifier.
7426
7702
  */
7427
7703
  async releaseActiveAgent() {
7428
- const agent = this.activeAgent;
7429
- if (!agent) return;
7430
- if (agent.onDeactivate) {
7431
- try {
7432
- await agent.onDeactivate({
7433
- agentName: agent.name,
7434
- sessionKey: this.sessionKey,
7435
- signal: this.lifecycleAbortController.signal
7436
- });
7437
- } catch (e) {
7438
- logger2.warn(`OrchestratingDriver: release onDeactivate("${agent.name}") threw:`, e);
7439
- }
7440
- }
7441
- this.activeAgent = void 0;
7442
- if (this.flowOwnerAgentName !== null) {
7443
- this.flowOwnerAgentName = null;
7444
- this.dispatchEvent(new CustomEvent("flow-owner-changed", { detail: null }));
7704
+ const released = await this.agentSession.release();
7705
+ if (!released) return;
7706
+ if (released.flowOwnerChangedTo !== void 0) {
7707
+ this.dispatchEvent(
7708
+ new CustomEvent("flow-owner-changed", { detail: released.flowOwnerChangedTo })
7709
+ );
7445
7710
  }
7446
- this.dispatchEvent(new CustomEvent("agent-released", { detail: agent }));
7711
+ this.dispatchEvent(new CustomEvent("agent-released", { detail: released.previousAgent }));
7447
7712
  this.dispatchEvent(new CustomEvent("agent-changed", { detail: void 0 }));
7448
7713
  }
7449
7714
  /**
@@ -7461,21 +7726,8 @@ var OrchestratingDriver = class extends EventTarget {
7461
7726
  * release resources cleanly.
7462
7727
  */
7463
7728
  async dispose() {
7464
- const previousAgent = this.activeAgent;
7465
- if (previousAgent?.onDeactivate) {
7466
- try {
7467
- await previousAgent.onDeactivate({
7468
- agentName: previousAgent.name,
7469
- sessionKey: this.sessionKey,
7470
- signal: this.lifecycleAbortController.signal
7471
- });
7472
- } catch (e) {
7473
- logger2.warn(`OrchestratingDriver: dispose onDeactivate("${previousAgent.name}") threw:`, e);
7474
- }
7475
- }
7476
- this.lifecycleAbortController.abort();
7477
- this.chatDriver.dispose();
7478
- this.activeAgent = void 0;
7729
+ await this.agentSession.dispose();
7730
+ await this.chatDriver.dispose();
7479
7731
  }
7480
7732
  /**
7481
7733
  * Record one routing decision on the debug timeline — what was chosen, why, what it cost, and
@@ -7663,7 +7915,7 @@ ${recentMessages}` : ""}`;
7663
7915
  * — protects against stale state after an agents-array reassignment.
7664
7916
  */
7665
7917
  resolvePinnedAgent() {
7666
- const name = this.flowOwnerAgentName ?? this.pinnedAgentName;
7918
+ const name = this.agentSession.getLockedName();
7667
7919
  if (name === null) return void 0;
7668
7920
  const match = this.agents.find((a) => a.name === name);
7669
7921
  if (!match) {