@genesislcap/ai-assistant 15.58.1 → 15.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/ai-assistant.api.json +10096 -6986
  2. package/dist/ai-assistant.d.ts +545 -101
  3. package/dist/chat-driver.cjs +624 -207
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +624 -207
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +1836 -378
  8. package/dist/dts/components/agent-session/agent-session.d.ts +19 -3
  9. package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -1
  10. package/dist/dts/components/ai-driver/ai-driver.d.ts +120 -10
  11. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  12. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +103 -0
  13. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -0
  14. package/dist/dts/components/ai-driver/index.d.ts +3 -1
  15. package/dist/dts/components/ai-driver/index.d.ts.map +1 -1
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts +84 -8
  17. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  18. package/dist/dts/components/chat-driver/interaction-controller.d.ts +37 -3
  19. package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +30 -93
  21. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  22. package/dist/dts/index.d.ts +4 -1
  23. package/dist/dts/index.d.ts.map +1 -1
  24. package/dist/dts/main/main.d.ts +4 -4
  25. package/dist/dts/main/main.d.ts.map +1 -1
  26. package/dist/dts/state/debug-event-log.d.ts +1 -1
  27. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  28. package/dist/dts/state/interaction-context.d.ts +7 -1
  29. package/dist/dts/state/interaction-context.d.ts.map +1 -1
  30. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  31. package/dist/dts/suggestions/generate-suggestions.d.ts.map +1 -1
  32. package/dist/dts/types/handler-capability.d.ts +28 -0
  33. package/dist/dts/types/handler-capability.d.ts.map +1 -0
  34. package/dist/dts/types/interaction-context.d.ts +16 -0
  35. package/dist/dts/types/interaction-context.d.ts.map +1 -1
  36. package/dist/dts/types/interaction-interceptor.d.ts +109 -0
  37. package/dist/dts/types/interaction-interceptor.d.ts.map +1 -0
  38. package/dist/dts/utils/delegated-message.d.ts +52 -0
  39. package/dist/dts/utils/delegated-message.d.ts.map +1 -0
  40. package/dist/dts/utils/history-transform.d.ts.map +1 -1
  41. package/dist/esm/components/agent-session/agent-session.js +17 -0
  42. package/dist/esm/components/ai-driver/ai-driver.js +21 -1
  43. package/dist/esm/components/ai-driver/base-driver-decorator.js +211 -0
  44. package/dist/esm/components/ai-driver/index.js +2 -1
  45. package/dist/esm/components/chat-driver/chat-driver.js +195 -14
  46. package/dist/esm/components/chat-driver/interaction-controller.js +168 -14
  47. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +72 -196
  48. package/dist/esm/index.js +1 -0
  49. package/dist/esm/main/main.js +79 -77
  50. package/dist/esm/state/debug-event-log.js +7 -2
  51. package/dist/esm/state/interaction-context.js +11 -0
  52. package/dist/esm/state/persistence/session-persister.js +5 -6
  53. package/dist/esm/suggestions/generate-suggestions.js +8 -1
  54. package/dist/esm/types/handler-capability.js +1 -0
  55. package/dist/esm/types/interaction-interceptor.js +1 -0
  56. package/dist/esm/utils/delegated-message.js +25 -0
  57. package/dist/esm/utils/history-transform.js +12 -1
  58. package/package.json +17 -17
@@ -4027,6 +4027,10 @@ var META_EVENT_IMPORTANCE = {
4027
4027
  "interaction.outcome": "normal",
4028
4028
  // An outcome that could not be recorded: the widget goes on showing only the answer.
4029
4029
  "interaction.outcome-dropped": "high",
4030
+ // A claimant handing a pending interaction to the person, or taking it back.
4031
+ "interaction.held": "normal",
4032
+ // A claim on a `userOnly` interaction, refused. Expected when an interceptor claims broadly.
4033
+ "interaction.claim-refused": "low",
4030
4034
  "external-diagnostics.folded": "normal",
4031
4035
  "session.cleared": "normal",
4032
4036
  "session.switched": "low",
@@ -4238,6 +4242,14 @@ function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKE
4238
4242
  };
4239
4243
  }
4240
4244
 
4245
+ // src/utils/delegated-message.ts
4246
+ var DEFAULT_DELEGATE_FRAMING = "Sent on the user's behalf.";
4247
+ function frameDelegatedContent(content, framing) {
4248
+ return `(${framing ?? DEFAULT_DELEGATE_FRAMING})
4249
+
4250
+ ${content}`;
4251
+ }
4252
+
4241
4253
  // src/utils/logger.ts
4242
4254
  var logger2 = createLogger("ai-assistant");
4243
4255
 
@@ -4289,7 +4301,9 @@ Additional guidance: "${prompt2}"` : "";
4289
4301
  }
4290
4302
  const conversationContext = history.filter(
4291
4303
  (m) => reachesProvider(m) && (m.role === "user" || m.role === "assistant") && !m.toolCalls?.length && !m.thinking && !!m.content?.trim()
4292
- ).slice(-HISTORY_WINDOW).map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n");
4304
+ ).slice(-HISTORY_WINDOW).map(
4305
+ (m) => m.role === "user" ? `User: ${m.delegate ? frameDelegatedContent(m.content ?? "", m.delegateFraming) : m.content}` : `Assistant: ${m.content}`
4306
+ ).join("\n");
4293
4307
  return `<conversation_history>
4294
4308
  ${conversationContext}
4295
4309
  </conversation_history>
@@ -4679,7 +4693,8 @@ function renderMessageForSummary(m) {
4679
4693
  case "compacted-summary":
4680
4694
  return `[Earlier summary] ${m.content}`;
4681
4695
  case "user":
4682
- return m.content ? `User: ${m.content}` : "";
4696
+ if (!m.content) return "";
4697
+ return `User: ${m.delegate ? frameDelegatedContent(m.content, m.delegateFraming) : m.content}`;
4683
4698
  case "assistant": {
4684
4699
  if (m.category === "reasoning") {
4685
4700
  return m.content ? `Thinking: ${m.content}` : "";
@@ -4711,6 +4726,9 @@ Summarize the conversation above so it can be dropped from context while preserv
4711
4726
  }
4712
4727
  function normalizeForProvider(history) {
4713
4728
  return history.map((m) => {
4729
+ if (m.role === "user" && m.delegate) {
4730
+ return { ...m, content: frameDelegatedContent(m.content ?? "", m.delegateFraming) };
4731
+ }
4714
4732
  if (m.role !== "compacted-summary") return m;
4715
4733
  return {
4716
4734
  ...m,
@@ -4925,6 +4943,7 @@ var AgentSession = class {
4925
4943
  transition.flowOwnerChangedTo = agent.name;
4926
4944
  }
4927
4945
  this.active = agent;
4946
+ if (switched) this.reportTransition(transition);
4928
4947
  return transition;
4929
4948
  }
4930
4949
  /**
@@ -4946,8 +4965,21 @@ var AgentSession = class {
4946
4965
  this.flowOwner = null;
4947
4966
  transition.flowOwnerChangedTo = null;
4948
4967
  }
4968
+ this.reportTransition(transition);
4949
4969
  return transition;
4950
4970
  }
4971
+ /**
4972
+ * Hand a transition to the owner. A throwing listener is logged rather than propagated, for the
4973
+ * same reason a throwing hook is: the transition has already happened, and failing the caller
4974
+ * would leave it believing it had not.
4975
+ */
4976
+ reportTransition(transition) {
4977
+ try {
4978
+ this.deps.onTransition?.(transition);
4979
+ } catch (e) {
4980
+ logger2.warn(`${WARN_PREFIX}: onTransition threw:`, e);
4981
+ }
4982
+ }
4951
4983
  /**
4952
4984
  * Fire `onDeactivate` on the active agent and abort pending lifecycle work, for session
4953
4985
  * teardown, so machines release resources cleanly. The session is not reusable afterwards: the
@@ -5173,8 +5205,12 @@ function createInteractionContext(interactionId) {
5173
5205
  const listeners = /* @__PURE__ */ new Set();
5174
5206
  const disposers = /* @__PURE__ */ new Set();
5175
5207
  let disposed = false;
5208
+ let heldBy = "user";
5176
5209
  const context = {
5177
5210
  interactionId,
5211
+ get heldBy() {
5212
+ return heldBy;
5213
+ },
5178
5214
  state,
5179
5215
  once(key, factory) {
5180
5216
  if (onceCache.has(key)) return onceCache.get(key);
@@ -5226,11 +5262,18 @@ function createInteractionContext(interactionId) {
5226
5262
  disposers.clear();
5227
5263
  listeners.clear();
5228
5264
  onceCache.clear();
5265
+ },
5266
+ setHeldBy(holder) {
5267
+ if (disposed || holder === heldBy) return false;
5268
+ heldBy = holder;
5269
+ context.notify();
5270
+ return true;
5229
5271
  }
5230
5272
  };
5231
5273
  }
5232
5274
 
5233
5275
  // src/components/chat-driver/interaction-controller.ts
5276
+ var DEFAULT_DECISION_FRAMING = "Decided on the user's behalf";
5234
5277
  var STOPPED = {
5235
5278
  status: "timeout",
5236
5279
  message: "The turn was stopped before this was answered."
@@ -5248,6 +5291,94 @@ var InteractionController = class {
5248
5291
  * one-interaction-at-a-time guard in `request`).
5249
5292
  */
5250
5293
  this.contexts = /* @__PURE__ */ new Map();
5294
+ /**
5295
+ * Told about every interaction this controller opens (GENC-1660), in registration order. Only
5296
+ * the root's are ever consulted: a sub-agent's request is opened by the root.
5297
+ */
5298
+ this.interceptors = [];
5299
+ }
5300
+ /** See `ChatDriver.addInteractionInterceptor`. Returns a remover. */
5301
+ addInterceptor(interceptor) {
5302
+ if (!interceptor.id || interceptor.id === "user") {
5303
+ throw new Error(
5304
+ `addInteractionInterceptor: an interceptor needs an id other than 'user' (got "${interceptor.id}").`
5305
+ );
5306
+ }
5307
+ this.interceptors.push(interceptor);
5308
+ return () => {
5309
+ const index = this.interceptors.indexOf(interceptor);
5310
+ if (index >= 0) this.interceptors.splice(index, 1);
5311
+ };
5312
+ }
5313
+ /**
5314
+ * Offer a new interaction to the interceptors and record who ends up holding it.
5315
+ *
5316
+ * **Outermost first.** A decorator installs its interceptor on the driver it wraps when it is
5317
+ * constructed, and decorators are constructed innermost first, so registration order is
5318
+ * innermost first and the walk runs it backwards. The first claim wins; later interceptors still
5319
+ * see the interaction but their `hold` does nothing. A claim on a `userOnly` interaction is
5320
+ * refused.
5321
+ *
5322
+ * Runs before the interaction's message is appended, so the widget's first render already knows
5323
+ * its holder and nothing appears only to disappear.
5324
+ */
5325
+ offer(handle, opened) {
5326
+ let claimant;
5327
+ for (const interceptor of [...this.interceptors].reverse()) {
5328
+ const hold = this.holdFor(handle, interceptor, () => claimant === interceptor);
5329
+ let answer;
5330
+ try {
5331
+ answer = interceptor.onInteractionOpened({ ...opened, ctx: handle.context, hold });
5332
+ } catch (e) {
5333
+ logger2.warn(`InteractionController: interceptor "${interceptor.id}" threw:`, e);
5334
+ continue;
5335
+ }
5336
+ if (answer !== "claim" || claimant) continue;
5337
+ if (opened.userOnly) {
5338
+ recordMetaEvent(this.deps.sessionKey, "interaction.claim-refused", {
5339
+ interactionId: opened.interactionId,
5340
+ claimant: interceptor.id,
5341
+ reason: "userOnly"
5342
+ });
5343
+ continue;
5344
+ }
5345
+ claimant = interceptor;
5346
+ handle.setHeldBy(interceptor.id);
5347
+ const pending = this.pending.get(opened.interactionId);
5348
+ if (pending) pending.claimant = interceptor;
5349
+ }
5350
+ return handle.context.heldBy;
5351
+ }
5352
+ /** The {@link InteractionHold} for one interceptor: it applies only while that one is the claimant. */
5353
+ holdFor(handle, interceptor, isClaimant) {
5354
+ return {
5355
+ set: (holder) => {
5356
+ if (!isClaimant()) return false;
5357
+ const next = holder === "user" ? "user" : interceptor.id;
5358
+ if (!handle.setHeldBy(next)) return false;
5359
+ recordMetaEvent(this.deps.sessionKey, "interaction.held", {
5360
+ interactionId: handle.context.interactionId,
5361
+ heldBy: next,
5362
+ claimant: interceptor.id
5363
+ });
5364
+ return true;
5365
+ },
5366
+ resolve: (result) => {
5367
+ const { interactionId } = handle.context;
5368
+ const pending = this.pending.get(interactionId);
5369
+ if (!isClaimant() || !pending || handle.context.heldBy !== interceptor.id) return false;
5370
+ pending.settledBy = interceptor.id;
5371
+ this.resolve(interactionId, result);
5372
+ return true;
5373
+ },
5374
+ recordDecision: (decision) => {
5375
+ if (!isClaimant()) return false;
5376
+ const pending = this.pending.get(handle.context.interactionId);
5377
+ if (!pending) return false;
5378
+ pending.decision = { choice: decision.choice, shownToUser: decision.shownToUser };
5379
+ return true;
5380
+ }
5381
+ };
5251
5382
  }
5252
5383
  /** See `ChatDriver.setHostInteractionRequester`. */
5253
5384
  setHostRequester(fn, recordOutcome) {
@@ -5257,10 +5388,14 @@ var InteractionController = class {
5257
5388
  /**
5258
5389
  * See `ChatDriver.requestInteraction`. `onOpened` is told the id of the interaction this call
5259
5390
  * opens, wherever it opens it, so the caller can later record its outcome and no other's.
5391
+ * `innerPath` is the owner path a sub-agent's request has gathered on its way up, and
5392
+ * `onSettled` is told who answered, wherever it resolves (GENC-1660).
5260
5393
  */
5261
- request(componentName, data, options, onOpened) {
5394
+ request(componentName, data, options, onOpened, innerPath = [], onSettled) {
5395
+ const agent = this.deps.activeAgentName();
5396
+ const ownerPath = agent ? [agent, ...innerPath] : [...innerPath];
5262
5397
  if (this.hostRequester) {
5263
- return this.hostRequester(componentName, data, options, onOpened);
5398
+ return this.hostRequester(componentName, data, options, onOpened, ownerPath, onSettled);
5264
5399
  }
5265
5400
  if (this.pending.size > 0) {
5266
5401
  throw new Error(
@@ -5276,6 +5411,7 @@ var InteractionController = class {
5276
5411
  this.pending.set(interactionId, {
5277
5412
  resolve,
5278
5413
  reject,
5414
+ onSettled,
5279
5415
  overrideId: chatInputDuringExecution ? interactionId : void 0,
5280
5416
  // Resolve the interaction with a timeout result if the user does not
5281
5417
  // respond in time. `resolve` runs the same teardown a normal resolution
@@ -5288,10 +5424,22 @@ var InteractionController = class {
5288
5424
  });
5289
5425
  }, timeoutMs) : void 0
5290
5426
  });
5427
+ const handle = createInteractionContext(interactionId);
5428
+ this.contexts.set(interactionId, handle);
5429
+ const heldBy = this.offer(handle, {
5430
+ interactionId,
5431
+ componentName,
5432
+ data,
5433
+ ownerPath,
5434
+ userOnly: options?.userOnly === true
5435
+ });
5291
5436
  recordMetaEvent(this.deps.sessionKey, "interaction.requested", {
5292
5437
  interactionId,
5293
5438
  component: componentName,
5294
- agent: this.deps.activeAgentName()
5439
+ agent: this.deps.activeAgentName(),
5440
+ ownerPath,
5441
+ heldBy,
5442
+ ...options?.userOnly ? { userOnly: true } : {}
5295
5443
  });
5296
5444
  this.deps.activityBus.publish("interaction-requested", void 0);
5297
5445
  if (chatInputDuringExecution) {
@@ -5301,7 +5449,6 @@ var InteractionController = class {
5301
5449
  })
5302
5450
  );
5303
5451
  }
5304
- this.contexts.set(interactionId, createInteractionContext(interactionId));
5305
5452
  this.deps.appendToHistory({
5306
5453
  role: "assistant",
5307
5454
  content: "",
@@ -5312,6 +5459,11 @@ var InteractionController = class {
5312
5459
  ...presentation ? { presentation } : {}
5313
5460
  }
5314
5461
  });
5462
+ const opened = this.pending.get(interactionId);
5463
+ if (opened) {
5464
+ opened.appended = true;
5465
+ if (opened.early) this.resolve(interactionId, opened.early.result);
5466
+ }
5315
5467
  const signal = options?.signal;
5316
5468
  if (signal) {
5317
5469
  const onAbort = () => this.resolve(interactionId, STOPPED);
@@ -5337,6 +5489,10 @@ var InteractionController = class {
5337
5489
  logger2.warn(`Interaction with ID ${interactionId} not found.`);
5338
5490
  return;
5339
5491
  }
5492
+ if (!interaction.appended) {
5493
+ interaction.early ??= { result };
5494
+ return;
5495
+ }
5340
5496
  if (interaction.timeoutHandle) clearTimeout(interaction.timeoutHandle);
5341
5497
  interaction.unlinkSignal?.();
5342
5498
  this.deps.foldDiagnostics(
@@ -5344,20 +5500,50 @@ var InteractionController = class {
5344
5500
  result?.diagnostics
5345
5501
  );
5346
5502
  const costUsd = typeof result?.costUsd === "number" && Number.isFinite(result.costUsd) && result.costUsd > 0 ? result.costUsd : void 0;
5503
+ const unanswered = result?.status === "timeout";
5504
+ const answeredBy = unanswered ? void 0 : interaction.settledBy ?? "user";
5505
+ const decision = answeredBy && answeredBy !== "user" ? {
5506
+ framing: interaction.claimant?.decisionFraming ?? DEFAULT_DECISION_FRAMING,
5507
+ ...interaction.decision?.choice !== void 0 && {
5508
+ choice: interaction.decision.choice
5509
+ },
5510
+ ...interaction.decision?.shownToUser !== void 0 && {
5511
+ shownToUser: interaction.decision.shownToUser
5512
+ }
5513
+ } : void 0;
5347
5514
  const patched = this.deps.patchInteractionMessage(interactionId, (message) => ({
5348
5515
  ...message,
5349
- interaction: { ...message.interaction, resolved: result },
5516
+ interaction: {
5517
+ ...message.interaction,
5518
+ resolved: result,
5519
+ ...answeredBy ? { answeredBy } : {}
5520
+ },
5350
5521
  ...costUsd != null ? { externalCostUsd: (message.externalCostUsd ?? 0) + costUsd } : {}
5351
5522
  }));
5352
5523
  if (patched) this.deps.notifyHistoryUpdated();
5353
5524
  if (typeof result?.userMessage === "string" && result.userMessage.trim()) {
5354
- this.deps.appendToHistory({ role: "synthetic-user", content: result.userMessage });
5525
+ this.deps.appendToHistory(
5526
+ answeredBy && answeredBy !== "user" ? {
5527
+ role: "assistant",
5528
+ content: result.userMessage,
5529
+ delegate: answeredBy,
5530
+ excludeFromProvider: true
5531
+ } : { role: "synthetic-user", content: result.userMessage }
5532
+ );
5355
5533
  }
5356
5534
  if (interaction.overrideId) {
5357
5535
  this.deps.emit(new CustomEvent("interaction-stop", { detail: { interactionId } }));
5358
5536
  }
5359
- recordMetaEvent(this.deps.sessionKey, "interaction.resolved", { interactionId });
5537
+ recordMetaEvent(this.deps.sessionKey, "interaction.resolved", {
5538
+ interactionId,
5539
+ ...answeredBy ? { answeredBy } : {}
5540
+ });
5360
5541
  this.deps.activityBus.publish("interaction-resolved", void 0);
5542
+ try {
5543
+ interaction.onSettled?.({ answeredBy, decision });
5544
+ } catch (e) {
5545
+ logger2.warn("InteractionController: onSettled threw:", e);
5546
+ }
5361
5547
  interaction.resolve(result);
5362
5548
  this.pending.delete(interactionId);
5363
5549
  this.contexts.get(interactionId)?.dispose();
@@ -5756,6 +5942,18 @@ var budgetDetailOf = (e) => {
5756
5942
  ...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
5757
5943
  };
5758
5944
  };
5945
+ function withDelegatedDecisions(result, content, decisions, hasImages) {
5946
+ if (decisions.length === 0) return content;
5947
+ if (!hasImages && result !== null && typeof result === "object" && !Array.isArray(result)) {
5948
+ return JSON.stringify({ ...result, decidedOnUsersBehalf: decisions });
5949
+ }
5950
+ return [content, ...decisions.map(describeDelegatedDecision)].join("\n\n");
5951
+ }
5952
+ function describeDelegatedDecision(decision) {
5953
+ const choice = decision.choice !== void 0 ? `: "${decision.choice}"` : "";
5954
+ const shown = decision.shownToUser ? ` What the user was shown, as your words: "${decision.shownToUser}"` : " Nothing was shown to the user about it.";
5955
+ return `${decision.framing}${choice}.${shown}`;
5956
+ }
5759
5957
  function inFlightToolCallsAt(history) {
5760
5958
  for (let i = history.length - 1; i >= 0; i -= 1) {
5761
5959
  const message = history[i];
@@ -5921,6 +6119,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5921
6119
  * on that one was reset by every handoff inside a single message.
5922
6120
  */
5923
6121
  this.messageController = new AbortController();
6122
+ /** Capabilities provided on this driver by decorators (GENC-1660). Only the root's are used. */
6123
+ this.handlerCapabilities = /* @__PURE__ */ new Map();
5924
6124
  /**
5925
6125
  * Caches validated provider lookups per name within the current agent. Cleared
5926
6126
  * by `installAgent` so each new agent's static/function-resolved names are
@@ -5944,9 +6144,17 @@ var ChatDriver = class _ChatDriver extends EventTarget {
5944
6144
  * the point: two would let `getActiveAgent()` disagree between the outer and inner driver.
5945
6145
  * `@internal` rather than private so that reach is legal without six delegating one-liners.
5946
6146
  *
6147
+ * **This driver raises the lifecycle events for every transition of the session**, whoever made
6148
+ * it: itself, through {@link ChatDriver.activateAgent} or a release, or the orchestrator reaching
6149
+ * through (GENC-1485). Before, only the orchestrator dispatched them, so a host on a bare
6150
+ * `ChatDriver` never learned its flow owner.
6151
+ *
5947
6152
  * @internal
5948
6153
  */
5949
- this.agentSession = new AgentSession({ sessionKey: () => this.sessionKey });
6154
+ this.agentSession = new AgentSession({
6155
+ sessionKey: () => this.sessionKey,
6156
+ onTransition: (t) => this.dispatchAgentTransition(t)
6157
+ });
5950
6158
  /**
5951
6159
  * A provider refusal was observed this turn, so the tool loop must not call the model again
5952
6160
  * (GENC-1506).
@@ -6455,6 +6663,51 @@ var ChatDriver = class _ChatDriver extends EventTarget {
6455
6663
  await this.agentSession.activate(config);
6456
6664
  this.installAgent(config);
6457
6665
  }
6666
+ /**
6667
+ * {@inheritDoc AiDriver.resumeRestoredFlow}
6668
+ *
6669
+ * This driver holds no list of agents, so it resumes only the `agent` it is handed (GENC-1660):
6670
+ * whoever runs a bare `ChatDriver` knows which agent that is, and after a reload only the name
6671
+ * survived. Without one it does nothing, as a driver without this member did before; given one
6672
+ * that is not the restored flow owner, it refuses.
6673
+ *
6674
+ * For headless callers: the shipped host restores and resumes through `OrchestratingDriver`,
6675
+ * and a bare `ChatDriver` does not answer `canRestoreFlowOwner`, so the host never plans a resume
6676
+ * for one.
6677
+ *
6678
+ * The orchestrator does not delegate here, because it installs a decorated config of its own;
6679
+ * this is the bare-driver path.
6680
+ *
6681
+ * @beta
6682
+ */
6683
+ async resumeRestoredFlow(resumeNote, agent) {
6684
+ if (!agent) return void 0;
6685
+ const owner = this.agentSession.getFlowOwner();
6686
+ if (owner !== null && owner !== agent.name) {
6687
+ logger2.warn(
6688
+ `ChatDriver: resumeRestoredFlow was handed "${agent.name}", but the flow belongs to "${owner}" \u2014 not resuming.`
6689
+ );
6690
+ return void 0;
6691
+ }
6692
+ await this.activateAgent(agent);
6693
+ if (!resumeNote || agent.resumeOnRestore === false) return void 0;
6694
+ return this.continueFromHistory(void 0, { tailNote: resumeNote });
6695
+ }
6696
+ /**
6697
+ * Raise the host-facing lifecycle events for one session transition (GENC-1485), in the order
6698
+ * the orchestrator raised them before it stopped doing so itself: the flow lock first, then the
6699
+ * release, then the active agent.
6700
+ */
6701
+ dispatchAgentTransition(t) {
6702
+ if (t.flowOwnerChangedTo !== void 0) {
6703
+ this.dispatchEvent(new CustomEvent("flow-owner-changed", { detail: t.flowOwnerChangedTo }));
6704
+ }
6705
+ const active = this.agentSession.getActive();
6706
+ if (!active && t.previousAgent) {
6707
+ this.dispatchEvent(new CustomEvent("agent-released", { detail: t.previousAgent }));
6708
+ }
6709
+ this.dispatchEvent(new CustomEvent("agent-changed", { detail: active }));
6710
+ }
6458
6711
  /**
6459
6712
  * {@inheritDoc AiDriver.getActiveAgent}
6460
6713
  *
@@ -6490,6 +6743,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
6490
6743
  setFlowOwner(name) {
6491
6744
  this.agentSession.setFlowOwner(name);
6492
6745
  }
6746
+ /**
6747
+ * {@inheritDoc AiDriver.getFlowOwner}
6748
+ *
6749
+ * @beta
6750
+ */
6751
+ getFlowOwner() {
6752
+ return this.agentSession.getFlowOwner();
6753
+ }
6493
6754
  /**
6494
6755
  * Install `config` as the active agent **without firing lifecycle hooks**.
6495
6756
  *
@@ -7063,8 +7324,84 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7063
7324
  * and `presentation` to control whether the host wraps the widget in a chat
7064
7325
  * bubble and/or shows the "Assistant" label.
7065
7326
  */
7066
- async requestInteraction(componentName, data, options, onOpened) {
7067
- return this.interactions.request(componentName, data, options, onOpened);
7327
+ async requestInteraction(componentName, data, options, onOpened, innerPath, onSettled) {
7328
+ return this.interactions.request(
7329
+ componentName,
7330
+ data,
7331
+ options,
7332
+ onOpened,
7333
+ innerPath,
7334
+ onSettled
7335
+ );
7336
+ }
7337
+ /**
7338
+ * {@inheritDoc AiDriver.addInteractionInterceptor}
7339
+ *
7340
+ * Consulted only on the root driver, where every interaction opens: a sub-agent's request
7341
+ * travels up to its root, carrying its owner path, and is offered there once.
7342
+ *
7343
+ * @beta
7344
+ */
7345
+ addInteractionInterceptor(interceptor) {
7346
+ return this.interactions.addInterceptor(interceptor);
7347
+ }
7348
+ /**
7349
+ * {@inheritDoc AiDriver.presentInteraction}
7350
+ *
7351
+ * @beta
7352
+ */
7353
+ presentInteraction(componentName, data, options) {
7354
+ return this.requestInteraction(componentName, data, options);
7355
+ }
7356
+ /**
7357
+ * {@inheritDoc AiDriver.provideHandlerCapability}
7358
+ *
7359
+ * Throws if `name` is already provided: two decorators answering to one name would leave a
7360
+ * handler unable to tell which it is talking to.
7361
+ *
7362
+ * @beta
7363
+ */
7364
+ provideHandlerCapability(name, factory) {
7365
+ if (this.handlerCapabilities.has(name)) {
7366
+ throw new Error(`provideHandlerCapability: "${name}" is already provided.`);
7367
+ }
7368
+ this.handlerCapabilities.set(name, factory);
7369
+ return () => {
7370
+ if (this.handlerCapabilities.get(name) === factory) this.handlerCapabilities.delete(name);
7371
+ };
7372
+ }
7373
+ /**
7374
+ * The capabilities this driver's handlers see, and who they see them as: the root's factories,
7375
+ * the root's flow owner, and the owner path from the root down to this driver's agent.
7376
+ */
7377
+ capabilityScope() {
7378
+ const own = this.activeAgentName ? [this.activeAgentName] : [];
7379
+ if (!this.capabilityParent) {
7380
+ return {
7381
+ factories: this.handlerCapabilities,
7382
+ scope: { ownerPath: own, flowOwner: this.agentSession.getFlowOwner() }
7383
+ };
7384
+ }
7385
+ const up = this.capabilityParent.capabilityScope();
7386
+ return {
7387
+ factories: up.factories,
7388
+ scope: { ownerPath: [...up.scope.ownerPath, ...own], flowOwner: up.scope.flowOwner }
7389
+ };
7390
+ }
7391
+ /** Build `context.capabilities` for one handler call, or `undefined` when nothing is provided. */
7392
+ buildCapabilities() {
7393
+ const { factories, scope } = this.capabilityScope();
7394
+ if (factories.size === 0) return void 0;
7395
+ const capabilities2 = {};
7396
+ for (const [name, factory] of factories) {
7397
+ try {
7398
+ const value = factory(scope);
7399
+ if (value !== void 0) capabilities2[name] = value;
7400
+ } catch (e) {
7401
+ logger2.warn(`ChatDriver: handler capability "${name}" threw \u2014 leaving it out:`, e);
7402
+ }
7403
+ }
7404
+ return capabilities2;
7068
7405
  }
7069
7406
  /**
7070
7407
  * Record what came of an interaction, after it resolved, on its history message
@@ -7136,8 +7473,10 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7136
7473
  this.history = [...messages];
7137
7474
  this.notifyHistoryUpdated();
7138
7475
  }
7139
- async sendMessage(userInput, attachments) {
7476
+ async sendMessage(userInput, attachments, options) {
7140
7477
  if (this.busy || !userInput.trim() && !attachments?.length) return { reason: "done" };
7478
+ const delegate = options?.delegate;
7479
+ const delegateFraming = delegate ? options?.delegateFraming ?? DEFAULT_DELEGATE_FRAMING : void 0;
7141
7480
  this.busy = true;
7142
7481
  this.beginTurn();
7143
7482
  this.beginMessage();
@@ -7154,7 +7493,12 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7154
7493
  this.aiServiceWall = void 0;
7155
7494
  this.aiServiceNoticesShown.clear();
7156
7495
  this.aiServiceNoticesQueued = [];
7157
- this.appendToHistory({ role: "user", content: userInput, attachments });
7496
+ this.appendToHistory({
7497
+ role: "user",
7498
+ content: userInput,
7499
+ attachments,
7500
+ ...delegate ? { delegate, delegateFraming } : {}
7501
+ });
7158
7502
  this.turnStartedAt = Date.now();
7159
7503
  recordMetaEvent(this.sessionKey, "turn.start", {
7160
7504
  phase: "sendMessage",
@@ -7163,7 +7507,10 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7163
7507
  this.activityBus.publish("tool-loop-start", void 0);
7164
7508
  let result = { reason: "done" };
7165
7509
  try {
7166
- result = await this.runToolLoop(userInput, attachments);
7510
+ result = await this.runToolLoop(
7511
+ delegate ? frameDelegatedContent(userInput, delegateFraming) : userInput,
7512
+ attachments
7513
+ );
7167
7514
  return result;
7168
7515
  } catch (e) {
7169
7516
  logger2.error("ChatDriver error:", e);
@@ -7216,13 +7563,25 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7216
7563
  * kept on a seven-way fan-out, reporting ~2.3x under the real cost, with the
7217
7564
  * error growing as the fan-out widens.
7218
7565
  */
7219
- buildHandlerContext(activeToolCallId, traceCapture) {
7566
+ buildHandlerContext(activeToolCallId, traceCapture, decisions) {
7220
7567
  let opened;
7221
7568
  let recorded = false;
7569
+ const capabilities2 = this.buildCapabilities();
7222
7570
  return {
7223
- requestInteraction: (componentName, data, options) => this.requestInteraction(componentName, data, options, (interactionId) => {
7224
- opened = interactionId;
7225
- }),
7571
+ ...capabilities2 && { capabilities: capabilities2 },
7572
+ requestInteraction: (componentName, data, options) => this.requestInteraction(
7573
+ componentName,
7574
+ data,
7575
+ options,
7576
+ (interactionId) => {
7577
+ opened = interactionId;
7578
+ },
7579
+ void 0,
7580
+ // An interaction an interceptor answered is reported in THIS call's result (GENC-1660).
7581
+ (settled) => {
7582
+ if (settled.decision) decisions?.push(settled.decision);
7583
+ }
7584
+ ),
7226
7585
  recordInteractionOutcome: (outcome) => {
7227
7586
  if (!opened) return false;
7228
7587
  if (recorded) {
@@ -7341,11 +7700,12 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7341
7700
  }
7342
7701
  child.installAgent({ ...subConfig, primerHistory: effectivePrimer });
7343
7702
  child.setHostInteractionRequester(
7344
- (componentName, data, opts, onOpened) => this.requestInteraction(componentName, data, opts, onOpened),
7703
+ (componentName, data, opts, onOpened, ownerPath, onSettled) => this.requestInteraction(componentName, data, opts, onOpened, ownerPath, onSettled),
7345
7704
  // An outcome goes where its interaction went.
7346
7705
  (interactionId, outcome) => this.recordInteractionOutcome(interactionId, outcome)
7347
7706
  );
7348
7707
  child.hostAIServiceNotice = (code) => this.noticeAIServiceRefusal(code);
7708
+ child.capabilityParent = this;
7349
7709
  const forwardTrace = (e) => {
7350
7710
  this.dispatchEvent(
7351
7711
  new CustomEvent("sub-agent-history-updated", {
@@ -8056,10 +8416,19 @@ ${tailBody}
8056
8416
  }
8057
8417
  const traceCapture = { traces: [] };
8058
8418
  const capturedTrace = () => traceCapture.traces.length ? traceCapture.traces.flat() : void 0;
8419
+ const decisions = [];
8059
8420
  try {
8060
- const result = await handler(tc.args, this.buildHandlerContext(tc.id, traceCapture));
8421
+ const result = await handler(
8422
+ tc.args,
8423
+ this.buildHandlerContext(tc.id, traceCapture, decisions)
8424
+ );
8061
8425
  const resultImages = toolResultImages(result);
8062
- const content = resultImages ? typeof result.content === "string" ? result.content : JSON.stringify({ ...result, attachments: void 0 }) : typeof result === "string" ? result : JSON.stringify(result);
8426
+ const content = withDelegatedDecisions(
8427
+ result,
8428
+ resultImages ? typeof result.content === "string" ? result.content : JSON.stringify({ ...result, attachments: void 0 }) : typeof result === "string" ? result : JSON.stringify(result),
8429
+ decisions,
8430
+ !!resultImages
8431
+ );
8063
8432
  executedById.set(tc.id, {
8064
8433
  toolCallId: tc.id,
8065
8434
  content,
@@ -8259,6 +8628,189 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
8259
8628
  }
8260
8629
  };
8261
8630
 
8631
+ // src/components/ai-driver/ai-driver.ts
8632
+ var AI_DRIVER_EVENT_TYPES = Object.keys({
8633
+ "history-updated": true,
8634
+ "sub-agent-history-updated": true,
8635
+ "sub-agent-start": true,
8636
+ "sub-agent-stop": true,
8637
+ "interaction-start": true,
8638
+ "interaction-stop": true,
8639
+ "provider-changed": true,
8640
+ "external-cost-recorded": true,
8641
+ "agent-changed": true,
8642
+ "agent-released": true,
8643
+ "flow-owner-changed": true,
8644
+ "orchestrating-start": true,
8645
+ "orchestrating-stop": true
8646
+ });
8647
+
8648
+ // src/components/ai-driver/base-driver-decorator.ts
8649
+ var BaseDriverDecorator = class _BaseDriverDecorator extends EventTarget {
8650
+ constructor(inner) {
8651
+ super();
8652
+ this.inner = inner;
8653
+ /** Undoes this layer's hold on its inner driver; see {@link BaseDriverDecorator.detach}. */
8654
+ this.detachers = [];
8655
+ for (const type of AI_DRIVER_EVENT_TYPES) {
8656
+ const forward = (e) => {
8657
+ this.dispatchEvent(new CustomEvent(type, { detail: e.detail }));
8658
+ };
8659
+ inner.addEventListener(type, forward);
8660
+ this.onDetach(() => inner.removeEventListener(type, forward));
8661
+ }
8662
+ }
8663
+ /**
8664
+ * Let go of the inner driver WITHOUT disposing it: stop forwarding its events, and undo whatever
8665
+ * a subclass registered through {@link BaseDriverDecorator.onDetach} (its interceptors, its
8666
+ * capabilities, its own listeners). For dropping or replacing a decorator while the driver it
8667
+ * wrapped lives on — otherwise the old layer keeps re-dispatching every event and is never
8668
+ * collected. Idempotent. `dispose()` detaches too.
8669
+ */
8670
+ detach() {
8671
+ for (const undo of this.detachers.splice(0)) {
8672
+ try {
8673
+ undo();
8674
+ } catch {
8675
+ }
8676
+ }
8677
+ }
8678
+ /** Register something {@link BaseDriverDecorator.detach} must undo. */
8679
+ onDetach(undo) {
8680
+ this.detachers.push(undo);
8681
+ }
8682
+ sendMessage(input, attachments, options) {
8683
+ return this.inner.sendMessage(input, attachments, options);
8684
+ }
8685
+ continueFromHistory(transientPrimer, options) {
8686
+ return this.inner.continueFromHistory(transientPrimer, options);
8687
+ }
8688
+ resolveInteraction(interactionId, result) {
8689
+ this.inner.resolveInteraction(interactionId, result);
8690
+ }
8691
+ recordExternalCost(interactionId, costUsd) {
8692
+ return this.inner.recordExternalCost?.(interactionId, costUsd) ?? false;
8693
+ }
8694
+ getInteractionContext(interactionId) {
8695
+ return this.inner.getInteractionContext?.(interactionId);
8696
+ }
8697
+ loadHistory(messages) {
8698
+ this.inner.loadHistory(messages);
8699
+ }
8700
+ primeRestoredAgentState(snapshots) {
8701
+ this.inner.primeRestoredAgentState(snapshots);
8702
+ }
8703
+ canRestoreFlowOwner(agentName) {
8704
+ return this.inner.canRestoreFlowOwner?.(agentName) ?? false;
8705
+ }
8706
+ canResumeRestoredFlow(agentName) {
8707
+ return this.inner.canResumeRestoredFlow?.(agentName) ?? false;
8708
+ }
8709
+ resumeRestoredFlow(resumeNote, agent) {
8710
+ return this.inner.resumeRestoredFlow?.(resumeNote, agent) ?? Promise.resolve(void 0);
8711
+ }
8712
+ getRawHistory() {
8713
+ return this.inner.getRawHistory?.() ?? [];
8714
+ }
8715
+ getHistoryForPersistence() {
8716
+ return this.inner.getHistoryForPersistence?.() ?? [...this.getRawHistory()];
8717
+ }
8718
+ getCondensePoliciesForPersistence(messages) {
8719
+ return this.inner.getCondensePoliciesForPersistence?.(messages) ?? [];
8720
+ }
8721
+ restoreCondensePolicies(entries) {
8722
+ return this.inner.restoreCondensePolicies?.(entries) ?? { restored: 0, awaitingReactivation: 0 };
8723
+ }
8724
+ getActiveAgent() {
8725
+ return this.inner.getActiveAgent();
8726
+ }
8727
+ setPinnedAgent(name) {
8728
+ this.inner.setPinnedAgent(name);
8729
+ }
8730
+ setFlowOwner(name) {
8731
+ this.inner.setFlowOwner(name);
8732
+ }
8733
+ getFlowOwner() {
8734
+ return this.inner.getFlowOwner?.() ?? null;
8735
+ }
8736
+ /**
8737
+ * Forwarded, so an interceptor installed on any layer lands on the driver that opens the
8738
+ * interactions. A decorator that answers interactions installs its own on `this.inner`.
8739
+ */
8740
+ addInteractionInterceptor(interceptor) {
8741
+ return this.inner.addInteractionInterceptor?.(interceptor) ?? (() => void 0);
8742
+ }
8743
+ presentInteraction(componentName, data, options) {
8744
+ return this.inner.presentInteraction?.(componentName, data, options) ?? Promise.reject(new Error("presentInteraction: the inner driver cannot show interactions."));
8745
+ }
8746
+ provideHandlerCapability(name, factory) {
8747
+ return this.inner.provideHandlerCapability?.(name, factory) ?? (() => void 0);
8748
+ }
8749
+ getContextReserve() {
8750
+ return this.inner.getContextReserve?.();
8751
+ }
8752
+ getPreambleTokens() {
8753
+ return this.inner.getPreambleTokens?.();
8754
+ }
8755
+ setContextGuard(policy) {
8756
+ this.inner.setContextGuard?.(policy);
8757
+ }
8758
+ compact(options) {
8759
+ return this.inner.compact?.(options) ?? Promise.resolve(null);
8760
+ }
8761
+ getCompactionPlan(options) {
8762
+ return this.inner.getCompactionPlan?.(options) ?? null;
8763
+ }
8764
+ canCompact(options) {
8765
+ return this.inner.canCompact?.(options) ?? false;
8766
+ }
8767
+ isBusy() {
8768
+ return this.inner.isBusy();
8769
+ }
8770
+ getSuggestions(history, prompt2, count, allAgentInfo) {
8771
+ return this.inner.getSuggestions(history, prompt2, count, allAgentInfo);
8772
+ }
8773
+ getTurnSnapshots() {
8774
+ return this.inner.getTurnSnapshots?.() ?? [];
8775
+ }
8776
+ /**
8777
+ * The inner driver's entries. Diagnostics compose down the stack: a decorator with entries of
8778
+ * its own overrides this and appends them to `super.getExternalDiagnostics()`.
8779
+ */
8780
+ getExternalDiagnostics() {
8781
+ return this.inner.getExternalDiagnostics?.() ?? [];
8782
+ }
8783
+ getActiveProviderName() {
8784
+ return this.inner.getActiveProviderName();
8785
+ }
8786
+ dispose() {
8787
+ const disposed = this.inner.dispose?.();
8788
+ this.detach();
8789
+ return disposed;
8790
+ }
8791
+ cancel() {
8792
+ this.inner.cancel?.();
8793
+ }
8794
+ /**
8795
+ * Every driver in the stack, outermost first: `driver`, then each inner driver down to the one
8796
+ * that is not a decorator.
8797
+ *
8798
+ * For diagnostics and labelling only: behaviour must go through `AiDriver` members, which every
8799
+ * layer forwards, never through a concrete class found here.
8800
+ *
8801
+ * @internal
8802
+ */
8803
+ static layers(driver) {
8804
+ const layers = [driver];
8805
+ let current = driver;
8806
+ while (current instanceof _BaseDriverDecorator) {
8807
+ current = current.inner;
8808
+ layers.push(current);
8809
+ }
8810
+ return layers;
8811
+ }
8812
+ };
8813
+
8262
8814
  // src/components/orchestrating-driver/orchestrating-driver.ts
8263
8815
  var DEFAULT_MAX_HANDOFFS = 3;
8264
8816
  var DEFAULT_CLASSIFIER_HISTORY_LENGTH = 4;
@@ -8321,9 +8873,9 @@ Politely let the user know what you can help with and invite them to rephrase th
8321
8873
 
8322
8874
  ${UNROUTABLE_INSTRUCTION}`;
8323
8875
  }
8324
- var OrchestratingDriver = class extends EventTarget {
8876
+ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator {
8325
8877
  constructor(providerRegistry, agents, options = {}) {
8326
- super();
8878
+ super(_OrchestratingDriver.createInner(providerRegistry, agents, options));
8327
8879
  this.providerRegistry = providerRegistry;
8328
8880
  this.agents = agents;
8329
8881
  /**
@@ -8347,7 +8899,6 @@ var OrchestratingDriver = class extends EventTarget {
8347
8899
  this.maxHandoffs = options.maxHandoffs ?? DEFAULT_MAX_HANDOFFS;
8348
8900
  this.classifierHistoryLength = options.classifierHistoryLength ?? DEFAULT_CLASSIFIER_HISTORY_LENGTH;
8349
8901
  this.classifierRetries = options.classifierRetries ?? DEFAULT_CLASSIFIER_RETRIES;
8350
- validateStaticAgentProviders(agents, providerRegistry);
8351
8902
  this.specialists = agents.filter(isSpecialist).filter((a) => !a.excludeFromClassifier);
8352
8903
  const fallbacks = agents.filter(isFallback);
8353
8904
  if (fallbacks.length > 1) {
@@ -8357,51 +8908,10 @@ var OrchestratingDriver = class extends EventTarget {
8357
8908
  }
8358
8909
  const rawFallback = fallbacks[0];
8359
8910
  this.fallback = rawFallback ? { ...rawFallback, systemPrompt: buildFallbackSystemPrompt(rawFallback, this.specialists) } : void 0;
8360
- this.chatDriver = new ChatDriver(providerRegistry, {
8361
- maxToolIterations: options.maxToolIterations,
8362
- maxFoldOperations: options.maxFoldOperations,
8363
- maxTurnSnapshots: options.maxTurnSnapshots,
8364
- sessionKey: this.sessionKey,
8365
- activityBus: options.activityBus,
8366
- budgetExhaustedMessage: options.budgetExhaustedMessage,
8367
- aiServiceErrorMessages: options.aiServiceErrorMessages,
8368
- // This driver releases after the turn so it can fire the host-facing events; see the option.
8369
- deferAgentRelease: true
8370
- });
8371
- this.chatDriver.addEventListener("history-updated", (e) => {
8372
- this.dispatchEvent(new CustomEvent("history-updated", { detail: e.detail }));
8373
- });
8374
- this.chatDriver.addEventListener("sub-agent-history-updated", (e) => {
8375
- this.dispatchEvent(
8376
- new CustomEvent("sub-agent-history-updated", { detail: e.detail })
8377
- );
8378
- });
8379
- this.chatDriver.addEventListener("sub-agent-start", (e) => {
8380
- this.dispatchEvent(new CustomEvent("sub-agent-start", { detail: e.detail }));
8381
- });
8382
- this.chatDriver.addEventListener("sub-agent-stop", (e) => {
8383
- this.dispatchEvent(new CustomEvent("sub-agent-stop", { detail: e.detail }));
8384
- });
8385
- this.chatDriver.addEventListener("interaction-start", (e) => {
8386
- this.dispatchEvent(
8387
- new CustomEvent("interaction-start", { detail: e.detail })
8388
- );
8389
- });
8390
- this.chatDriver.addEventListener("interaction-stop", (e) => {
8391
- this.dispatchEvent(
8392
- new CustomEvent("interaction-stop", { detail: e.detail })
8393
- );
8394
- });
8395
- this.chatDriver.addEventListener("provider-changed", (e) => {
8396
- this.dispatchEvent(
8397
- new CustomEvent("provider-changed", { detail: e.detail })
8398
- );
8399
- });
8400
- this.chatDriver.addEventListener("external-cost-recorded", (e) => {
8401
- this.dispatchEvent(
8402
- new CustomEvent("external-cost-recorded", { detail: e.detail })
8403
- );
8404
- });
8911
+ }
8912
+ /** The inner driver this one built and routes onto. */
8913
+ get chatDriver() {
8914
+ return this.inner;
8405
8915
  }
8406
8916
  /**
8407
8917
  * The stateful-agent contract (§7.1): active agent, user pin, flow lock, restored state, and
@@ -8425,108 +8935,23 @@ var OrchestratingDriver = class extends EventTarget {
8425
8935
  get activeAgent() {
8426
8936
  return this.agentSession.getActive();
8427
8937
  }
8428
- resolveInteraction(interactionId, result) {
8429
- this.chatDriver.resolveInteraction(interactionId, result);
8430
- }
8431
- /** Delegated for the same reason as `resolveInteraction`: the history that owns the
8432
- * interaction — and therefore the cost — lives on the wrapped driver, not here. */
8433
- recordExternalCost(interactionId, costUsd) {
8434
- return this.chatDriver.recordExternalCost(interactionId, costUsd);
8435
- }
8436
- getInteractionContext(interactionId) {
8437
- return this.chatDriver.getInteractionContext(interactionId);
8438
- }
8439
- isBusy() {
8440
- return this.chatDriver.isBusy();
8441
- }
8442
- /** Currently active provider name from the underlying ChatDriver. */
8443
- getActiveProviderName() {
8444
- return this.chatDriver.getActiveProviderName();
8445
- }
8446
- /**
8447
- * Pins routing to a specific agent by name. While pinned, the classifier is
8448
- * skipped and the continuation tool is hidden from the agent's tool list, so
8449
- * the agent cannot quietly hand back to the orchestrator. Pass `null` to
8450
- * return to automatic routing.
8451
- */
8452
- setPinnedAgent(name) {
8453
- this.agentSession.setPinned(name);
8454
- }
8455
- /**
8456
- * Set the flow-owner lock — used to restore state from Redux on driver
8457
- * reconstruction (agents-array changes, pop-in/pop-out). Not intended for
8458
- * external callers to manipulate routing; that's what `setPinnedAgent` is
8459
- * for.
8460
- */
8461
- setFlowOwner(name) {
8462
- this.agentSession.setFlowOwner(name);
8463
- }
8464
- loadHistory(messages) {
8465
- this.chatDriver.loadHistory(messages);
8466
- }
8467
- getRawHistory() {
8468
- return this.chatDriver.getHistory();
8469
- }
8470
8938
  /**
8471
- * Delegates to the inner {@link ChatDriver} — condensation policies are registered there, so it
8472
- * is the only object that knows which payloads the model has stopped seeing (GENC-1566).
8473
- *
8474
- * **This forward is the whole feature, for any multi-agent host.** `AiDriver` declares
8475
- * `getHistoryForPersistence` optional, and `SessionPersister` calls it through `?.`, falling back
8476
- * to {@link OrchestratingDriver.getRawHistory}. This class does NOT extend `ChatDriver` — it
8477
- * wraps one — so without an explicit forward the optional call resolves to `undefined` and every
8478
- * host on an orchestrating driver silently keeps persisting the full payloads. Measured exactly
8479
- * that way on a Create run: 10 `context.condensed` events fired, and the session still stored
8480
- * the two largest `read_context` results at 26,907 and 23,060 chars. The method existed, the
8481
- * tests passed, and the feature did nothing.
8482
- */
8483
- getHistoryForPersistence() {
8484
- return this.chatDriver.getHistoryForPersistence();
8485
- }
8486
- /**
8487
- * Delegates to the inner {@link ChatDriver}, which owns the condensation registry and clocks
8488
- * (GENC-1632). The forward matters for the same reason as `getHistoryForPersistence` above: the
8489
- * persister reaches it through `?.`, so a missing forward would compile, pass the ChatDriver
8490
- * tests, and do nothing for every orchestrated host — which is every host with an agents array.
8939
+ * Build the inner driver, after validating the agents' providers, so a bad config throws before
8940
+ * a driver exists to subscribe to the registry. Static because it runs before `super()`.
8491
8941
  */
8492
- getCondensePoliciesForPersistence(messages) {
8493
- return this.chatDriver.getCondensePoliciesForPersistence(messages);
8494
- }
8495
- /**
8496
- * Delegates to the inner {@link ChatDriver} (GENC-1632). The deferred activation stamp it
8497
- * performs is taken by the inner driver's `installAgent`, which this driver's own
8498
- * `installAgent` always reaches, so nothing extra is needed here.
8499
- */
8500
- restoreCondensePolicies(entries) {
8501
- return this.chatDriver.restoreCondensePolicies(entries);
8502
- }
8503
- /** {@inheritDoc AiDriver.setContextGuard} */
8504
- setContextGuard(policy) {
8505
- this.chatDriver.setContextGuard(policy);
8506
- }
8507
- /** {@inheritDoc AiDriver.compact} */
8508
- compact(options) {
8509
- return this.chatDriver.compact(options);
8510
- }
8511
- /** {@inheritDoc AiDriver.canCompact} */
8512
- canCompact(options) {
8513
- return this.chatDriver.canCompact(options);
8514
- }
8515
- /** {@inheritDoc AiDriver.getPreambleTokens} */
8516
- getPreambleTokens() {
8517
- return this.chatDriver.getPreambleTokens();
8518
- }
8519
- /** {@inheritDoc AiDriver.getCompactionPlan} */
8520
- getCompactionPlan(options) {
8521
- return this.chatDriver.getCompactionPlan(options);
8522
- }
8523
- /** Delegates to the inner {@link ChatDriver} — turns are captured there. */
8524
- getTurnSnapshots() {
8525
- return this.chatDriver.getTurnSnapshots();
8526
- }
8527
- /** Delegates to the inner {@link ChatDriver} — interactions resolve there, so it holds the buffer. */
8528
- getExternalDiagnostics() {
8529
- return this.chatDriver.getExternalDiagnostics();
8942
+ static createInner(providerRegistry, agents, options = {}) {
8943
+ validateStaticAgentProviders(agents, providerRegistry);
8944
+ return new ChatDriver(providerRegistry, {
8945
+ maxToolIterations: options.maxToolIterations,
8946
+ maxFoldOperations: options.maxFoldOperations,
8947
+ maxTurnSnapshots: options.maxTurnSnapshots,
8948
+ sessionKey: options.sessionKey ?? "",
8949
+ activityBus: options.activityBus,
8950
+ budgetExhaustedMessage: options.budgetExhaustedMessage,
8951
+ aiServiceErrorMessages: options.aiServiceErrorMessages,
8952
+ // A release ends this driver's handoff loop, so it acts on the flag itself; see the option.
8953
+ deferAgentRelease: true
8954
+ });
8530
8955
  }
8531
8956
  async getSuggestions(history, prompt2, count, allAgentInfo) {
8532
8957
  const pinned = this.resolvePinnedAgent();
@@ -8541,14 +8966,19 @@ var OrchestratingDriver = class extends EventTarget {
8541
8966
  }));
8542
8967
  return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
8543
8968
  }
8544
- async sendMessage(input, attachments) {
8969
+ async sendMessage(input, attachments, options) {
8970
+ const delegated = options?.delegate ? {
8971
+ delegate: options.delegate,
8972
+ delegateFraming: options.delegateFraming ?? DEFAULT_DELEGATE_FRAMING
8973
+ } : {};
8545
8974
  try {
8546
- return await this.runOrchestratedTurn(input, attachments);
8975
+ return await this.runOrchestratedTurn(input, attachments, options, delegated);
8547
8976
  } catch (e) {
8548
8977
  const pendingUserMessage = this.userMessageAppended ? void 0 : {
8549
8978
  role: "user",
8550
8979
  content: input,
8551
- ...attachments ? { attachments } : {}
8980
+ ...attachments ? { attachments } : {},
8981
+ ...delegated
8552
8982
  };
8553
8983
  if (e instanceof BudgetExhaustedError) {
8554
8984
  return this.chatDriver.reportBudgetExhausted(e, pendingUserMessage);
@@ -8559,13 +8989,13 @@ var OrchestratingDriver = class extends EventTarget {
8559
8989
  throw e;
8560
8990
  }
8561
8991
  }
8562
- async runOrchestratedTurn(input, attachments) {
8992
+ async runOrchestratedTurn(input, attachments, options, delegated) {
8563
8993
  this.cancelled = false;
8564
8994
  this.userMessageAppended = false;
8565
8995
  const history = this.chatDriver.getHistory();
8566
8996
  this.dispatchEvent(
8567
8997
  new CustomEvent("history-updated", {
8568
- detail: [...history, { role: "user", content: input, attachments }]
8998
+ detail: [...history, { role: "user", content: input, attachments, ...delegated }]
8569
8999
  })
8570
9000
  );
8571
9001
  const pinned = this.resolvePinnedAgent();
@@ -8575,7 +9005,11 @@ var OrchestratingDriver = class extends EventTarget {
8575
9005
  this.recordRouting("pinned", pinned, { durationMs: 0 });
8576
9006
  routed = { agent: pinned, decision: "pinned" };
8577
9007
  } else {
8578
- routed = await this.classify(input, history, this.activeAgent);
9008
+ routed = await this.classify(
9009
+ delegated.delegate ? frameDelegatedContent(input, delegated.delegateFraming) : input,
9010
+ history,
9011
+ this.activeAgent
9012
+ );
8579
9013
  }
8580
9014
  let currentAgent = routed.agent;
8581
9015
  let isHandoff = false;
@@ -8602,7 +9036,7 @@ var OrchestratingDriver = class extends EventTarget {
8602
9036
  result = await this.chatDriver.continueFromHistory(contextPrimer);
8603
9037
  } else {
8604
9038
  this.userMessageAppended = true;
8605
- result = await this.chatDriver.sendMessage(input, attachments);
9039
+ result = await this.chatDriver.sendMessage(input, attachments, options);
8606
9040
  }
8607
9041
  lastResult = result;
8608
9042
  if (this.chatDriver.getAgentReleaseRequested()) {
@@ -8635,18 +9069,12 @@ var OrchestratingDriver = class extends EventTarget {
8635
9069
  this.chatDriver.reportStoppedBeforeTurn({
8636
9070
  role: "user",
8637
9071
  content: input,
8638
- ...attachments ? { attachments } : {}
9072
+ ...attachments ? { attachments } : {},
9073
+ ...delegated
8639
9074
  });
8640
9075
  }
8641
9076
  return lastResult?.reason === "done" ? lastResult : { reason: "done" };
8642
9077
  }
8643
- async continueFromHistory(transientPrimer, options) {
8644
- return this.chatDriver.continueFromHistory(transientPrimer, options);
8645
- }
8646
- /** {@inheritDoc AiDriver.primeRestoredAgentState} */
8647
- primeRestoredAgentState(snapshots) {
8648
- this.agentSession.primeRestoredStates(snapshots);
8649
- }
8650
9078
  /** {@inheritDoc AiDriver.canRestoreFlowOwner} */
8651
9079
  canRestoreFlowOwner(agentName) {
8652
9080
  return this.agents.some((a) => a.name === agentName);
@@ -8663,6 +9091,10 @@ var OrchestratingDriver = class extends EventTarget {
8663
9091
  * to classify, and a restored lock already says which agent owns the flow. `continueFromHistory`
8664
9092
  * on its own would not do — it neither routes nor installs, so it would run on whatever agent
8665
9093
  * happened to be installed (none, straight after a reload).
9094
+ *
9095
+ * Looks the owner up in its own agents list, so the `agent` argument is not needed and is
9096
+ * ignored; and does not delegate to the inner driver's resume, which would install the agent
9097
+ * undecorated.
8666
9098
  */
8667
9099
  async resumeRestoredFlow(resumeNote) {
8668
9100
  const owner = this.agentSession.getFlowOwner();
@@ -8699,15 +9131,8 @@ var OrchestratingDriver = class extends EventTarget {
8699
9131
  }
8700
9132
  return winner ? { ...winner, agentName, basis: "reachable-max" } : void 0;
8701
9133
  }
8702
- /** {@inheritDoc AiDriver.getActiveAgent} */
8703
- getActiveAgent() {
8704
- return this.agentSession.getActive();
8705
- }
8706
9134
  async installAgent(agent) {
8707
- const { previousAgent, flowOwnerChangedTo } = await this.agentSession.activate(agent);
8708
- if (flowOwnerChangedTo !== void 0) {
8709
- this.dispatchEvent(new CustomEvent("flow-owner-changed", { detail: flowOwnerChangedTo }));
8710
- }
9135
+ const { previousAgent } = await this.agentSession.activate(agent);
8711
9136
  const isTerminal = isFallback(agent) || this.agentSession.getPinned() !== null || this.agentSession.getFlowOwner() !== null || !!(agent.onActivate || agent.onDeactivate);
8712
9137
  let agentToApply = agent;
8713
9138
  if (!isTerminal) {
@@ -8730,12 +9155,11 @@ var OrchestratingDriver = class extends EventTarget {
8730
9155
  this.chatDriver.setProviderHistoryTransform((h) => transformHistoryForAgent(h, agent.name));
8731
9156
  this.chatDriver.installAgent(agentToApply);
8732
9157
  this.dispatchEvent(new CustomEvent("orchestrating-stop"));
8733
- this.dispatchEvent(new CustomEvent("agent-changed", { detail: agent }));
8734
9158
  }
8735
9159
  /**
8736
- * Release the current stateful agent: fire `onDeactivate`, clear the
8737
- * flow-owner lock, dispatch events so the host (and Redux) reflect the
8738
- * unlocked state. Called automatically when a tool handler invokes
9160
+ * Release the current stateful agent: fire `onDeactivate` and clear the flow-owner lock. The
9161
+ * inner driver raises the events from the session's transition, so the host (and Redux) reflect
9162
+ * the unlocked state (GENC-1485). Called automatically when a tool handler invokes
8739
9163
  * `context.releaseAgent`.
8740
9164
  *
8741
9165
  * The user-pin (`pinnedAgentName`) is intentionally *not* cleared — a user
@@ -8744,15 +9168,7 @@ var OrchestratingDriver = class extends EventTarget {
8744
9168
  * routing through the classifier.
8745
9169
  */
8746
9170
  async releaseActiveAgent() {
8747
- const released = await this.agentSession.release();
8748
- if (!released) return;
8749
- if (released.flowOwnerChangedTo !== void 0) {
8750
- this.dispatchEvent(
8751
- new CustomEvent("flow-owner-changed", { detail: released.flowOwnerChangedTo })
8752
- );
8753
- }
8754
- this.dispatchEvent(new CustomEvent("agent-released", { detail: released.previousAgent }));
8755
- this.dispatchEvent(new CustomEvent("agent-changed", { detail: void 0 }));
9171
+ await this.agentSession.release();
8756
9172
  }
8757
9173
  /**
8758
9174
  * Stop the current turn (user "stop" button). Cancels the inner chat driver's
@@ -8771,6 +9187,7 @@ var OrchestratingDriver = class extends EventTarget {
8771
9187
  async dispose() {
8772
9188
  await this.agentSession.dispose();
8773
9189
  await this.chatDriver.dispose();
9190
+ this.detach();
8774
9191
  }
8775
9192
  /**
8776
9193
  * Record one routing decision on the debug timeline — what was chosen, why, what it cost, and
@@ -8812,7 +9229,7 @@ var OrchestratingDriver = class extends EventTarget {
8812
9229
  classifierTranscript(history) {
8813
9230
  const lines = history.filter((m) => (m.role === "user" || m.role === "assistant") && reachesProvider(m)).filter((m) => !!m.content?.trim()).slice(-this.classifierHistoryLength).map((m) => {
8814
9231
  const speaker = m.role === "user" ? "User" : "Assistant";
8815
- const content = m.content ?? "";
9232
+ const content = m.role === "user" && m.delegate ? frameDelegatedContent(m.content ?? "", m.delegateFraming) : m.content ?? "";
8816
9233
  const body = content.length > CLASSIFIER_MESSAGE_CHARS ? `${content.slice(0, CLASSIFIER_MESSAGE_CHARS)}\u2026` : content;
8817
9234
  return `${speaker}: ${body}`;
8818
9235
  });