@genesislcap/ai-assistant 15.24.1 → 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
@@ -3783,6 +3783,77 @@ function createInteractionContext(interactionId) {
3783
3783
  };
3784
3784
  }
3785
3785
 
3786
+ // src/suggestions/generate-suggestions.ts
3787
+ var HISTORY_WINDOW = 8;
3788
+ function capabilities(ctx, allAgentInfo) {
3789
+ let agentContext = "";
3790
+ let toolContext = "";
3791
+ if (allAgentInfo?.length) {
3792
+ const agentDescriptions = allAgentInfo.map((agent) => {
3793
+ const tools = agent.tools.map((t) => t.name).join(", ");
3794
+ return `- ${agent.name} (${agent.description}): ${tools ? `(Tools: ${tools})` : "No tools"}`;
3795
+ }).join("\n");
3796
+ agentContext = `The assistant has the following capabilities:
3797
+ ${agentDescriptions}`;
3798
+ const allToolNames = [
3799
+ ...new Set(allAgentInfo.flatMap((agent) => agent.tools.map((t) => t.name)))
3800
+ ].join(", ");
3801
+ toolContext = allToolNames ? `You have access to the following tools across all agents: ${allToolNames}.` : "";
3802
+ } else if (ctx.activeAgentName) {
3803
+ const toolNames = ctx.toolDefinitions.map((tool) => tool.name).join(", ");
3804
+ agentContext = `You are currently acting as the "${ctx.activeAgentName}" agent.`;
3805
+ toolContext = toolNames ? `You have access to the following tools: ${toolNames}.` : "";
3806
+ }
3807
+ return agentContext || toolContext ? `
3808
+
3809
+ <capabilities>
3810
+ ${[agentContext, toolContext].filter(Boolean).join(
3811
+ "\n"
3812
+ )}
3813
+ </capabilities>
3814
+ Suggestions must only cover what the agent is capable of. Do not suggest anything outside these capabilities.` : "";
3815
+ }
3816
+ var OUTPUT_FORMAT = (count) => `
3817
+
3818
+ Output format (strict):
3819
+ - Exactly ${count} lines.
3820
+ - One suggestion per line.
3821
+ - No numbering, bullets, markdown, emojis, code blocks, or quotes around the suggestion.
3822
+ - No preamble, headings, summary, or commentary before or after the list.`;
3823
+ 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.";
3824
+ function buildSuggestionsPrompt(ctx, history, prompt2, count, allAgentInfo) {
3825
+ const capabilitiesBlock = capabilities(ctx, allAgentInfo);
3826
+ const guidanceBlock = prompt2 ? `
3827
+
3828
+ Additional guidance: "${prompt2}"` : "";
3829
+ if (history.length === 0) {
3830
+ 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);
3831
+ }
3832
+ const conversationContext = history.filter(
3833
+ (m) => (m.role === "user" || m.role === "assistant") && !m.toolCalls?.length && !m.thinking && !!m.content?.trim()
3834
+ ).slice(-HISTORY_WINDOW).map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
3835
+ return `<conversation_history>
3836
+ ${conversationContext}
3837
+ </conversation_history>
3838
+
3839
+ 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);
3840
+ }
3841
+ function parseSuggestions(text, count) {
3842
+ 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);
3843
+ }
3844
+ async function generateSuggestions(ctx, history, prompt2, count, allAgentInfo) {
3845
+ const defaultProvider = ctx.registry.default();
3846
+ if (!defaultProvider.prompt) {
3847
+ logger2.warn("ChatDriver: default AI provider does not implement prompt()");
3848
+ return [];
3849
+ }
3850
+ const userMessage = buildSuggestionsPrompt(ctx, history, prompt2, count, allAgentInfo);
3851
+ const text = await defaultProvider.prompt(userMessage, {
3852
+ systemPrompt: SUGGESTIONS_SYSTEM_PROMPT
3853
+ });
3854
+ return parseSuggestions(text, count);
3855
+ }
3856
+
3786
3857
  // src/utils/context-tokens.ts
3787
3858
  var APPROX_CHARS_PER_TOKEN = 4;
3788
3859
  var IMAGE_TOKEN_ESTIMATE = 1600;
@@ -4256,6 +4327,12 @@ The above summarizes earlier parts of this conversation that were compacted. Con
4256
4327
  });
4257
4328
  }
4258
4329
 
4330
+ // src/utils/resolve-turn-input.ts
4331
+ async function resolveTurnInput(input, ctx) {
4332
+ if (input === void 0) return void 0;
4333
+ return typeof input === "function" ? input(ctx) : input;
4334
+ }
4335
+
4259
4336
  // src/utils/sum-usage.ts
4260
4337
  function emptyUsage() {
4261
4338
  return {
@@ -4326,9 +4403,358 @@ function accumulate(messages, into) {
4326
4403
  }
4327
4404
  }
4328
4405
 
4406
+ // src/components/agent-session/agent-session.ts
4407
+ var WARN_PREFIX = "AgentSession";
4408
+ var AgentSession = class {
4409
+ constructor(deps) {
4410
+ this.deps = deps;
4411
+ /**
4412
+ * Aborted on disposal. Threaded into `AgentLifecycleContext.signal` so long-running
4413
+ * `onActivate` work can bail if the session disconnects.
4414
+ */
4415
+ this.lifecycleAbortController = new AbortController();
4416
+ /**
4417
+ * Sticky user pick from the picker (or the host's `setAgent` API). Only changes on explicit
4418
+ * user action. Survives flow completion: when a stateful agent releases, the user-pin stays put
4419
+ * and routing falls back to it on the next turn.
4420
+ */
4421
+ this.pinned = null;
4422
+ /**
4423
+ * Transient flow lock — set when a stateful agent activates, cleared on release. While
4424
+ * non-null, routing precedence goes to this agent and the picker is locked. Independent of
4425
+ * the user pin so completing a flow doesn't wipe the user's sticky choice.
4426
+ */
4427
+ this.flowOwner = null;
4428
+ /**
4429
+ * Per-agent restored state for a rehydrated session, keyed by agent name. Primed by the host on
4430
+ * restore; each entry is passed to that agent's `onActivate` as `ctx.restored` on its next
4431
+ * activation, then deleted so a later re-activation starts fresh.
4432
+ */
4433
+ this.restoredStates = {};
4434
+ }
4435
+ /** The active agent, or `undefined` before the first activation and after a release. */
4436
+ getActive() {
4437
+ return this.active;
4438
+ }
4439
+ /** The sticky user pin, or `null` for automatic routing. */
4440
+ getPinned() {
4441
+ return this.pinned;
4442
+ }
4443
+ /** The transient flow lock, or `null` when no flow holds routing. */
4444
+ getFlowOwner() {
4445
+ return this.flowOwner;
4446
+ }
4447
+ /**
4448
+ * The name routing is locked to, flow lock first. A flow in progress outranks the user's
4449
+ * sticky pick for the duration of the flow; neither being set means automatic routing.
4450
+ */
4451
+ getLockedName() {
4452
+ return this.flowOwner ?? this.pinned;
4453
+ }
4454
+ /** Set the sticky user pin. `null` returns to automatic routing. */
4455
+ setPinned(name) {
4456
+ this.pinned = name;
4457
+ }
4458
+ /**
4459
+ * Set the flow-owner lock directly — used to restore state on driver reconstruction
4460
+ * (agents-array changes, pop-in/pop-out), not to steer routing. Writes without firing hooks or
4461
+ * reporting a transition, because the flow it names is already running.
4462
+ */
4463
+ setFlowOwner(name) {
4464
+ this.flowOwner = name;
4465
+ }
4466
+ /** Prime per-agent restored state for a rehydrated session. Replaces any pending entries. */
4467
+ primeRestoredStates(snapshots) {
4468
+ this.restoredStates = { ...snapshots };
4469
+ }
4470
+ /** Whether a restored snapshot is waiting for this agent's next activation. */
4471
+ hasRestoredState(agentName) {
4472
+ return this.restoredStates[agentName] !== void 0;
4473
+ }
4474
+ /**
4475
+ * Make `agent` active, firing the lifecycle hooks around the swap: outgoing `onDeactivate`
4476
+ * first, then incoming `onActivate`. Both are awaited so heavy activation work (a machine
4477
+ * restore, say) completes before the agent's first turn runs, and a throw from either is logged
4478
+ * rather than propagated — an agent whose hook fails still activates.
4479
+ *
4480
+ * Re-activating the same agent by name is not a switch: no hooks fire, no lock moves, and no
4481
+ * handoff is recorded. That guard matters because a per-turn re-resolve calls through here.
4482
+ */
4483
+ async activate(agent) {
4484
+ const previousAgent = this.active;
4485
+ const switched = !previousAgent || previousAgent.name !== agent.name;
4486
+ const transition = { previousAgent, switched };
4487
+ if (switched) {
4488
+ recordMetaEvent(this.deps.sessionKey(), "agent.handoff", {
4489
+ from: previousAgent?.name ?? null,
4490
+ to: agent.name,
4491
+ // GENC-1351: was a restored snapshot waiting for this agent at activation?
4492
+ restored: this.hasRestoredState(agent.name)
4493
+ });
4494
+ }
4495
+ if (switched && previousAgent) await this.fireDeactivate(previousAgent, "switch");
4496
+ if (switched && agent.onActivate) {
4497
+ const restored = this.restoredStates[agent.name];
4498
+ if (restored !== void 0) delete this.restoredStates[agent.name];
4499
+ try {
4500
+ await agent.onActivate({
4501
+ agentName: agent.name,
4502
+ sessionKey: this.deps.sessionKey(),
4503
+ previousAgentName: previousAgent?.name,
4504
+ signal: this.lifecycleAbortController.signal,
4505
+ restored
4506
+ });
4507
+ } catch (e) {
4508
+ logger2.warn(`${WARN_PREFIX}: onActivate("${agent.name}") threw:`, e);
4509
+ }
4510
+ }
4511
+ const hasLifecycleHooks = !!(agent.onActivate || agent.onDeactivate);
4512
+ if (switched && hasLifecycleHooks && this.flowOwner !== agent.name) {
4513
+ this.flowOwner = agent.name;
4514
+ transition.flowOwnerChangedTo = agent.name;
4515
+ }
4516
+ this.active = agent;
4517
+ return transition;
4518
+ }
4519
+ /**
4520
+ * Release the active agent: fire `onDeactivate` and drop the flow lock.
4521
+ *
4522
+ * The user pin is intentionally **not** cleared — someone who explicitly picked this agent
4523
+ * should stay on it after the flow ends, so the next message starts a fresh flow on the same
4524
+ * agent rather than routing through the classifier.
4525
+ *
4526
+ * Returns `undefined` when there was no active agent, so a caller can skip its events.
4527
+ */
4528
+ async release() {
4529
+ const agent = this.active;
4530
+ if (!agent) return void 0;
4531
+ await this.fireDeactivate(agent, "release");
4532
+ this.active = void 0;
4533
+ const transition = { previousAgent: agent, switched: true };
4534
+ if (this.flowOwner !== null) {
4535
+ this.flowOwner = null;
4536
+ transition.flowOwnerChangedTo = null;
4537
+ }
4538
+ return transition;
4539
+ }
4540
+ /**
4541
+ * Fire `onDeactivate` on the active agent and abort pending lifecycle work, for session
4542
+ * teardown, so machines release resources cleanly. The session is not reusable afterwards: the
4543
+ * abort signal stays aborted, so a later `activate` would hand its hook a dead signal.
4544
+ *
4545
+ * `onDeactivate` runs **before** the abort, deliberately: teardown that needs to flush state
4546
+ * would otherwise be handed an already-aborted signal and have to skip its own work. The cost is
4547
+ * that an `onActivate` still in flight keeps running until `onDeactivate` finishes — both hold
4548
+ * the same signal. Acceptable because the two are sequenced by `activate`, so overlap only
4549
+ * happens if a hook resolves before the work it started; a hook that must stop immediately
4550
+ * should own its own cancellation.
4551
+ */
4552
+ async dispose() {
4553
+ const agent = this.active;
4554
+ if (agent) await this.fireDeactivate(agent, "dispose");
4555
+ this.lifecycleAbortController.abort();
4556
+ this.active = void 0;
4557
+ }
4558
+ /**
4559
+ * The one place `onDeactivate` is invoked. `reason` only reaches the warning text, which is what
4560
+ * tells a log reader which path fired.
4561
+ */
4562
+ async fireDeactivate(agent, reason) {
4563
+ if (!agent.onDeactivate) return;
4564
+ const label = reason === "switch" ? "" : `${reason} `;
4565
+ try {
4566
+ await agent.onDeactivate({
4567
+ agentName: agent.name,
4568
+ sessionKey: this.deps.sessionKey(),
4569
+ signal: this.lifecycleAbortController.signal
4570
+ });
4571
+ } catch (e) {
4572
+ logger2.warn(`${WARN_PREFIX}: ${label}onDeactivate("${agent.name}") threw:`, e);
4573
+ }
4574
+ }
4575
+ };
4576
+
4577
+ // src/components/chat-driver/chat-driver.constants.ts
4578
+ var DEFAULT_MAX_TOOL_ITERATIONS = 50;
4579
+ var DEFAULT_MAX_FOLD_OPERATIONS = 5;
4580
+ var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
4581
+ var DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
4582
+ var MAX_STALE_TOOL_CALLS = DEFAULT_MAX_UNKNOWN_TOOL_CALLS * 2;
4583
+ var MAX_MALFORMED_RETRIES = 5;
4584
+ var MAX_EMPTY_RESPONSE_RETRIES = 3;
4585
+ 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.";
4586
+ 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.";
4587
+ var MAX_SETUP_TRANSPORT_RETRIES = 3;
4588
+ var HARVESTED_SUBAGENT_EVENTS = /* @__PURE__ */ new Set([
4589
+ "turn.retry",
4590
+ "turn.error",
4591
+ "tool.failed",
4592
+ "tool.unresolved"
4593
+ ]);
4594
+ var REQUEST_CONTINUATION_TOOL = "request_continuation";
4595
+ var HANDOFF_TOOL_RESULT_PLACEHOLDER = "Handoff to another specialist \u2014 routing continues on the next turn.";
4596
+
4329
4597
  // src/utils/tool-fold.ts
4330
4598
  var TOOL_FOLD_SYMBOL = Symbol("toolFold");
4331
4599
 
4600
+ // src/components/chat-driver/fold-controller.ts
4601
+ var foldOf = (handler) => handler ? handler[TOOL_FOLD_SYMBOL] : void 0;
4602
+ var FoldController = class {
4603
+ constructor(deps) {
4604
+ this.deps = deps;
4605
+ this.stack = [];
4606
+ /**
4607
+ * Consecutive fold opens/closes with no tool call between them. A model that only opens and
4608
+ * closes is making no progress, and the loop breaks out past a ceiling.
4609
+ */
4610
+ this.consecutiveOps = 0;
4611
+ }
4612
+ /** Drop all fold state. Called on an agent swap — each agent starts outside every fold. */
4613
+ reset() {
4614
+ this.stack = [];
4615
+ this.consecutiveOps = 0;
4616
+ }
4617
+ /** Names of the currently open folds, outermost first. */
4618
+ activeNames() {
4619
+ return this.stack.map((f) => f.foldName);
4620
+ }
4621
+ get depth() {
4622
+ return this.stack.length;
4623
+ }
4624
+ /** The innermost open fold's name, or `undefined` when none is open. */
4625
+ get topName() {
4626
+ return this.stack[this.stack.length - 1]?.foldName;
4627
+ }
4628
+ /** Count one fold operation and return the new consecutive total. */
4629
+ noteOp() {
4630
+ this.consecutiveOps += 1;
4631
+ return this.consecutiveOps;
4632
+ }
4633
+ /** Real work happened, so the no-progress streak restarts. */
4634
+ clearOps() {
4635
+ this.consecutiveOps = 0;
4636
+ }
4637
+ /** Fold metadata for a tool name, or `undefined` if it is not a fold facade. */
4638
+ foldFor(toolName) {
4639
+ return foldOf(this.deps.getHandlers()[toolName]);
4640
+ }
4641
+ /**
4642
+ * Which fold to **open** to reach `toolName`, or `null` if no registered fold contains it.
4643
+ *
4644
+ * Returns the outermost *registered* ancestor, not the innermost fold holding the tool — the one
4645
+ * consumer uses the answer to tell the model what to call next, and a nested fold cannot be
4646
+ * opened before its parent. The recursion therefore only descends through closed folds: once a
4647
+ * fold is open its children are top-level in the live handler map and match directly.
4648
+ */
4649
+ findContaining(toolName, handlers) {
4650
+ const source = handlers ?? this.deps.getHandlers();
4651
+ for (const handler of Object.values(source)) {
4652
+ const fold = foldOf(handler);
4653
+ if (!fold) continue;
4654
+ if (fold.handlers[toolName]) return fold.name;
4655
+ if (this.findContaining(toolName, fold.handlers)) return fold.name;
4656
+ }
4657
+ return null;
4658
+ }
4659
+ /**
4660
+ * If an open fold is hiding a previously-available tool, the fold to close to start getting it
4661
+ * back, else `null`.
4662
+ *
4663
+ * Only exclusive folds hide tools — they replace the tool set on open rather than extending it —
4664
+ * so a base tool that was visible beforehand sits in a frame's `previousHandlers` but not in the
4665
+ * live map. Only the top fold's `close_` tool is active, so that is always the actionable step:
4666
+ * closing repeatedly walks back to a tool further down the stack.
4667
+ */
4668
+ hidingTool(toolName) {
4669
+ if (this.stack.length === 0) return null;
4670
+ const hidden = this.stack.some((f) => f.previousHandlers[toolName]);
4671
+ return hidden ? this.topName : null;
4672
+ }
4673
+ /** Open a fold: push a frame, swap the tool set, and return the response for the model. */
4674
+ open(foldName, fold, args, activeToolCallId) {
4675
+ for (const key of Object.keys(args)) {
4676
+ const innerHandler = fold.handlers[key];
4677
+ if (innerHandler) {
4678
+ logger2.debug(`ChatDriver: fold shortcut dispatch "${foldName}" \u2192 "${key}"`);
4679
+ this.pushFrame(foldName);
4680
+ this.applyToolSet(fold, foldName);
4681
+ this.clearOps();
4682
+ const innerArgs = typeof args[key] === "object" && args[key] !== null ? args[key] : {};
4683
+ return innerHandler(innerArgs, this.deps.handlerContext(activeToolCallId)).then(
4684
+ (r) => typeof r === "string" ? r : JSON.stringify(r)
4685
+ );
4686
+ }
4687
+ }
4688
+ this.pushFrame(foldName);
4689
+ this.applyToolSet(fold, foldName);
4690
+ const closeToolName = `close_${foldName}`;
4691
+ const innerToolNames = fold.tools.map((t) => t.name);
4692
+ let message = `Fold opened: ${foldName}. Tools now available: ${[...innerToolNames, closeToolName].join(", ")}.`;
4693
+ if (fold.usageNotes) message += ` Notes: ${fold.usageNotes}`;
4694
+ message += ` Call ${closeToolName} when done to return to the previous tools.`;
4695
+ return Promise.resolve(message);
4696
+ }
4697
+ /** Close the top fold: pop the frame, restore the tool set it captured. */
4698
+ close() {
4699
+ const frame = this.stack.pop();
4700
+ if (!frame) return "No fold is currently open.";
4701
+ this.deps.setDefinitions(frame.previousDefinitions);
4702
+ this.deps.setHandlers(frame.previousHandlers);
4703
+ const toolNames = frame.previousDefinitions.map((t) => t.name);
4704
+ return `Fold closed: ${frame.foldName}. Tools now available: ${toolNames.join(", ")}.`;
4705
+ }
4706
+ /** The fold-awareness suffix appended to the system prompt on each LLM call. */
4707
+ systemPromptSuffix() {
4708
+ const activeFolds = Object.values(this.deps.getHandlers()).map(foldOf).filter((f) => !!f);
4709
+ if (activeFolds.length === 0 && this.stack.length === 0) return "";
4710
+ const parts = [
4711
+ "\n\n--- Tool Folds ---",
4712
+ "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."
4713
+ ];
4714
+ if (this.topName) {
4715
+ parts.push(
4716
+ `You are currently inside the "${this.topName}" fold. Call close_${this.topName} when you are done with these tools.`
4717
+ );
4718
+ }
4719
+ for (const fold of activeFolds) parts.push(`\u2022 ${fold.name}: ${fold.description}`);
4720
+ return parts.join("\n");
4721
+ }
4722
+ pushFrame(foldName) {
4723
+ this.stack.push({
4724
+ foldName,
4725
+ previousDefinitions: [...this.deps.getDefinitions()],
4726
+ previousHandlers: { ...this.deps.getHandlers() }
4727
+ });
4728
+ }
4729
+ /**
4730
+ * Install the fold's inner tool set, replacing it (exclusive) or extending it (non-exclusive),
4731
+ * and inject the close tool. Does not touch the stack.
4732
+ */
4733
+ applyToolSet(fold, foldName) {
4734
+ const closeToolName = `close_${foldName}`;
4735
+ const newDefs = [];
4736
+ const newHandlers = {};
4737
+ if (!fold.exclusive) {
4738
+ for (const def of this.deps.getDefinitions()) {
4739
+ if (def.name !== foldName) newDefs.push(def);
4740
+ }
4741
+ for (const [name, handler] of Object.entries(this.deps.getHandlers())) {
4742
+ if (name !== foldName) newHandlers[name] = handler;
4743
+ }
4744
+ }
4745
+ newDefs.push(...fold.tools);
4746
+ Object.assign(newHandlers, fold.handlers);
4747
+ newDefs.push({
4748
+ name: closeToolName,
4749
+ description: `Close the ${foldName} fold and return to the previous set of tools.`,
4750
+ parameters: { type: "object", properties: {} }
4751
+ });
4752
+ newHandlers[closeToolName] = async () => this.close();
4753
+ this.deps.setDefinitions(newDefs);
4754
+ this.deps.setHandlers(newHandlers);
4755
+ }
4756
+ };
4757
+
4332
4758
  // src/components/chat-driver/turn-failures.ts
4333
4759
  var TURN_FAILURE_EXITS = {
4334
4760
  contextGuard: {
@@ -4400,6 +4826,15 @@ var TURN_FAILURE_EXITS = {
4400
4826
  // `TurnFailureReason` and cost the literal types that let `turn-failures.test.ts` pin the
4401
4827
  // two deliberate divergences at compile time instead of at runtime.
4402
4828
  };
4829
+ function turnDone(failureReason, budget, providerRefused) {
4830
+ if (!failureReason) return { reason: "done" };
4831
+ return {
4832
+ reason: "done",
4833
+ failureReason,
4834
+ ...budget ? { budget } : {},
4835
+ ...providerRefused ? { providerRefused } : {}
4836
+ };
4837
+ }
4403
4838
 
4404
4839
  // src/components/chat-driver/chat-driver.ts
4405
4840
  var toolResultImages = (result) => {
@@ -4430,25 +4865,6 @@ var budgetDetailOf = (e) => {
4430
4865
  ...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
4431
4866
  };
4432
4867
  };
4433
- var DEFAULT_MAX_TOOL_ITERATIONS = 50;
4434
- var DEFAULT_MAX_FOLD_OPERATIONS = 5;
4435
- var DEFAULT_MAX_TURN_SNAPSHOTS = 400;
4436
- var DEFAULT_MAX_UNKNOWN_TOOL_CALLS = 5;
4437
- var MAX_STALE_TOOL_CALLS = DEFAULT_MAX_UNKNOWN_TOOL_CALLS * 2;
4438
- var MAX_MALFORMED_RETRIES = 5;
4439
- var MAX_EMPTY_RESPONSE_RETRIES = 3;
4440
- 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.";
4441
- 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.";
4442
- var MAX_SETUP_TRANSPORT_RETRIES = 3;
4443
- var SUGGESTIONS_HISTORY_WINDOW = 8;
4444
- var HARVESTED_SUBAGENT_EVENTS = /* @__PURE__ */ new Set([
4445
- "turn.retry",
4446
- "turn.error",
4447
- "tool.failed",
4448
- "tool.unresolved"
4449
- ]);
4450
- var REQUEST_CONTINUATION_TOOL = "request_continuation";
4451
- var HANDOFF_TOOL_RESULT_PLACEHOLDER = "Handoff to another specialist \u2014 routing continues on the next turn.";
4452
4868
  var ChatDriver = class _ChatDriver extends EventTarget {
4453
4869
  constructor(providerRegistry, config = {}) {
4454
4870
  super();
@@ -4511,10 +4927,18 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4511
4927
  * (`callPhaseEpoch < currentPhaseEpoch`), with an `agentEnd` backstop.
4512
4928
  */
4513
4929
  this.currentPhaseEpoch = 0;
4514
- /** Stack of fold frames — grows when a fold opens, shrinks when it closes. */
4515
- this.foldStack = [];
4516
- /** Consecutive fold open/close ops without a real tool call. Reset on real tool execution. */
4517
- this.consecutiveFoldOps = 0;
4930
+ /** Tool folds: the open-fold stack, the no-progress counter, and the tool-surface swap. */
4931
+ this.folds = new FoldController({
4932
+ getDefinitions: () => this.toolDefinitions,
4933
+ setDefinitions: (defs) => {
4934
+ this.toolDefinitions = defs;
4935
+ },
4936
+ getHandlers: () => this.toolHandlers,
4937
+ setHandlers: (handlers) => {
4938
+ this.toolHandlers = handlers;
4939
+ },
4940
+ handlerContext: (activeToolCallId) => this.buildHandlerContext(activeToolCallId)
4941
+ });
4518
4942
  /** Consecutive unknown-tool calls without a real tool call. Reset on real tool execution. */
4519
4943
  this.consecutiveUnknownToolCalls = 0;
4520
4944
  /**
@@ -4614,6 +5038,19 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4614
5038
  * picked up on the next turn.
4615
5039
  */
4616
5040
  this.resolvedStatusCache = /* @__PURE__ */ new Map();
5041
+ /**
5042
+ * The stateful-agent contract for this driver: active agent, user pin, flow lock, restored
5043
+ * state, and the `onActivate` / `onDeactivate` hooks (§7.1).
5044
+ *
5045
+ * **Public because `OrchestratingDriver` reaches through to it.** The orchestrator decorates a
5046
+ * config before installing it (the cross-agent continuation tool), so it cannot route through
5047
+ * {@link ChatDriver.activateAgent} and must drive the session itself — and one shared session is
5048
+ * the point: two would let `getActiveAgent()` disagree between the outer and inner driver.
5049
+ * `@internal` rather than private so that reach is legal without six delegating one-liners.
5050
+ *
5051
+ * @internal
5052
+ */
5053
+ this.agentSession = new AgentSession({ sessionKey: () => this.sessionKey });
4617
5054
  /**
4618
5055
  * A provider refusal was observed this turn, so the tool loop must not call the model again
4619
5056
  * (GENC-1506).
@@ -4650,6 +5087,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4650
5087
  condenseBatchCalls = 1,
4651
5088
  maxTurnSnapshots = DEFAULT_MAX_TURN_SNAPSHOTS,
4652
5089
  sessionKey = "",
5090
+ deferAgentRelease = false,
4653
5091
  activityBus = NOOP_ACTIVITY_BUS,
4654
5092
  budgetExhaustedMessage,
4655
5093
  providerRefusedMessage
@@ -4657,6 +5095,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4657
5095
  this.maxToolIterations = maxToolIterations;
4658
5096
  this.condenseBatchCalls = condenseBatchCalls;
4659
5097
  this.sessionKey = sessionKey;
5098
+ this.deferAgentRelease = deferAgentRelease;
4660
5099
  this.activityBus = activityBus;
4661
5100
  this.budgetExhaustedMessageOverride = budgetExhaustedMessage;
4662
5101
  this.providerRefusedMessageOverride = providerRefusedMessage;
@@ -4704,12 +5143,13 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4704
5143
  * interaction is instead settled by `resolveInteraction` (user response or
4705
5144
  * timeout), which also stamps history and reverts the chat-input override.
4706
5145
  */
4707
- dispose() {
5146
+ async dispose() {
4708
5147
  this.lifecycleController.abort(new DOMException("AI assistant driver disposed", "AbortError"));
4709
5148
  for (const handle of this.interactionContexts.values()) handle.dispose();
4710
5149
  this.interactionContexts.clear();
4711
5150
  this.unsubscribeRegistry?.();
4712
5151
  this.unsubscribeRegistry = void 0;
5152
+ await this.agentSession.dispose();
4713
5153
  }
4714
5154
  /**
4715
5155
  * Stop the current turn (user "stop" button). Aborts the in-flight provider
@@ -4758,23 +5198,6 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4758
5198
  }
4759
5199
  return { reason: "done" };
4760
5200
  }
4761
- /**
4762
- * Build the `done` loop result, carrying the typed failure reason when the turn
4763
- * bailed (PTC-0). The discriminant stays `'done'` either way — the same value a
4764
- * clean turn returns — so consumers matching on `reason === 'done'` are unchanged;
4765
- * `failureReason` is simply present on a failure and absent on success. Omitted
4766
- * (rather than set to `undefined`) so a happy-path result stays byte-identical to
4767
- * the historical `{ reason: 'done' }`.
4768
- */
4769
- turnDone(failureReason, budget, providerRefused) {
4770
- if (!failureReason) return { reason: "done" };
4771
- return {
4772
- reason: "done",
4773
- failureReason,
4774
- ...budget ? { budget } : {},
4775
- ...providerRefused ? { providerRefused } : {}
4776
- };
4777
- }
4778
5201
  /**
4779
5202
  * End the turn on a terminal failure: fail upward when this driver is a sub-agent, speak
4780
5203
  * to the user when it is not, and hand the reason to the caller either way.
@@ -4800,7 +5223,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4800
5223
  } else {
4801
5224
  this.appendDriverNotice(overrides?.copy ?? exit.copy);
4802
5225
  }
4803
- return this.turnDone(exit.resultReason, overrides?.budget, overrides?.providerRefused);
5226
+ return turnDone(exit.resultReason, overrides?.budget, overrides?.providerRefused);
4804
5227
  }
4805
5228
  /**
4806
5229
  * Terminal budget outcome for a wall hit **outside** the tool loop — today,
@@ -4853,7 +5276,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4853
5276
  isSubAgent: this.isSubAgent
4854
5277
  });
4855
5278
  this.appendDriverNotice(this.budgetExhaustedBubble(e));
4856
- return this.turnDone("budget-exhausted", budgetDetailOf(e));
5279
+ return turnDone("budget-exhausted", budgetDetailOf(e));
4857
5280
  }
4858
5281
  /**
4859
5282
  * The sentence shown when the upstream provider refuses (GENC-1506).
@@ -4898,8 +5321,66 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4898
5321
  };
4899
5322
  }
4900
5323
  /**
4901
- * Swap in a new agent's configuration. Called by OrchestratingDriver before
4902
- * each specialist turn so the shared driver runs with the right tools and prompt.
5324
+ * Make `config` the active agent, firing its lifecycle hooks — `onDeactivate` on the outgoing
5325
+ * agent, then `onActivate` on this one — before installing it.
5326
+ *
5327
+ * This is the entry point a host should use. {@link ChatDriver.applyAgent} installs a config
5328
+ * *without* firing hooks, which is right for the two callers that need it (the orchestrator,
5329
+ * which fires them through the shared session, and `invokeSubAgent`, where a child's config is
5330
+ * not an activation) and wrong for everyone else.
5331
+ *
5332
+ * @beta
5333
+ */
5334
+ async activateAgent(config) {
5335
+ await this.agentSession.activate(config);
5336
+ this.applyAgent(config);
5337
+ }
5338
+ /**
5339
+ * {@inheritDoc AiDriver.getActiveAgent}
5340
+ *
5341
+ * @beta
5342
+ */
5343
+ getActiveAgent() {
5344
+ return this.agentSession.getActive();
5345
+ }
5346
+ /** {@inheritDoc AiDriver.primeRestoredAgentState} */
5347
+ primeRestoredAgentState(snapshots) {
5348
+ this.agentSession.primeRestoredStates(snapshots);
5349
+ }
5350
+ /**
5351
+ * Record the sticky agent pin. See {@link AiDriver.setPinnedAgent} for the contract.
5352
+ *
5353
+ * Recorded, not acted on: this driver has one agent and nothing to route between, so a pin
5354
+ * changes no behaviour here. It is still honoured as state, because the session outlives the
5355
+ * question of which driver is holding it — a host that pins before an orchestrator is built
5356
+ * would otherwise lose the pick.
5357
+ *
5358
+ * @beta
5359
+ */
5360
+ setPinnedAgent(name) {
5361
+ this.agentSession.setPinned(name);
5362
+ }
5363
+ /**
5364
+ * Record the transient flow lock. See {@link AiDriver.setFlowOwner} for the contract.
5365
+ *
5366
+ * Recorded, not acted on, for the same reason as {@link ChatDriver.setPinnedAgent}.
5367
+ *
5368
+ * @beta
5369
+ */
5370
+ setFlowOwner(name) {
5371
+ this.agentSession.setFlowOwner(name);
5372
+ }
5373
+ /**
5374
+ * Install `config` as the active agent **without firing lifecycle hooks**.
5375
+ *
5376
+ * Prefer {@link ChatDriver.activateAgent} unless you are deliberately skipping the hooks. Two
5377
+ * callers are: `OrchestratingDriver`, which fires them through the shared
5378
+ * {@link ChatDriver.agentSession} and then installs a decorated config, and `invokeSubAgent`,
5379
+ * where the child driver's config is a tool surface rather than an activation.
5380
+ *
5381
+ * Renamed to `installAgent` in a follow-up (GENC-1580) — see `docs/migration-GENC-1580.md`.
5382
+ * Until then the name says less than it should, so do not add a third caller to this path
5383
+ * without checking which of the two above it resembles.
4903
5384
  */
4904
5385
  applyAgent(config) {
4905
5386
  if (config.name !== this.activeAgentName) this.currentActivation += 1;
@@ -4938,8 +5419,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
4938
5419
  if (typeof config.provider === "string") {
4939
5420
  this.resolveProviderByName(config.provider, config.name);
4940
5421
  }
4941
- this.foldStack = [];
4942
- this.consecutiveFoldOps = 0;
5422
+ this.folds.reset();
4943
5423
  this.everSeenToolNames.clear();
4944
5424
  }
4945
5425
  /**
@@ -5056,15 +5536,6 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5056
5536
  this.resolvedStatusCache.set(name, status);
5057
5537
  return status;
5058
5538
  }
5059
- /**
5060
- * Resolve a per-turn config input that is either a static value or a function
5061
- * of the turn context — the value-or-resolver shape shared by `provider`,
5062
- * `temperature`, and `toolChoice`. Returns undefined when the input is unset.
5063
- */
5064
- async resolveTurnInput(input, ctx) {
5065
- if (input === void 0) return void 0;
5066
- return typeof input === "function" ? input(ctx) : input;
5067
- }
5068
5539
  /**
5069
5540
  * Returns the early-stop result set by `completeSubAgent`, if any.
5070
5541
  * Called by a parent `ChatDriver` after running this instance as a sub-agent.
@@ -5111,6 +5582,19 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5111
5582
  getAgentReleaseRequested() {
5112
5583
  return this.agentReleaseRequested;
5113
5584
  }
5585
+ /**
5586
+ * Act on a `releaseAgent` call from a tool handler: fire `onDeactivate` and drop the flow lock,
5587
+ * so the next turn starts clean and the persister does not serialize a flow that is over.
5588
+ *
5589
+ * No-op when the owner handles release ({@link ChatDriverConfig.deferAgentRelease}). The flag is
5590
+ * cleared here rather than only at the next `sendMessage`, so the signal cannot be acted on twice
5591
+ * and release a later agent that never asked.
5592
+ */
5593
+ async releaseIfRequested() {
5594
+ if (this.deferAgentRelease || !this.agentReleaseRequested) return;
5595
+ this.agentReleaseRequested = false;
5596
+ await this.agentSession.release();
5597
+ }
5114
5598
  /**
5115
5599
  * Return the per-turn snapshots captured so far. Used by the host's debug
5116
5600
  * log exporter to show what the LLM saw on each turn — system prompt, tool
@@ -5258,7 +5742,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5258
5742
  }
5259
5743
  /** Returns the current fold stack names for debugging. */
5260
5744
  getActiveFoldNames() {
5261
- return this.foldStack.map((f) => f.foldName);
5745
+ return this.folds.activeNames();
5262
5746
  }
5263
5747
  /**
5264
5748
  * The transcript as it should be **stored**: every tool payload the model has already stopped
@@ -5280,65 +5764,23 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5280
5764
  getHistoryForPersistence() {
5281
5765
  return condenseForPersistence([...this.history], this.condensePolicies);
5282
5766
  }
5767
+ /**
5768
+ * {@inheritDoc AiDriver.getSuggestions}
5769
+ *
5770
+ * @beta
5771
+ */
5283
5772
  async getSuggestions(history, prompt2, count, allAgentInfo) {
5284
- const defaultProvider = this.providerRegistry.default();
5285
- if (!defaultProvider.prompt) {
5286
- logger2.warn("ChatDriver: default AI provider does not implement prompt()");
5287
- return [];
5288
- }
5289
- let agentContext = "";
5290
- let toolContext = "";
5291
- if (allAgentInfo?.length) {
5292
- const agentDescriptions = allAgentInfo.map((agent) => {
5293
- const tools = agent.tools.map((t) => t.name).join(", ");
5294
- return `- ${agent.name} (${agent.description}): ${tools ? `(Tools: ${tools})` : "No tools"}`;
5295
- }).join("\n");
5296
- agentContext = `The assistant has the following capabilities:
5297
- ${agentDescriptions}`;
5298
- const allToolNames = allAgentInfo.flatMap((agent) => agent.tools.map((t) => t.name)).filter((value, index, self) => self.indexOf(value) === index).join(", ");
5299
- toolContext = allToolNames ? `You have access to the following tools across all agents: ${allToolNames}.` : "";
5300
- } else if (this.activeAgentName) {
5301
- const toolNames = this.toolDefinitions.map((tool) => tool.name).join(", ");
5302
- agentContext = `You are currently acting as the "${this.activeAgentName}" agent.`;
5303
- toolContext = toolNames ? `You have access to the following tools: ${toolNames}.` : "";
5304
- }
5305
- 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.";
5306
- const capabilitiesBlock = agentContext || toolContext ? `
5307
-
5308
- <capabilities>
5309
- ${[agentContext, toolContext].filter(Boolean).join("\n")}
5310
- </capabilities>
5311
- Suggestions must only cover what the agent is capable of. Do not suggest anything outside these capabilities.` : "";
5312
- const guidanceBlock = prompt2 ? `
5313
-
5314
- Additional guidance: "${prompt2}"` : "";
5315
- let userMessage;
5316
- if (history.length === 0) {
5317
- 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 + `
5318
-
5319
- Output format (strict):
5320
- - Exactly ${count} lines.
5321
- - One suggestion per line.
5322
- - No numbering, bullets, markdown, emojis, code blocks, or quotes around the suggestion.
5323
- - No preamble, headings, summary, or commentary before or after the list.`;
5324
- } else {
5325
- const conversationContext = history.filter(
5326
- (m) => (m.role === "user" || m.role === "assistant") && !m.toolCalls?.length && !m.thinking && !!m.content?.trim()
5327
- ).slice(-SUGGESTIONS_HISTORY_WINDOW).map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
5328
- userMessage = `<conversation_history>
5329
- ${conversationContext}
5330
- </conversation_history>
5331
-
5332
- 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 + `
5333
-
5334
- Output format (strict):
5335
- - Exactly ${count} lines.
5336
- - One suggestion per line.
5337
- - No numbering, bullets, markdown, emojis, code blocks, or quotes around the suggestion.
5338
- - No preamble, headings, summary, or commentary before or after the list.`;
5339
- }
5340
- const text = await defaultProvider.prompt(userMessage, { systemPrompt });
5341
- 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);
5773
+ return generateSuggestions(
5774
+ {
5775
+ registry: this.providerRegistry,
5776
+ activeAgentName: this.activeAgentName,
5777
+ toolDefinitions: this.toolDefinitions
5778
+ },
5779
+ history,
5780
+ prompt2,
5781
+ count,
5782
+ allAgentInfo
5783
+ );
5342
5784
  }
5343
5785
  /**
5344
5786
  * Whether `compact()` would do anything right now — a summarizable run of older
@@ -5716,7 +6158,7 @@ Output format (strict):
5716
6158
  this.appendDriverNotice(
5717
6159
  "Sorry, something went wrong on my end. Please try again in a moment."
5718
6160
  );
5719
- result = this.turnDone("exception");
6161
+ result = turnDone("exception");
5720
6162
  return result;
5721
6163
  } finally {
5722
6164
  recordMetaEvent(this.sessionKey, "turn.end", {
@@ -5727,6 +6169,7 @@ Output format (strict):
5727
6169
  this.busy = false;
5728
6170
  this.endTurn();
5729
6171
  this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
6172
+ await this.releaseIfRequested();
5730
6173
  }
5731
6174
  }
5732
6175
  /**
@@ -5980,7 +6423,7 @@ Output format (strict):
5980
6423
  this.appendDriverNotice(
5981
6424
  "Sorry, something went wrong on my end. Please try again in a moment."
5982
6425
  );
5983
- result = this.turnDone("exception");
6426
+ result = turnDone("exception");
5984
6427
  return result;
5985
6428
  } finally {
5986
6429
  recordMetaEvent(this.sessionKey, "turn.end", {
@@ -5991,136 +6434,10 @@ Output format (strict):
5991
6434
  this.busy = false;
5992
6435
  this.endTurn();
5993
6436
  this.activityBus.publish("tool-loop-end", this.loopEndDetail(result));
6437
+ await this.releaseIfRequested();
5994
6438
  }
5995
6439
  }
5996
6440
  // ---------------------------------------------------------------------------
5997
- // Fold mechanics
5998
- // ---------------------------------------------------------------------------
5999
- /** Extract ToolFold metadata from a handler, or undefined if it isn't a fold facade. */
6000
- getFold(toolName) {
6001
- const handler = this.toolHandlers[toolName];
6002
- return handler ? handler[TOOL_FOLD_SYMBOL] : void 0;
6003
- }
6004
- /**
6005
- * Search all currently registered fold facades (and their nested folds recursively)
6006
- * to find which fold contains a given tool name. Returns the immediate parent fold name.
6007
- */
6008
- findFoldContaining(toolName, handlers) {
6009
- const source = handlers ?? this.toolHandlers;
6010
- for (const [, handler] of Object.entries(source)) {
6011
- const fold = handler[TOOL_FOLD_SYMBOL];
6012
- if (!fold) continue;
6013
- if (fold.handlers[toolName]) return fold.name;
6014
- const nested = this.findFoldContaining(toolName, fold.handlers);
6015
- if (nested) return fold.name;
6016
- }
6017
- return null;
6018
- }
6019
- /**
6020
- * If an open fold is hiding a previously-available tool, return the name of
6021
- * the fold to close to start getting it back. Only exclusive folds hide tools
6022
- * (they replace the tool set on open rather than extending it), so a base tool
6023
- * that was visible before the fold opened now sits in a fold-stack frame's
6024
- * `previousHandlers` but not in the live handler map. Only the top fold's
6025
- * `close_` tool is active, so that's always the actionable next step — even
6026
- * when the tool lives further down the stack, closing repeatedly walks back to
6027
- * it. Returns null when no open fold accounts for the tool.
6028
- */
6029
- foldHidingTool(toolName) {
6030
- if (this.foldStack.length === 0) return null;
6031
- const hidden = this.foldStack.some((f) => f.previousHandlers[toolName]);
6032
- return hidden ? this.foldStack[this.foldStack.length - 1].foldName : null;
6033
- }
6034
- /**
6035
- * Install the fold's inner tool set, replacing (exclusive) or extending (non-exclusive)
6036
- * the current tool set. Also injects the close tool. Does NOT touch the fold stack.
6037
- */
6038
- applyFoldToolSet(fold, foldName) {
6039
- const closeToolName = `close_${foldName}`;
6040
- const newDefs = [];
6041
- const newHandlers = {};
6042
- if (!fold.exclusive) {
6043
- for (const def of this.toolDefinitions) {
6044
- if (def.name !== foldName) newDefs.push(def);
6045
- }
6046
- for (const [name, handler] of Object.entries(this.toolHandlers)) {
6047
- if (name !== foldName) newHandlers[name] = handler;
6048
- }
6049
- }
6050
- newDefs.push(...fold.tools);
6051
- Object.assign(newHandlers, fold.handlers);
6052
- newDefs.push({
6053
- name: closeToolName,
6054
- description: `Close the ${foldName} fold and return to the previous set of tools.`,
6055
- parameters: { type: "object", properties: {} }
6056
- });
6057
- newHandlers[closeToolName] = async () => this.closeFold();
6058
- this.toolDefinitions = newDefs;
6059
- this.toolHandlers = newHandlers;
6060
- }
6061
- /** Open a fold: push a stack frame, swap the tool set, return the response message. */
6062
- openFold(foldName, fold, args, activeToolCallId) {
6063
- for (const key of Object.keys(args)) {
6064
- const innerHandler = fold.handlers[key];
6065
- if (innerHandler) {
6066
- logger2.debug(`ChatDriver: fold shortcut dispatch "${foldName}" \u2192 "${key}"`);
6067
- this.pushFoldFrame(foldName);
6068
- this.applyFoldToolSet(fold, foldName);
6069
- this.consecutiveFoldOps = 0;
6070
- const innerArgs = typeof args[key] === "object" && args[key] !== null ? args[key] : {};
6071
- return innerHandler(innerArgs, this.buildHandlerContext(activeToolCallId)).then(
6072
- (r) => typeof r === "string" ? r : JSON.stringify(r)
6073
- );
6074
- }
6075
- }
6076
- this.pushFoldFrame(foldName);
6077
- this.applyFoldToolSet(fold, foldName);
6078
- const innerToolNames = fold.tools.map((t) => t.name);
6079
- const closeToolName = `close_${foldName}`;
6080
- let message = `Fold opened: ${foldName}. Tools now available: ${[...innerToolNames, closeToolName].join(", ")}.`;
6081
- if (fold.usageNotes) message += ` Notes: ${fold.usageNotes}`;
6082
- message += ` Call ${closeToolName} when done to return to the previous tools.`;
6083
- return Promise.resolve(message);
6084
- }
6085
- pushFoldFrame(foldName) {
6086
- this.foldStack.push({
6087
- foldName,
6088
- previousDefinitions: [...this.toolDefinitions],
6089
- previousHandlers: { ...this.toolHandlers }
6090
- });
6091
- }
6092
- /** Close the top fold: pop the stack frame, restore the previous tool set. */
6093
- closeFold() {
6094
- const frame = this.foldStack.pop();
6095
- if (!frame) return "No fold is currently open.";
6096
- this.toolDefinitions = frame.previousDefinitions;
6097
- this.toolHandlers = frame.previousHandlers;
6098
- const toolNames = this.toolDefinitions.map((t) => t.name);
6099
- return `Fold closed: ${frame.foldName}. Tools now available: ${toolNames.join(", ")}.`;
6100
- }
6101
- /** Build the fold-awareness suffix appended to the system prompt each LLM call. */
6102
- buildFoldSystemPromptSuffix() {
6103
- const activeFolds = [];
6104
- for (const handler of Object.values(this.toolHandlers)) {
6105
- const fold = handler[TOOL_FOLD_SYMBOL];
6106
- if (fold) activeFolds.push(fold);
6107
- }
6108
- if (activeFolds.length === 0 && this.foldStack.length === 0) return "";
6109
- const parts = ["\n\n--- Tool Folds ---"];
6110
- parts.push(
6111
- "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."
6112
- );
6113
- if (this.foldStack.length > 0) {
6114
- const current = this.foldStack[this.foldStack.length - 1];
6115
- parts.push(
6116
- `You are currently inside the "${current.foldName}" fold. Call close_${current.foldName} when you are done with these tools.`
6117
- );
6118
- }
6119
- for (const fold of activeFolds) {
6120
- parts.push(`\u2022 ${fold.name}: ${fold.description}`);
6121
- }
6122
- return parts.join("\n");
6123
- }
6124
6441
  // ---------------------------------------------------------------------------
6125
6442
  // Tool loop
6126
6443
  // ---------------------------------------------------------------------------
@@ -6215,7 +6532,7 @@ Output format (strict):
6215
6532
  if (typeof this.displayName === "function") {
6216
6533
  this.activeAgentLabel = await this.displayName(promptCtx);
6217
6534
  }
6218
- const foldSuffix = this.buildFoldSystemPromptSuffix();
6535
+ const foldSuffix = this.folds.systemPromptSuffix();
6219
6536
  const retryNudge = malformedAttempts > 0 ? `
6220
6537
 
6221
6538
  ${MALFORMED_CALL_NUDGE}` : emptyResponseAttempts > 0 ? `
@@ -6257,14 +6574,14 @@ ${EMPTY_RESPONSE_NUDGE}` : "";
6257
6574
  ] = (
6258
6575
  // oxlint-disable-next-line no-await-in-loop
6259
6576
  await Promise.all([
6260
- this.resolveTurnInput(this.activeTemperatureInput, promptCtx),
6261
- this.resolveTurnInput(this.activeToolChoiceInput, promptCtx),
6262
- this.resolveTurnInput(this.activeCachePolicyInput, promptCtx),
6263
- this.resolveTurnInput(this.activeTailContextInput, promptCtx),
6264
- this.resolveTurnInput(this.activeResponseSchemaInput, promptCtx),
6577
+ resolveTurnInput(this.activeTemperatureInput, promptCtx),
6578
+ resolveTurnInput(this.activeToolChoiceInput, promptCtx),
6579
+ resolveTurnInput(this.activeCachePolicyInput, promptCtx),
6580
+ resolveTurnInput(this.activeTailContextInput, promptCtx),
6581
+ resolveTurnInput(this.activeResponseSchemaInput, promptCtx),
6265
6582
  // Only consulted on the first iteration (see `pinnedThinkingPolicy`); resolved
6266
6583
  // alongside the others so a resolver still sees the same turn context.
6267
- thinkingPolicyPinned ? Promise.resolve(void 0) : this.resolveTurnInput(
6584
+ thinkingPolicyPinned ? Promise.resolve(void 0) : resolveTurnInput(
6268
6585
  this.activeThinkingPolicyInput,
6269
6586
  promptCtx
6270
6587
  )
@@ -6440,7 +6757,7 @@ ${tailBody}
6440
6757
  isSubAgent: true
6441
6758
  });
6442
6759
  this.failSubAgent("budget_exhausted", this.budgetWallDetail);
6443
- return this.turnDone("budget-exhausted", this.budgetWallDetail);
6760
+ return turnDone("budget-exhausted", this.budgetWallDetail);
6444
6761
  }
6445
6762
  return this.reportBudgetExhausted(e);
6446
6763
  }
@@ -6573,36 +6890,36 @@ ${tailBody}
6573
6890
  if (toolCalls.length > 0) {
6574
6891
  await Promise.all(
6575
6892
  toolCalls.map(async (tc) => {
6576
- const fold = this.getFold(tc.name);
6893
+ const fold = this.folds.foldFor(tc.name);
6577
6894
  if (fold) {
6578
- this.consecutiveFoldOps += 1;
6579
- if (this.consecutiveFoldOps > this.maxFoldOperations) {
6895
+ const foldOps = this.folds.noteOp();
6896
+ if (foldOps > this.maxFoldOperations) {
6580
6897
  logger2.warn(
6581
6898
  `ChatDriver: fold operation limit (${this.maxFoldOperations}) reached \u2014 injecting guidance`
6582
6899
  );
6583
6900
  executedById.set(tc.id, {
6584
6901
  toolCallId: tc.id,
6585
- 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.`
6902
+ 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.`
6586
6903
  });
6587
6904
  return;
6588
6905
  }
6589
- const content = await this.openFold(tc.name, fold, tc.args, tc.id);
6906
+ const content = await this.folds.open(tc.name, fold, tc.args, tc.id);
6590
6907
  executedById.set(tc.id, { toolCallId: tc.id, content });
6591
6908
  iterations -= 1;
6592
6909
  return;
6593
6910
  }
6594
- if (tc.name.startsWith("close_") && this.foldStack.length > 0) {
6595
- const topFoldName = this.foldStack[this.foldStack.length - 1].foldName;
6911
+ if (tc.name.startsWith("close_") && this.folds.depth > 0) {
6912
+ const topFoldName = this.folds.topName;
6596
6913
  if (tc.name === `close_${topFoldName}`) {
6597
- this.consecutiveFoldOps += 1;
6598
- if (this.consecutiveFoldOps > this.maxFoldOperations) {
6914
+ const foldOps = this.folds.noteOp();
6915
+ if (foldOps > this.maxFoldOperations) {
6599
6916
  executedById.set(tc.id, {
6600
6917
  toolCallId: tc.id,
6601
- 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.`
6918
+ 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.`
6602
6919
  });
6603
6920
  return;
6604
6921
  }
6605
- const content = this.closeFold();
6922
+ const content = this.folds.close();
6606
6923
  executedById.set(tc.id, { toolCallId: tc.id, content });
6607
6924
  iterations -= 1;
6608
6925
  return;
@@ -6610,7 +6927,7 @@ ${tailBody}
6610
6927
  }
6611
6928
  const handler = this.toolHandlers[tc.name];
6612
6929
  if (!handler) {
6613
- const containingFold = this.findFoldContaining(tc.name);
6930
+ const containingFold = this.folds.findContaining(tc.name);
6614
6931
  if (containingFold) {
6615
6932
  logger2.debug(
6616
6933
  `ChatDriver: model called folded tool "${tc.name}" \u2014 guiding to open "${containingFold}"`
@@ -6630,7 +6947,7 @@ ${tailBody}
6630
6947
  }
6631
6948
  if (this.everSeenToolNames.has(tc.name)) {
6632
6949
  this.consecutiveUnknownToolCalls += 1;
6633
- const hidingFold = this.foldHidingTool(tc.name);
6950
+ const hidingFold = this.folds.hidingTool(tc.name);
6634
6951
  let content;
6635
6952
  if (hidingFold) {
6636
6953
  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}.`;
@@ -6726,12 +7043,12 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6726
7043
  );
6727
7044
  }
6728
7045
  if (anyRealToolExecuted) {
6729
- this.consecutiveFoldOps = 0;
7046
+ this.folds.clearOps();
6730
7047
  this.consecutiveUnknownToolCalls = 0;
6731
7048
  this.recentUnknownToolNames.clear();
6732
7049
  this.recentStaleToolNames.clear();
6733
7050
  }
6734
- const foldPath = this.foldStack.map((f) => f.foldName);
7051
+ const foldPath = this.folds.activeNames();
6735
7052
  for (const tc of response.toolCalls) {
6736
7053
  if (tc.name === REQUEST_CONTINUATION_TOOL) {
6737
7054
  this.appendToHistory({
@@ -6768,7 +7085,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6768
7085
  const tcMsg = this.history[tcMsgIdx];
6769
7086
  const availableToolNames = Object.keys(this.toolHandlers);
6770
7087
  const annotatedCalls = tcMsg.toolCalls.map((tc) => {
6771
- const isFoldOpen = !!this.getFold(tc.name) || // Was a fold facade at time of the call (now the tool set has changed)
7088
+ const isFoldOpen = !!this.folds.foldFor(tc.name) || // Was a fold facade at time of the call (now the tool set has changed)
6772
7089
  // — detect by checking if the result message indicated a fold open
6773
7090
  executedById.get(tc.id)?.content?.startsWith("Fold opened:");
6774
7091
  const isFoldClose = executedById.get(tc.id)?.content?.startsWith("Fold closed:");
@@ -6827,7 +7144,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6827
7144
  return { reason: "agent-handoff", summary, remainingTask };
6828
7145
  }
6829
7146
  if (this.subAgentCompletion) {
6830
- return this.turnDone();
7147
+ return turnDone();
6831
7148
  }
6832
7149
  }
6833
7150
  if (iterations >= this.maxToolIterations) {
@@ -6841,7 +7158,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
6841
7158
  });
6842
7159
  return this.endTurnWithFailure(TURN_FAILURE_EXITS.maxIterations);
6843
7160
  }
6844
- return this.turnDone();
7161
+ return turnDone();
6845
7162
  }
6846
7163
  /**
6847
7164
  * Append prose the DRIVER wrote rather than the model — a failure notice, a limit explanation,
@@ -6949,11 +7266,6 @@ var OrchestratingDriver = class extends EventTarget {
6949
7266
  super();
6950
7267
  this.providerRegistry = providerRegistry;
6951
7268
  this.agents = agents;
6952
- /**
6953
- * Aborted on driver disposal. Threaded into `AgentLifecycleContext.signal`
6954
- * so long-running `onActivate` work can bail if the session disconnects.
6955
- */
6956
- this.lifecycleAbortController = new AbortController();
6957
7269
  /**
6958
7270
  * Set by `cancel()` for the duration of a `sendMessage` run. Breaks the
6959
7271
  * handoff loop so we don't classify or start another agent turn after the
@@ -6971,28 +7283,6 @@ var OrchestratingDriver = class extends EventTarget {
6971
7283
  * made it vanish. Reset at the top of each `runOrchestratedTurn`.
6972
7284
  */
6973
7285
  this.userMessageAppended = false;
6974
- /**
6975
- * Sticky user pick from the picker (or the host's `setAgent` API). Only
6976
- * changes on explicit user action. Survives flow completion: when a stateful
6977
- * agent releases, the user-pin stays put and routing falls back to it on
6978
- * the next turn.
6979
- */
6980
- this.pinnedAgentName = null;
6981
- /**
6982
- * Transient flow lock — set when a stateful agent's `onActivate` fires,
6983
- * cleared by `releaseActiveAgent`. While non-null, routing precedence goes
6984
- * to this agent and the picker is locked. Independent of
6985
- * `pinnedAgentName` so completing a flow doesn't wipe the user's sticky
6986
- * choice.
6987
- */
6988
- this.flowOwnerAgentName = null;
6989
- /**
6990
- * Per-agent restored state for a rehydrated session, keyed by agent name.
6991
- * Primed by the host on restore; each entry is passed to that agent's
6992
- * `onActivate` as `ctx.restored` on its next activation, then deleted so a
6993
- * later re-activation starts fresh.
6994
- */
6995
- this.restoredAgentStates = {};
6996
7286
  this.sessionKey = options.sessionKey ?? "";
6997
7287
  this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS;
6998
7288
  this.classifierHistoryLength = options.classifierHistoryLength ?? DEFAULT_CLASSIFIER_HISTORY_LENGTH;
@@ -7013,7 +7303,9 @@ var OrchestratingDriver = class extends EventTarget {
7013
7303
  maxTurnSnapshots: options.maxTurnSnapshots,
7014
7304
  sessionKey: this.sessionKey,
7015
7305
  activityBus: options.activityBus,
7016
- budgetExhaustedMessage: options.budgetExhaustedMessage
7306
+ budgetExhaustedMessage: options.budgetExhaustedMessage,
7307
+ // This driver releases after the turn so it can fire the host-facing events; see the option.
7308
+ deferAgentRelease: true
7017
7309
  });
7018
7310
  this.chatDriver.addEventListener("history-updated", (e) => {
7019
7311
  this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
@@ -7050,6 +7342,28 @@ var OrchestratingDriver = class extends EventTarget {
7050
7342
  );
7051
7343
  });
7052
7344
  }
7345
+ /**
7346
+ * The stateful-agent contract (§7.1): active agent, user pin, flow lock, restored state, and
7347
+ * the `onActivate` / `onDeactivate` hooks.
7348
+ *
7349
+ * **The inner driver's session, not one of our own.** One session is the invariant: two would
7350
+ * let the outer and inner driver disagree about which agent is active, and the inner one would
7351
+ * never be activated at all, because this driver installs a *decorated* config through
7352
+ * `applyAgent` rather than going via `ChatDriver.activateAgent`. This driver keeps the routing
7353
+ * decisions; the session owns the lifecycle.
7354
+ */
7355
+ get agentSession() {
7356
+ return this.chatDriver.agentSession;
7357
+ }
7358
+ /**
7359
+ * The active agent, or `undefined` before the first activation and after a release.
7360
+ *
7361
+ * Kept as an accessor over `agentSession` rather than removed: it is a public member of an
7362
+ * exported class, so nothing in this package reading it is not the same as nothing reading it.
7363
+ */
7364
+ get activeAgent() {
7365
+ return this.agentSession.getActive();
7366
+ }
7053
7367
  resolveInteraction(interactionId, result) {
7054
7368
  this.chatDriver.resolveInteraction(interactionId, result);
7055
7369
  }
@@ -7075,7 +7389,7 @@ var OrchestratingDriver = class extends EventTarget {
7075
7389
  * return to automatic routing.
7076
7390
  */
7077
7391
  setPinnedAgent(name) {
7078
- this.pinnedAgentName = name;
7392
+ this.agentSession.setPinned(name);
7079
7393
  }
7080
7394
  /**
7081
7395
  * Set the flow-owner lock — used to restore state from Redux on driver
@@ -7084,7 +7398,7 @@ var OrchestratingDriver = class extends EventTarget {
7084
7398
  * for.
7085
7399
  */
7086
7400
  setFlowOwner(name) {
7087
- this.flowOwnerAgentName = name;
7401
+ this.agentSession.setFlowOwner(name);
7088
7402
  }
7089
7403
  loadHistory(messages) {
7090
7404
  this.chatDriver.loadHistory(messages);
@@ -7243,7 +7557,7 @@ var OrchestratingDriver = class extends EventTarget {
7243
7557
  }
7244
7558
  /** {@inheritDoc AiDriver.primeRestoredAgentState} */
7245
7559
  primeRestoredAgentState(snapshots) {
7246
- this.restoredAgentStates = { ...snapshots };
7560
+ this.agentSession.primeRestoredStates(snapshots);
7247
7561
  }
7248
7562
  /**
7249
7563
  * {@inheritDoc AiDriver.getContextReserve}
@@ -7273,51 +7587,14 @@ var OrchestratingDriver = class extends EventTarget {
7273
7587
  }
7274
7588
  /** {@inheritDoc AiDriver.getActiveAgent} */
7275
7589
  getActiveAgent() {
7276
- return this.activeAgent;
7590
+ return this.agentSession.getActive();
7277
7591
  }
7278
7592
  async applyAgent(agent) {
7279
- const previousAgent = this.activeAgent;
7280
- const isSwitch = !previousAgent || previousAgent.name !== agent.name;
7281
- if (isSwitch) {
7282
- recordMetaEvent(this.sessionKey, "agent.handoff", {
7283
- from: previousAgent?.name ?? null,
7284
- to: agent.name,
7285
- // GENC-1351: was a restored snapshot waiting for this agent at activation?
7286
- restored: this.restoredAgentStates[agent.name] !== void 0
7287
- });
7593
+ const { previousAgent, flowOwnerChangedTo } = await this.agentSession.activate(agent);
7594
+ if (flowOwnerChangedTo !== void 0) {
7595
+ this.dispatchEvent(new CustomEvent("flow-owner-changed", { detail: flowOwnerChangedTo }));
7288
7596
  }
7289
- if (isSwitch && previousAgent?.onDeactivate) {
7290
- try {
7291
- await previousAgent.onDeactivate({
7292
- agentName: previousAgent.name,
7293
- sessionKey: this.sessionKey,
7294
- signal: this.lifecycleAbortController.signal
7295
- });
7296
- } catch (e) {
7297
- logger2.warn(`OrchestratingDriver: onDeactivate("${previousAgent.name}") threw:`, e);
7298
- }
7299
- }
7300
- if (isSwitch && agent.onActivate) {
7301
- const restored = this.restoredAgentStates[agent.name];
7302
- if (restored !== void 0) delete this.restoredAgentStates[agent.name];
7303
- try {
7304
- await agent.onActivate({
7305
- agentName: agent.name,
7306
- sessionKey: this.sessionKey,
7307
- previousAgentName: previousAgent?.name,
7308
- signal: this.lifecycleAbortController.signal,
7309
- restored
7310
- });
7311
- } catch (e) {
7312
- logger2.warn(`OrchestratingDriver: onActivate("${agent.name}") threw:`, e);
7313
- }
7314
- }
7315
- const hasLifecycleHooks = !!(agent.onActivate || agent.onDeactivate);
7316
- if (isSwitch && hasLifecycleHooks && this.flowOwnerAgentName !== agent.name) {
7317
- this.flowOwnerAgentName = agent.name;
7318
- this.dispatchEvent(new CustomEvent("flow-owner-changed", { detail: agent.name }));
7319
- }
7320
- const isTerminal = isFallback(agent) || this.pinnedAgentName !== null || this.flowOwnerAgentName !== null || hasLifecycleHooks;
7597
+ const isTerminal = isFallback(agent) || this.agentSession.getPinned() !== null || this.agentSession.getFlowOwner() !== null || !!(agent.onActivate || agent.onDeactivate);
7321
7598
  let agentToApply = agent;
7322
7599
  if (!isTerminal) {
7323
7600
  const declaredTools = agent.toolDefinitions;
@@ -7338,7 +7615,6 @@ var OrchestratingDriver = class extends EventTarget {
7338
7615
  }
7339
7616
  this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
7340
7617
  this.chatDriver.applyAgent(agentToApply);
7341
- this.activeAgent = agent;
7342
7618
  this.dispatchEvent(new CustomEvent("orchestrating-stop"));
7343
7619
  this.dispatchEvent(new CustomEvent("agent-changed", { detail: agent }));
7344
7620
  }
@@ -7354,25 +7630,14 @@ var OrchestratingDriver = class extends EventTarget {
7354
7630
  * routing through the classifier.
7355
7631
  */
7356
7632
  async releaseActiveAgent() {
7357
- const agent = this.activeAgent;
7358
- if (!agent) return;
7359
- if (agent.onDeactivate) {
7360
- try {
7361
- await agent.onDeactivate({
7362
- agentName: agent.name,
7363
- sessionKey: this.sessionKey,
7364
- signal: this.lifecycleAbortController.signal
7365
- });
7366
- } catch (e) {
7367
- logger2.warn(`OrchestratingDriver: release onDeactivate("${agent.name}") threw:`, e);
7368
- }
7369
- }
7370
- this.activeAgent = void 0;
7371
- if (this.flowOwnerAgentName !== null) {
7372
- this.flowOwnerAgentName = null;
7373
- this.dispatchEvent(new CustomEvent("flow-owner-changed", { detail: null }));
7633
+ const released = await this.agentSession.release();
7634
+ if (!released) return;
7635
+ if (released.flowOwnerChangedTo !== void 0) {
7636
+ this.dispatchEvent(
7637
+ new CustomEvent("flow-owner-changed", { detail: released.flowOwnerChangedTo })
7638
+ );
7374
7639
  }
7375
- this.dispatchEvent(new CustomEvent("agent-released", { detail: agent }));
7640
+ this.dispatchEvent(new CustomEvent("agent-released", { detail: released.previousAgent }));
7376
7641
  this.dispatchEvent(new CustomEvent("agent-changed", { detail: void 0 }));
7377
7642
  }
7378
7643
  /**
@@ -7390,21 +7655,8 @@ var OrchestratingDriver = class extends EventTarget {
7390
7655
  * release resources cleanly.
7391
7656
  */
7392
7657
  async dispose() {
7393
- const previousAgent = this.activeAgent;
7394
- if (previousAgent?.onDeactivate) {
7395
- try {
7396
- await previousAgent.onDeactivate({
7397
- agentName: previousAgent.name,
7398
- sessionKey: this.sessionKey,
7399
- signal: this.lifecycleAbortController.signal
7400
- });
7401
- } catch (e) {
7402
- logger2.warn(`OrchestratingDriver: dispose onDeactivate("${previousAgent.name}") threw:`, e);
7403
- }
7404
- }
7405
- this.lifecycleAbortController.abort();
7406
- this.chatDriver.dispose();
7407
- this.activeAgent = void 0;
7658
+ await this.agentSession.dispose();
7659
+ await this.chatDriver.dispose();
7408
7660
  }
7409
7661
  /**
7410
7662
  * Record one routing decision on the debug timeline — what was chosen, why, what it cost, and
@@ -7592,7 +7844,7 @@ ${recentMessages}` : ""}`;
7592
7844
  * — protects against stale state after an agents-array reassignment.
7593
7845
  */
7594
7846
  resolvePinnedAgent() {
7595
- const name = this.flowOwnerAgentName ?? this.pinnedAgentName;
7847
+ const name = this.agentSession.getLockedName();
7596
7848
  if (name === null) return void 0;
7597
7849
  const match = this.agents.find((a) => a.name === name);
7598
7850
  if (!match) {