@genesislcap/ai-assistant 15.61.0 → 15.63.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 (137) hide show
  1. package/dist/ai-assistant.api.json +18231 -11171
  2. package/dist/ai-assistant.d.ts +1235 -9
  3. package/dist/chat-driver.cjs +658 -50
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +657 -50
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +4759 -717
  8. package/dist/dts/autopilot/autopilot-driver.d.ts +278 -0
  9. package/dist/dts/autopilot/autopilot-driver.d.ts.map +1 -0
  10. package/dist/dts/autopilot/autopilot-session.d.ts +163 -0
  11. package/dist/dts/autopilot/autopilot-session.d.ts.map +1 -0
  12. package/dist/dts/autopilot/autopilot-tools.d.ts +15 -0
  13. package/dist/dts/autopilot/autopilot-tools.d.ts.map +1 -0
  14. package/dist/dts/autopilot/decision-message.d.ts +62 -0
  15. package/dist/dts/autopilot/decision-message.d.ts.map +1 -0
  16. package/dist/dts/autopilot/hand-back/hand-back.controller.d.ts +69 -0
  17. package/dist/dts/autopilot/hand-back/hand-back.controller.d.ts.map +1 -0
  18. package/dist/dts/autopilot/hand-back/hand-back.d.ts +22 -0
  19. package/dist/dts/autopilot/hand-back/hand-back.d.ts.map +1 -0
  20. package/dist/dts/autopilot/index.d.ts +4 -0
  21. package/dist/dts/autopilot/index.d.ts.map +1 -0
  22. package/dist/dts/channel/ai-activity-bus.d.ts.map +1 -1
  23. package/dist/dts/channel/ai-activity-channel.d.ts +17 -0
  24. package/dist/dts/channel/ai-activity-channel.d.ts.map +1 -1
  25. package/dist/dts/channel/autopilot-needs-user.d.ts +8 -0
  26. package/dist/dts/channel/autopilot-needs-user.d.ts.map +1 -0
  27. package/dist/dts/components/agent-picker/agent-picker.constants.d.ts +10 -0
  28. package/dist/dts/components/agent-picker/agent-picker.constants.d.ts.map +1 -1
  29. package/dist/dts/components/agent-picker/agent-picker.d.ts +4 -0
  30. package/dist/dts/components/agent-picker/agent-picker.d.ts.map +1 -1
  31. package/dist/dts/components/agent-picker/agent-picker.template.d.ts.map +1 -1
  32. package/dist/dts/components/ai-driver/ai-driver.d.ts +34 -1
  33. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  34. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +3 -1
  35. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -1
  36. package/dist/dts/components/chat-bubble/chat-bubble.d.ts +21 -0
  37. package/dist/dts/components/chat-bubble/chat-bubble.d.ts.map +1 -1
  38. package/dist/dts/components/chat-bubble/chat-bubble.styles.d.ts.map +1 -1
  39. package/dist/dts/components/chat-bubble/chat-bubble.template.d.ts.map +1 -1
  40. package/dist/dts/components/chat-driver/chat-driver.d.ts +5 -1
  41. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  42. package/dist/dts/components/chat-driver/interaction-controller.d.ts +27 -3
  43. package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
  44. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts +10 -2
  45. package/dist/dts/components/chat-interaction-wrapper/chat-interaction-wrapper.d.ts.map +1 -1
  46. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +9 -1
  47. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  48. package/dist/dts/config/config.d.ts +183 -0
  49. package/dist/dts/config/config.d.ts.map +1 -1
  50. package/dist/dts/config/define-stateful-agent.d.ts +31 -1
  51. package/dist/dts/config/define-stateful-agent.d.ts.map +1 -1
  52. package/dist/dts/genesis/write-commit.d.ts +17 -7
  53. package/dist/dts/genesis/write-commit.d.ts.map +1 -1
  54. package/dist/dts/index.d.ts +7 -3
  55. package/dist/dts/index.d.ts.map +1 -1
  56. package/dist/dts/main/main.d.ts +128 -5
  57. package/dist/dts/main/main.d.ts.map +1 -1
  58. package/dist/dts/main/main.styles.d.ts.map +1 -1
  59. package/dist/dts/main/main.template.d.ts +2 -2
  60. package/dist/dts/main/main.template.d.ts.map +1 -1
  61. package/dist/dts/react.d.ts +10 -0
  62. package/dist/dts/state/debug-event-log.d.ts +1 -1
  63. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  64. package/dist/dts/state/decision-surface.d.ts +46 -0
  65. package/dist/dts/state/decision-surface.d.ts.map +1 -0
  66. package/dist/dts/state/interaction-context.d.ts +21 -1
  67. package/dist/dts/state/interaction-context.d.ts.map +1 -1
  68. package/dist/dts/state/interaction-registry.d.ts +63 -0
  69. package/dist/dts/state/interaction-registry.d.ts.map +1 -0
  70. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  71. package/dist/dts/state/persistence/session-snapshot.d.ts +21 -0
  72. package/dist/dts/state/persistence/session-snapshot.d.ts.map +1 -1
  73. package/dist/dts/state/run-steps.d.ts +66 -0
  74. package/dist/dts/state/run-steps.d.ts.map +1 -0
  75. package/dist/dts/types/ai-chat-widget.d.ts +7 -1
  76. package/dist/dts/types/ai-chat-widget.d.ts.map +1 -1
  77. package/dist/dts/types/interaction-context.d.ts +66 -1
  78. package/dist/dts/types/interaction-context.d.ts.map +1 -1
  79. package/dist/dts/types/interaction-interceptor.d.ts +60 -2
  80. package/dist/dts/types/interaction-interceptor.d.ts.map +1 -1
  81. package/dist/dts/types/interaction-surface.d.ts +195 -0
  82. package/dist/dts/types/interaction-surface.d.ts.map +1 -0
  83. package/dist/dts/utils/context-tokens.d.ts +6 -0
  84. package/dist/dts/utils/context-tokens.d.ts.map +1 -1
  85. package/dist/dts/utils/delegated-message.d.ts +13 -0
  86. package/dist/dts/utils/delegated-message.d.ts.map +1 -1
  87. package/dist/dts/utils/history-transform.d.ts.map +1 -1
  88. package/dist/dts/utils/preserved-user-messages.d.ts +21 -0
  89. package/dist/dts/utils/preserved-user-messages.d.ts.map +1 -0
  90. package/dist/esm/autopilot/autopilot-driver.js +906 -0
  91. package/dist/esm/autopilot/autopilot-session.js +449 -0
  92. package/dist/esm/autopilot/autopilot-tools.js +68 -0
  93. package/dist/esm/autopilot/decision-message.js +206 -0
  94. package/dist/esm/autopilot/hand-back/hand-back.controller.js +47 -0
  95. package/dist/esm/autopilot/hand-back/hand-back.js +129 -0
  96. package/dist/esm/autopilot/index.js +4 -0
  97. package/dist/esm/channel/ai-activity-bus.js +7 -0
  98. package/dist/esm/channel/autopilot-needs-user.js +26 -0
  99. package/dist/esm/components/agent-picker/agent-picker.constants.js +10 -0
  100. package/dist/esm/components/agent-picker/agent-picker.js +11 -1
  101. package/dist/esm/components/agent-picker/agent-picker.template.js +2 -4
  102. package/dist/esm/components/ai-driver/ai-driver.js +2 -0
  103. package/dist/esm/components/ai-driver/base-driver-decorator.js +8 -0
  104. package/dist/esm/components/chat-bubble/chat-bubble.js +45 -0
  105. package/dist/esm/components/chat-bubble/chat-bubble.styles.js +26 -0
  106. package/dist/esm/components/chat-bubble/chat-bubble.template.js +13 -0
  107. package/dist/esm/components/chat-driver/chat-driver.js +53 -18
  108. package/dist/esm/components/chat-driver/interaction-controller.js +297 -12
  109. package/dist/esm/components/chat-interaction-wrapper/chat-interaction-wrapper.js +16 -2
  110. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +28 -11
  111. package/dist/esm/config/define-stateful-agent.js +34 -29
  112. package/dist/esm/genesis/write-outcome.js +2 -2
  113. package/dist/esm/index.js +3 -0
  114. package/dist/esm/main/main.js +334 -15
  115. package/dist/esm/main/main.styles.js +126 -0
  116. package/dist/esm/main/main.template.js +116 -11
  117. package/dist/esm/state/debug-event-log.js +28 -1
  118. package/dist/esm/state/decision-surface.js +209 -0
  119. package/dist/esm/state/interaction-context.js +89 -1
  120. package/dist/esm/state/interaction-registry.js +33 -0
  121. package/dist/esm/state/persistence/session-persister.js +23 -11
  122. package/dist/esm/state/persistence/session-snapshot.js +1 -1
  123. package/dist/esm/state/run-steps.js +77 -0
  124. package/dist/esm/types/interaction-surface.js +1 -0
  125. package/dist/esm/utils/context-tokens.js +23 -9
  126. package/dist/esm/utils/delegated-message.js +15 -0
  127. package/dist/esm/utils/history-transform.js +18 -7
  128. package/dist/esm/utils/preserved-user-messages.js +31 -0
  129. package/dist/react.cjs +7 -0
  130. package/dist/react.mjs +6 -0
  131. package/docs/styling.md +1 -1
  132. package/package.json +17 -17
  133. package/sandbox/README.md +38 -2
  134. package/sandbox/autopilot.ts +358 -0
  135. package/sandbox/controls.ts +24 -1
  136. package/sandbox/fixtures.ts +2 -0
  137. package/sandbox/sandbox.ts +188 -1
@@ -60,6 +60,7 @@ __export(chat_driver_node_exports, {
60
60
  buildTimelineEntries: () => buildTimelineEntries,
61
61
  clearSession: () => clearSession,
62
62
  defineAgent: () => defineAgent,
63
+ defineAutopilot: () => defineAutopilot,
63
64
  defineStatefulAgent: () => defineStatefulAgent,
64
65
  emptyUsage: () => emptyUsage,
65
66
  friendlyFallbackAgent: () => friendlyFallbackAgent,
@@ -3961,6 +3962,7 @@ var AgenticActivityBus = class {
3961
3962
  const inBrowser = typeof window !== "undefined" && typeof BroadcastChannel !== "undefined";
3962
3963
  if (this.crossTabEvents.size === 0 || !this.channelNameFactory || !inBrowser) return;
3963
3964
  this.channel = new BroadcastChannel(this.channelNameFactory());
3965
+ this.channel.unref?.();
3964
3966
  this.channel.onmessage = (ev) => {
3965
3967
  const { type, detail } = ev.data ?? {};
3966
3968
  if (type) this.notifyLocal(type, detail);
@@ -4037,10 +4039,10 @@ var agenticActivityBus = new AgenticActivityBus({
4037
4039
  });
4038
4040
 
4039
4041
  // src/config/validate-providers.ts
4040
- function resolveChatProvider(registry2, providerName, agentName) {
4041
- const provider = registry2.get(providerName);
4042
+ function resolveChatProvider(registry3, providerName, agentName) {
4043
+ const provider = registry3.get(providerName);
4042
4044
  if (!provider) {
4043
- const available = registry2.names().join(", ") || "(none)";
4045
+ const available = registry3.names().join(", ") || "(none)";
4044
4046
  throw new Error(
4045
4047
  `Agent "${agentName}" references unknown provider "${providerName}". Registered: ${available}`
4046
4048
  );
@@ -4052,13 +4054,13 @@ function resolveChatProvider(registry2, providerName, agentName) {
4052
4054
  }
4053
4055
  return provider;
4054
4056
  }
4055
- function validateStaticAgentProviders(agents, registry2) {
4057
+ function validateStaticAgentProviders(agents, registry3) {
4056
4058
  const visited = /* @__PURE__ */ new Set();
4057
4059
  const walk = (agent) => {
4058
4060
  if (visited.has(agent)) return;
4059
4061
  visited.add(agent);
4060
4062
  if (typeof agent.provider === "string") {
4061
- resolveChatProvider(registry2, agent.provider, agent.name);
4063
+ resolveChatProvider(registry3, agent.provider, agent.name);
4062
4064
  }
4063
4065
  for (const sub of agent.subAgents ?? []) walk(sub);
4064
4066
  };
@@ -4109,10 +4111,36 @@ var META_EVENT_IMPORTANCE = {
4109
4111
  "interaction.outcome": "normal",
4110
4112
  // An outcome that could not be recorded: the widget goes on showing only the answer.
4111
4113
  "interaction.outcome-dropped": "high",
4114
+ // A long action's step starting, finishing, or skipped on resume because it was already done.
4115
+ "interaction.step": "low",
4116
+ // A cut checkpoint taken over after a reload, refused (by sameWork, or as malformed), or dropped
4117
+ // at flow end.
4118
+ "interaction.checkpoint": "normal",
4112
4119
  // A claimant handing a pending interaction to the person, or taking it back.
4113
4120
  "interaction.held": "normal",
4114
4121
  // A claim on a `userOnly` interaction, refused. Expected when an interceptor claims broadly.
4115
4122
  "interaction.claim-refused": "low",
4123
+ // A claimant ran one of the surface's actions: what it did, and whether that settled it.
4124
+ "interaction.invoked": "normal",
4125
+ "autopilot.claimed": "normal",
4126
+ // One per interaction the autopilot does not take, so frequent and mostly expected.
4127
+ "autopilot.declined": "low",
4128
+ "autopilot.decision-sent": "low",
4129
+ "autopilot.acted": "normal",
4130
+ "autopilot.handed-back": "normal",
4131
+ "autopilot.paused": "normal",
4132
+ "autopilot.resumed": "normal",
4133
+ // The wrapper itself failed: the journey stops being automatic without anyone choosing that.
4134
+ "autopilot.error": "high",
4135
+ // A written answer counted against the step's limit; reaching it withdraws `reply`.
4136
+ "autopilot.refinement": "normal",
4137
+ // The autopilot showed its own widget because a working turn ended needing the person.
4138
+ "autopilot.handback-presented": "normal",
4139
+ // The autopilot was given the whole visible transcript: its first decision, a rebuild, or after
4140
+ // a compaction removed the last message it saw.
4141
+ "autopilot.catch-up": "normal",
4142
+ // Autopilot spend that reached no message, so it is missing from the session total.
4143
+ "autopilot.cost-dropped": "high",
4116
4144
  "external-diagnostics.folded": "normal",
4117
4145
  "session.cleared": "normal",
4118
4146
  "session.switched": "low",
@@ -4191,6 +4219,45 @@ function clearSession(key) {
4191
4219
  registry.delete(key);
4192
4220
  }
4193
4221
 
4222
+ // src/utils/delegated-message.ts
4223
+ var DEFAULT_DELEGATE_FRAMING = "Sent on the user's behalf.";
4224
+ function delegatedFields(options) {
4225
+ if (!options?.delegate) return {};
4226
+ const cost = options.externalCostUsd;
4227
+ return {
4228
+ delegate: options.delegate,
4229
+ delegateFraming: options.delegateFraming ?? DEFAULT_DELEGATE_FRAMING,
4230
+ ...typeof cost === "number" && Number.isFinite(cost) && cost > 0 ? { externalCostUsd: cost } : {}
4231
+ };
4232
+ }
4233
+ function frameDelegatedContent(content, framing) {
4234
+ return `(${framing ?? DEFAULT_DELEGATE_FRAMING})
4235
+
4236
+ ${content}`;
4237
+ }
4238
+
4239
+ // src/utils/preserved-user-messages.ts
4240
+ function freshUserMessages(toCompact) {
4241
+ return toCompact.flatMap(
4242
+ (m) => m.role === "user" && m.content?.trim() ? [
4243
+ {
4244
+ content: m.content,
4245
+ ...m.timestamp ? { timestamp: m.timestamp } : {},
4246
+ ...m.delegate ? { delegate: m.delegate, delegateFraming: m.delegateFraming } : {}
4247
+ }
4248
+ ] : []
4249
+ );
4250
+ }
4251
+ var preservedUserLine = (u) => `User: ${u.delegate ? frameDelegatedContent(u.content, u.delegateFraming) : u.content}`;
4252
+ function verbatimUserMessagesBlock(preserved) {
4253
+ if (!preserved?.length) return "";
4254
+ return `
4255
+
4256
+ <user_messages_verbatim>
4257
+ ${preserved.map(preservedUserLine).join("\n")}
4258
+ </user_messages_verbatim>`;
4259
+ }
4260
+
4194
4261
  // src/utils/context-tokens.ts
4195
4262
  var APPROX_CHARS_PER_TOKEN = 4;
4196
4263
  var IMAGE_TOKEN_ESTIMATE = 1600;
@@ -4224,7 +4291,7 @@ function imageCount(m) {
4224
4291
  }
4225
4292
  var TEXT_ATTACHMENT_FRAMING_CHARS = 9;
4226
4293
  function wireChars(m) {
4227
- let chars = m.content?.length ?? 0;
4294
+ let chars = (m.content?.length ?? 0) + verbatimUserMessagesBlock(m.compaction?.preservedUserMessages).length;
4228
4295
  for (const attachment of m.attachments ?? []) {
4229
4296
  if (attachment.kind === "image") continue;
4230
4297
  chars += TEXT_ATTACHMENT_FRAMING_CHARS + attachment.name.length + (attachment.content?.length ?? 0);
@@ -4313,8 +4380,11 @@ function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKE
4313
4380
  const { estimates, overhead, total } = calibrate(history, preambleTokens);
4314
4381
  let tail = 0;
4315
4382
  for (let i = cut; i < estimates.length; i += 1) tail += estimates[i];
4383
+ const kept = Math.ceil(
4384
+ verbatimUserMessagesBlock(freshUserMessages(history.slice(0, cut))).length / APPROX_CHARS_PER_TOKEN
4385
+ );
4316
4386
  const tokensBefore = total;
4317
- const tokensAfter = overhead + summaryTokens + tail;
4387
+ const tokensAfter = overhead + summaryTokens + kept + tail;
4318
4388
  return {
4319
4389
  cut,
4320
4390
  compactedCount: cut,
@@ -4324,14 +4394,6 @@ function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKE
4324
4394
  };
4325
4395
  }
4326
4396
 
4327
- // src/utils/delegated-message.ts
4328
- var DEFAULT_DELEGATE_FRAMING = "Sent on the user's behalf.";
4329
- function frameDelegatedContent(content, framing) {
4330
- return `(${framing ?? DEFAULT_DELEGATE_FRAMING})
4331
-
4332
- ${content}`;
4333
- }
4334
-
4335
4397
  // src/utils/logger.ts
4336
4398
  var logger2 = createLogger("ai-assistant");
4337
4399
 
@@ -4773,7 +4835,12 @@ function planCompaction(history, options = {}) {
4773
4835
  function renderMessageForSummary(m) {
4774
4836
  switch (m.role) {
4775
4837
  case "compacted-summary":
4776
- return `[Earlier summary] ${m.content}`;
4838
+ return [
4839
+ `[Earlier summary] ${m.content}`,
4840
+ ...(m.compaction?.preservedUserMessages ?? []).map(
4841
+ (u) => `[Kept word for word with it] ${preservedUserLine(u)}`
4842
+ )
4843
+ ].join("\n");
4777
4844
  case "user":
4778
4845
  if (!m.content) return "";
4779
4846
  return `User: ${m.delegate ? frameDelegatedContent(m.content, m.delegateFraming) : m.content}`;
@@ -4812,14 +4879,16 @@ function normalizeForProvider(history) {
4812
4879
  return { ...m, content: frameDelegatedContent(m.content ?? "", m.delegateFraming) };
4813
4880
  }
4814
4881
  if (m.role !== "compacted-summary") return m;
4882
+ const preserved = m.compaction?.preservedUserMessages ?? [];
4883
+ const verbatim = verbatimUserMessagesBlock(preserved);
4815
4884
  return {
4816
4885
  ...m,
4817
4886
  role: "user",
4818
4887
  content: `<conversation_summary>
4819
4888
  ${m.content}
4820
- </conversation_summary>
4889
+ </conversation_summary>${verbatim}
4821
4890
 
4822
- The above summarizes earlier parts of this conversation that were compacted. Continue naturally from it.`,
4891
+ The above summarizes earlier parts of this conversation that were compacted` + (preserved.length ? ", with the user's own messages from them kept word for word" : "") + ". Continue naturally from it.",
4823
4892
  compaction: void 0
4824
4893
  };
4825
4894
  });
@@ -5280,19 +5349,222 @@ var FoldController = class {
5280
5349
  }
5281
5350
  };
5282
5351
 
5352
+ // src/state/decision-surface.ts
5353
+ var SELECT_ACTION = "select";
5354
+ var REPLY_ACTION = "reply";
5355
+ var normalise = (option) => typeof option === "string" ? { id: option, label: option } : { id: option.id ?? option.label, label: option.label, enabledWhen: option.enabledWhen };
5356
+ var isEnabled = (option) => {
5357
+ try {
5358
+ return option.enabledWhen?.() ?? true;
5359
+ } catch {
5360
+ return false;
5361
+ }
5362
+ };
5363
+ function buildDecisionSurface(spec) {
5364
+ const options = spec.options.map(normalise);
5365
+ const ids = /* @__PURE__ */ new Set();
5366
+ for (const { id } of options) {
5367
+ if (ids.has(id)) throw new Error(`exposeDecision: two options share the id "${id}".`);
5368
+ if (spec.multi && id === SELECT_ACTION || spec.freeText && id === REPLY_ACTION) {
5369
+ throw new Error(`exposeDecision: an option cannot use the reserved id "${id}".`);
5370
+ }
5371
+ ids.add(id);
5372
+ }
5373
+ const byIdOrLabel = (key) => typeof key === "string" ? options.find((o) => o.id === key || o.label === key) : void 0;
5374
+ const actions = () => {
5375
+ const enabled = options.filter(isEnabled);
5376
+ return [
5377
+ ...options.map((o) => ({
5378
+ name: o.id,
5379
+ label: o.label,
5380
+ finishes: true,
5381
+ enabled: isEnabled(o)
5382
+ })),
5383
+ ...spec.multi ? [
5384
+ {
5385
+ name: SELECT_ACTION,
5386
+ finishes: true,
5387
+ enabled: enabled.length > 0,
5388
+ args: "options"
5389
+ }
5390
+ ] : [],
5391
+ ...spec.freeText ? [{ name: REPLY_ACTION, finishes: true, enabled: true, args: "text" }] : []
5392
+ ];
5393
+ };
5394
+ const answer = (a) => () => spec.choose(a);
5395
+ return {
5396
+ prompt: spec.prompt,
5397
+ facts: () => spec.facts?.() ?? {},
5398
+ actions,
5399
+ isReady: () => actions().some((a) => a.finishes && a.enabled),
5400
+ prepare(name, args) {
5401
+ if (spec.multi && name === SELECT_ACTION) {
5402
+ const picked = args?.options;
5403
+ if (!Array.isArray(picked) || picked.length === 0) {
5404
+ return {
5405
+ error: "invalid-args",
5406
+ message: "`select` takes { options: string[] }, not empty."
5407
+ };
5408
+ }
5409
+ const chosen = picked.map(byIdOrLabel);
5410
+ const unknown = picked.filter((_, i) => !chosen[i]);
5411
+ if (unknown.length) {
5412
+ return { error: "invalid-args", message: `Not options here: ${unknown.join(", ")}.` };
5413
+ }
5414
+ const disabled = chosen.filter((o) => !isEnabled(o));
5415
+ if (disabled.length) {
5416
+ return {
5417
+ error: "disabled",
5418
+ message: `Not available right now: ${disabled.map((o) => o.label).join(", ")}.`
5419
+ };
5420
+ }
5421
+ const picks = chosen;
5422
+ return {
5423
+ choice: picks.map((o) => o.label).join(", "),
5424
+ finishes: true,
5425
+ run: answer({ kind: "options", options: picks.map((o) => o.id) })
5426
+ };
5427
+ }
5428
+ if (spec.freeText && name === REPLY_ACTION) {
5429
+ const text = args?.text;
5430
+ if (typeof text !== "string" || !text.trim()) {
5431
+ return { error: "invalid-args", message: "`reply` takes { text: string }, not empty." };
5432
+ }
5433
+ return { choice: text, finishes: true, run: answer({ kind: "text", text }) };
5434
+ }
5435
+ const option = options.find((o) => o.id === name);
5436
+ if (!option) {
5437
+ return { error: "unknown-action", message: `No action "${name}" here.` };
5438
+ }
5439
+ if (!isEnabled(option)) {
5440
+ return { error: "disabled", message: `"${option.label}" is not available right now.` };
5441
+ }
5442
+ return {
5443
+ choice: option.label,
5444
+ finishes: true,
5445
+ run: answer({ kind: "option", option: option.id })
5446
+ };
5447
+ }
5448
+ };
5449
+ }
5450
+ var CHOICE_CHARS = 300;
5451
+ var matchesType = (value, type) => {
5452
+ switch (type) {
5453
+ case "string":
5454
+ return typeof value === "string";
5455
+ case "number":
5456
+ case "integer":
5457
+ return typeof value === "number" && (type === "number" || Number.isInteger(value));
5458
+ case "boolean":
5459
+ return typeof value === "boolean";
5460
+ case "array":
5461
+ return Array.isArray(value);
5462
+ case "object":
5463
+ return !!value && typeof value === "object" && !Array.isArray(value);
5464
+ default:
5465
+ return true;
5466
+ }
5467
+ };
5468
+ function checkArgs(args, schema) {
5469
+ if (!schema) return void 0;
5470
+ if (!matchesType(args, "object")) return "its arguments must be an object.";
5471
+ const record = args;
5472
+ const required = schema.required ?? [];
5473
+ const missing = required.filter((key) => record[key] === void 0);
5474
+ if (missing.length) return `missing: ${missing.join(", ")}.`;
5475
+ const properties = schema.properties ?? {};
5476
+ const wrong = Object.entries(properties).filter(([key, prop]) => record[key] !== void 0 && !matchesType(record[key], prop?.type)).map(([key, prop]) => `${key} should be ${String(prop?.type)}`);
5477
+ return wrong.length ? `${wrong.join("; ")}.` : void 0;
5478
+ }
5479
+ function buildFullSurface(spec) {
5480
+ const guard = (enabledWhen) => {
5481
+ try {
5482
+ return enabledWhen?.() ?? true;
5483
+ } catch {
5484
+ return false;
5485
+ }
5486
+ };
5487
+ const actions = () => Object.entries(spec.actions).map(([name, a]) => ({
5488
+ name,
5489
+ finishes: !!a.finishes,
5490
+ enabled: guard(a.enabledWhen),
5491
+ description: a.description,
5492
+ ...a.argsSchema ? { args: "fields", argsSchema: a.argsSchema } : {}
5493
+ }));
5494
+ return {
5495
+ prompt: spec.prompt,
5496
+ facts: () => ({}),
5497
+ describe: () => spec.describe(),
5498
+ views: spec.views,
5499
+ read: (view) => spec.read(view),
5500
+ actions,
5501
+ isReady: () => actions().some((a) => a.enabled),
5502
+ prepare(name, args) {
5503
+ const action = Object.prototype.hasOwnProperty.call(spec.actions, name) ? spec.actions[name] : void 0;
5504
+ if (!action) return { error: "unknown-action", message: `No action "${name}" here.` };
5505
+ if (!guard(action.enabledWhen)) {
5506
+ return { error: "disabled", message: `"${name}" is not available right now.` };
5507
+ }
5508
+ const problem = checkArgs(args ?? {}, action.argsSchema);
5509
+ if (problem) return { error: "invalid-args", message: `"${name}": ${problem}` };
5510
+ const shown = args && Object.keys(args).length ? ` ${JSON.stringify(args)}` : "";
5511
+ return {
5512
+ choice: `${name}${shown}`.slice(0, CHOICE_CHARS),
5513
+ finishes: !!action.finishes,
5514
+ run: () => action.run(args ?? {})
5515
+ };
5516
+ }
5517
+ };
5518
+ }
5519
+
5283
5520
  // src/state/interaction-context.ts
5284
- function createInteractionContext(interactionId) {
5521
+ function createInteractionContext(interactionId, deps = {}) {
5285
5522
  const state = {};
5286
5523
  const onceCache = /* @__PURE__ */ new Map();
5287
5524
  const listeners = /* @__PURE__ */ new Set();
5288
5525
  const disposers = /* @__PURE__ */ new Set();
5526
+ const readyListeners = /* @__PURE__ */ new Set();
5289
5527
  let disposed = false;
5528
+ let settled = false;
5290
5529
  let heldBy = "user";
5530
+ let controller;
5531
+ let surface;
5532
+ let lastActivityAt = Date.now();
5533
+ let checkpoint = deps.checkpoint;
5534
+ const checkReady = () => {
5535
+ if (disposed || !surface || readyListeners.size === 0 || !surface.isReady()) return;
5536
+ const waiting = [...readyListeners];
5537
+ readyListeners.clear();
5538
+ for (const fn of waiting) {
5539
+ try {
5540
+ fn();
5541
+ } catch (e) {
5542
+ logger2.error("InteractionContext onSurfaceReady callback threw", e);
5543
+ }
5544
+ }
5545
+ };
5291
5546
  const context = {
5292
5547
  interactionId,
5293
5548
  get heldBy() {
5294
5549
  return heldBy;
5295
5550
  },
5551
+ get controller() {
5552
+ return controller;
5553
+ },
5554
+ get surface() {
5555
+ return surface;
5556
+ },
5557
+ get lastActivityAt() {
5558
+ return lastActivityAt;
5559
+ },
5560
+ get checkpoint() {
5561
+ return checkpoint;
5562
+ },
5563
+ saveCheckpoint(next) {
5564
+ if (disposed) return;
5565
+ checkpoint = next;
5566
+ deps.saveCheckpoint?.(next);
5567
+ },
5296
5568
  state,
5297
5569
  once(key, factory) {
5298
5570
  if (onceCache.has(key)) return onceCache.get(key);
@@ -5307,6 +5579,7 @@ function createInteractionContext(interactionId) {
5307
5579
  return value;
5308
5580
  },
5309
5581
  notify() {
5582
+ lastActivityAt = Date.now();
5310
5583
  for (const listener of listeners) {
5311
5584
  try {
5312
5585
  listener();
@@ -5314,6 +5587,7 @@ function createInteractionContext(interactionId) {
5314
5587
  logger2.error("InteractionContext subscriber threw during notify()", e);
5315
5588
  }
5316
5589
  }
5590
+ checkReady();
5317
5591
  },
5318
5592
  subscribe(listener) {
5319
5593
  listeners.add(listener);
@@ -5327,6 +5601,36 @@ function createInteractionContext(interactionId) {
5327
5601
  return;
5328
5602
  }
5329
5603
  disposers.add(fn);
5604
+ },
5605
+ resolve(result) {
5606
+ if (settled || disposed) return;
5607
+ settled = true;
5608
+ deps.resolve?.(result);
5609
+ },
5610
+ exposeDecision(decision) {
5611
+ if (disposed) return;
5612
+ surface = buildDecisionSurface(decision);
5613
+ context.notify();
5614
+ },
5615
+ exposeSurface(spec) {
5616
+ if (disposed) return;
5617
+ surface = buildFullSurface(spec);
5618
+ context.notify();
5619
+ },
5620
+ onSurfaceReady(fn) {
5621
+ if (disposed) return () => void 0;
5622
+ if (surface?.isReady()) {
5623
+ fn();
5624
+ return () => void 0;
5625
+ }
5626
+ readyListeners.add(fn);
5627
+ return () => {
5628
+ readyListeners.delete(fn);
5629
+ };
5630
+ },
5631
+ claimantOwns(action) {
5632
+ if (heldBy === "user" || !surface) return false;
5633
+ return surface.actions().some((a) => a.name === action && a.enabled);
5330
5634
  }
5331
5635
  };
5332
5636
  return {
@@ -5343,6 +5647,7 @@ function createInteractionContext(interactionId) {
5343
5647
  }
5344
5648
  disposers.clear();
5345
5649
  listeners.clear();
5650
+ readyListeners.clear();
5346
5651
  onceCache.clear();
5347
5652
  },
5348
5653
  setHeldBy(holder) {
@@ -5350,12 +5655,70 @@ function createInteractionContext(interactionId) {
5350
5655
  heldBy = holder;
5351
5656
  context.notify();
5352
5657
  return true;
5658
+ },
5659
+ setController(value) {
5660
+ controller = value;
5661
+ },
5662
+ get surface() {
5663
+ return surface;
5353
5664
  }
5354
5665
  };
5355
5666
  }
5356
5667
 
5668
+ // src/state/interaction-registry.ts
5669
+ var registry2 = /* @__PURE__ */ new Map();
5670
+ function getInteractionRegistration(name) {
5671
+ return registry2.get(name);
5672
+ }
5673
+
5674
+ // src/state/run-steps.ts
5675
+ var recorders = /* @__PURE__ */ new WeakMap();
5676
+ function bindStepRecorder(ctx, record, refused) {
5677
+ recorders.set(ctx, { step: record, refused });
5678
+ }
5679
+
5357
5680
  // src/components/chat-driver/interaction-controller.ts
5358
5681
  var DEFAULT_DECISION_FRAMING = "Decided on the user's behalf";
5682
+ var DEFAULT_EDIT_FRAMING = "Changed on the user's behalf before the user answered; the user then finished it, confirming the changes";
5683
+ var BUSY = {
5684
+ error: "busy",
5685
+ message: "Another action on this interaction is still running, so this one was not run. Once that one has returned, try this again if it still applies."
5686
+ };
5687
+ var isThenable = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
5688
+ function editOf(by, action, choice, args) {
5689
+ const copy = jsonCopy(args);
5690
+ return { by, action, choice, ...copy !== void 0 && { args: copy } };
5691
+ }
5692
+ function jsonCopy(args) {
5693
+ if (args === void 0 || args === null) return void 0;
5694
+ try {
5695
+ const copy = JSON.parse(JSON.stringify(args));
5696
+ const empty = copy === "" || Array.isArray(copy) && copy.length === 0 || copy !== null && typeof copy === "object" && Object.keys(copy).length === 0;
5697
+ return empty ? void 0 : copy;
5698
+ } catch {
5699
+ return void 0;
5700
+ }
5701
+ }
5702
+ function doneOnUsersBehalf(interaction, answeredBy) {
5703
+ const edits = interaction.edits?.length ? interaction.edits.map((edit) => edit.choice) : void 0;
5704
+ if (answeredBy && answeredBy !== "user") {
5705
+ const recorded = interaction.decision;
5706
+ return {
5707
+ decision: {
5708
+ framing: interaction.claimant?.decisionFraming ?? DEFAULT_DECISION_FRAMING,
5709
+ ...edits && { edits },
5710
+ ...recorded?.choice !== void 0 && { choice: recorded.choice },
5711
+ ...recorded?.shownToUser !== void 0 && { shownToUser: recorded.shownToUser }
5712
+ }
5713
+ };
5714
+ }
5715
+ if (answeredBy === "user" && edits) {
5716
+ return {
5717
+ edited: { framing: interaction.claimant?.editFraming ?? DEFAULT_EDIT_FRAMING, edits }
5718
+ };
5719
+ }
5720
+ return {};
5721
+ }
5359
5722
  var STOPPED = {
5360
5723
  status: "timeout",
5361
5724
  message: "The turn was stopped before this was answered."
@@ -5379,6 +5742,72 @@ var InteractionController = class {
5379
5742
  */
5380
5743
  this.interceptors = [];
5381
5744
  }
5745
+ /** See `ChatDriver.getInteractionCheckpoint`. */
5746
+ getCheckpoint() {
5747
+ const cp = this.checkpoint;
5748
+ return cp ? { componentName: cp.componentName, value: cp.value } : void 0;
5749
+ }
5750
+ /** See `ChatDriver.restoreInteractionCheckpoint`. Restored means cut: its interaction is gone. */
5751
+ restoreCheckpoint(saved) {
5752
+ this.checkpoint = saved ? { componentName: saved.componentName, value: saved.value, cut: true } : void 0;
5753
+ }
5754
+ /** Drop the checkpoint, used or not, when its flow ends. */
5755
+ clearCheckpoint(why) {
5756
+ const cp = this.checkpoint;
5757
+ if (!cp) return;
5758
+ this.checkpoint = void 0;
5759
+ recordMetaEvent(this.deps.sessionKey, "interaction.checkpoint", {
5760
+ action: "cleared",
5761
+ component: cp.componentName,
5762
+ why,
5763
+ ...cp.interactionId ? { interactionId: cp.interactionId } : {}
5764
+ });
5765
+ }
5766
+ /**
5767
+ * Hand a cut checkpoint to a new request of the same widget type, if its registration's
5768
+ * `sameWork` agrees. Only the first such request is asked: a refused checkpoint is dropped, and
5769
+ * the work starts fresh as it would have without one.
5770
+ */
5771
+ takeOverCheckpoint(componentName, data, interactionId) {
5772
+ const cut = this.checkpoint;
5773
+ if (!cut?.cut || cut.componentName !== componentName) return void 0;
5774
+ this.checkpoint = void 0;
5775
+ const sameWork = getInteractionRegistration(componentName)?.sameWork;
5776
+ let agrees = true;
5777
+ if (sameWork) {
5778
+ try {
5779
+ agrees = sameWork(cut.value, data);
5780
+ } catch (e) {
5781
+ logger2.warn(`InteractionController: "${componentName}" sameWork threw; starting fresh:`, e);
5782
+ agrees = false;
5783
+ }
5784
+ }
5785
+ recordMetaEvent(this.deps.sessionKey, "interaction.checkpoint", {
5786
+ action: agrees ? "resumed" : "refused",
5787
+ component: componentName,
5788
+ interactionId,
5789
+ ...agrees ? {} : { saved: cut.value }
5790
+ });
5791
+ if (!agrees) return void 0;
5792
+ this.checkpoint = { componentName, value: cut.value, interactionId };
5793
+ return cut.value;
5794
+ }
5795
+ /** A controller's `ctx.saveCheckpoint`: kept as JSON, the form it is persisted in. */
5796
+ saveCheckpoint(interactionId, componentName, value) {
5797
+ if (!this.pending.has(interactionId)) return;
5798
+ let copy;
5799
+ try {
5800
+ copy = JSON.parse(JSON.stringify(value) ?? "null");
5801
+ } catch (e) {
5802
+ logger2.warn(
5803
+ `InteractionController: a "${componentName}" checkpoint is not storable as JSON:`,
5804
+ e
5805
+ );
5806
+ return;
5807
+ }
5808
+ this.checkpoint = { componentName, value: copy, interactionId };
5809
+ this.deps.emit(new CustomEvent("checkpoint-saved", { detail: { interactionId } }));
5810
+ }
5382
5811
  /** See `ChatDriver.addInteractionInterceptor`. Returns a remover. */
5383
5812
  addInterceptor(interceptor) {
5384
5813
  if (!interceptor.id || interceptor.id === "user") {
@@ -5438,6 +5867,10 @@ var InteractionController = class {
5438
5867
  if (!isClaimant()) return false;
5439
5868
  const next = holder === "user" ? "user" : interceptor.id;
5440
5869
  if (!handle.setHeldBy(next)) return false;
5870
+ if (next === "user") {
5871
+ const pending = this.pending.get(handle.context.interactionId);
5872
+ if (pending && !pending.running?.finishes) pending.settledBy = void 0;
5873
+ }
5441
5874
  recordMetaEvent(this.deps.sessionKey, "interaction.held", {
5442
5875
  interactionId: handle.context.interactionId,
5443
5876
  heldBy: next,
@@ -5459,9 +5892,87 @@ var InteractionController = class {
5459
5892
  if (!pending) return false;
5460
5893
  pending.decision = { choice: decision.choice, shownToUser: decision.shownToUser };
5461
5894
  return true;
5895
+ },
5896
+ invoke: async (action, args, options) => {
5897
+ const { interactionId } = handle.context;
5898
+ const pending = this.pending.get(interactionId);
5899
+ if (!isClaimant() || !pending || handle.context.heldBy !== interceptor.id) {
5900
+ return {
5901
+ ok: false,
5902
+ error: "not-held",
5903
+ message: pending ? "This interaction is with the person now, so it cannot be acted on." : "This interaction has already ended."
5904
+ };
5905
+ }
5906
+ const { surface } = handle;
5907
+ if (!surface) {
5908
+ return { ok: false, error: "no-surface", message: "Nothing has been exposed here yet." };
5909
+ }
5910
+ if (pending.running) {
5911
+ return { ok: false, ...BUSY, actions: surface.actions() };
5912
+ }
5913
+ const prepared = surface.prepare(action, args);
5914
+ if ("error" in prepared) {
5915
+ return { ok: false, ...prepared, actions: surface.actions() };
5916
+ }
5917
+ const previous = { decision: pending.decision, settledBy: pending.settledBy };
5918
+ const restore = () => {
5919
+ if (!prepared.finishes) return;
5920
+ pending.decision = previous.decision;
5921
+ pending.settledBy = handle.context.heldBy === interceptor.id ? previous.settledBy : void 0;
5922
+ };
5923
+ if (prepared.finishes) {
5924
+ pending.decision = { choice: prepared.choice, shownToUser: options?.note };
5925
+ pending.settledBy = interceptor.id;
5926
+ }
5927
+ const content = options?.note?.trim();
5928
+ const note = content ? { content, delegate: interceptor.id } : void 0;
5929
+ const token = {};
5930
+ pending.running = { token, finishes: prepared.finishes, note };
5931
+ let output;
5932
+ try {
5933
+ const ran = prepared.run();
5934
+ output = isThenable(ran) ? await ran : ran;
5935
+ } catch (e) {
5936
+ logger2.warn(`InteractionController: action "${action}" threw:`, e);
5937
+ if (pending.settledInRun !== token) restore();
5938
+ return {
5939
+ ok: false,
5940
+ error: "threw",
5941
+ message: e instanceof Error ? e.message : String(e)
5942
+ };
5943
+ } finally {
5944
+ if (pending.running?.token === token) pending.running = void 0;
5945
+ }
5946
+ const finished = pending.settledInRun === token;
5947
+ if (!finished) {
5948
+ restore();
5949
+ if (note && this.pending.has(interactionId)) this.appendNote(note);
5950
+ (pending.edits ??= []).push(editOf(interceptor.id, action, prepared.choice, args));
5951
+ }
5952
+ recordMetaEvent(this.deps.sessionKey, "interaction.invoked", {
5953
+ interactionId,
5954
+ claimant: interceptor.id,
5955
+ action,
5956
+ finished
5957
+ });
5958
+ return {
5959
+ ok: true,
5960
+ finished,
5961
+ actions: finished ? [] : surface.actions(),
5962
+ ...output !== void 0 && !finished ? { output } : {}
5963
+ };
5462
5964
  }
5463
5965
  };
5464
5966
  }
5967
+ /** A claimant's note about its choice: a delegated, display-only message in the assistant's voice. */
5968
+ appendNote(note) {
5969
+ this.deps.appendToHistory({
5970
+ role: "assistant",
5971
+ content: note.content,
5972
+ delegate: note.delegate,
5973
+ excludeFromProvider: true
5974
+ });
5975
+ }
5465
5976
  /** See `ChatDriver.setHostInteractionRequester`. */
5466
5977
  setHostRequester(fn, recordOutcome) {
5467
5978
  this.hostRequester = fn;
@@ -5506,8 +6017,36 @@ var InteractionController = class {
5506
6017
  });
5507
6018
  }, timeoutMs) : void 0
5508
6019
  });
5509
- const handle = createInteractionContext(interactionId);
6020
+ const handle = createInteractionContext(interactionId, {
6021
+ resolve: (result) => this.resolve(interactionId, result),
6022
+ checkpoint: this.takeOverCheckpoint(componentName, data, interactionId),
6023
+ saveCheckpoint: (value) => this.saveCheckpoint(interactionId, componentName, value)
6024
+ });
5510
6025
  this.contexts.set(interactionId, handle);
6026
+ bindStepRecorder(
6027
+ handle.context,
6028
+ (step, outcome) => recordMetaEvent(this.deps.sessionKey, "interaction.step", {
6029
+ interactionId,
6030
+ component: componentName,
6031
+ step,
6032
+ outcome
6033
+ }),
6034
+ (reason, saved) => recordMetaEvent(this.deps.sessionKey, "interaction.checkpoint", {
6035
+ action: "refused",
6036
+ component: componentName,
6037
+ interactionId,
6038
+ reason,
6039
+ saved
6040
+ })
6041
+ );
6042
+ const registration = getInteractionRegistration(componentName);
6043
+ if (registration?.controller) {
6044
+ try {
6045
+ handle.setController(registration.controller(data, handle.context));
6046
+ } catch (e) {
6047
+ logger2.error(`InteractionController: the "${componentName}" controller threw:`, e);
6048
+ }
6049
+ }
5511
6050
  const heldBy = this.offer(handle, {
5512
6051
  interactionId,
5513
6052
  componentName,
@@ -5584,21 +6123,20 @@ var InteractionController = class {
5584
6123
  const costUsd = typeof result?.costUsd === "number" && Number.isFinite(result.costUsd) && result.costUsd > 0 ? result.costUsd : void 0;
5585
6124
  const unanswered = result?.status === "timeout";
5586
6125
  const answeredBy = unanswered ? void 0 : interaction.settledBy ?? "user";
5587
- const decision = answeredBy && answeredBy !== "user" ? {
5588
- framing: interaction.claimant?.decisionFraming ?? DEFAULT_DECISION_FRAMING,
5589
- ...interaction.decision?.choice !== void 0 && {
5590
- choice: interaction.decision.choice
5591
- },
5592
- ...interaction.decision?.shownToUser !== void 0 && {
5593
- shownToUser: interaction.decision.shownToUser
5594
- }
5595
- } : void 0;
6126
+ const run = answeredBy && answeredBy !== "user" ? interaction.running : void 0;
6127
+ if (run) {
6128
+ interaction.settledInRun = run.token;
6129
+ if (run.note) this.appendNote(run.note);
6130
+ }
6131
+ const edits = interaction.edits?.length ? interaction.edits : void 0;
6132
+ const { decision, edited } = doneOnUsersBehalf(interaction, answeredBy);
5596
6133
  const patched = this.deps.patchInteractionMessage(interactionId, (message) => ({
5597
6134
  ...message,
5598
6135
  interaction: {
5599
6136
  ...message.interaction,
5600
6137
  resolved: result,
5601
- ...answeredBy ? { answeredBy } : {}
6138
+ ...answeredBy ? { answeredBy } : {},
6139
+ ...edits ? { edits } : {}
5602
6140
  },
5603
6141
  ...costUsd != null ? { externalCostUsd: (message.externalCostUsd ?? 0) + costUsd } : {}
5604
6142
  }));
@@ -5618,16 +6156,23 @@ var InteractionController = class {
5618
6156
  }
5619
6157
  recordMetaEvent(this.deps.sessionKey, "interaction.resolved", {
5620
6158
  interactionId,
5621
- ...answeredBy ? { answeredBy } : {}
6159
+ ...answeredBy ? { answeredBy } : {},
6160
+ ...edits ? { edits: edits.length } : {}
5622
6161
  });
5623
6162
  this.deps.activityBus.publish("interaction-resolved", void 0);
5624
6163
  try {
5625
- interaction.onSettled?.({ answeredBy, decision });
6164
+ interaction.onSettled?.({ answeredBy, decision, ...edited ? { edited } : {} });
5626
6165
  } catch (e) {
5627
6166
  logger2.warn("InteractionController: onSettled threw:", e);
5628
6167
  }
6168
+ this.deps.emit(
6169
+ new CustomEvent("interaction-settled", {
6170
+ detail: { interactionId, ...answeredBy ? { answeredBy } : {} }
6171
+ })
6172
+ );
5629
6173
  interaction.resolve(result);
5630
6174
  this.pending.delete(interactionId);
6175
+ if (this.checkpoint?.interactionId === interactionId) this.checkpoint = void 0;
5631
6176
  this.contexts.get(interactionId)?.dispose();
5632
6177
  this.contexts.delete(interactionId);
5633
6178
  }
@@ -6024,17 +6569,30 @@ var budgetDetailOf = (e) => {
6024
6569
  ...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
6025
6570
  };
6026
6571
  };
6027
- function withDelegatedDecisions(result, content, decisions, hasImages) {
6028
- if (decisions.length === 0) return content;
6572
+ function withDelegatedDecisions(result, content, done, hasImages) {
6573
+ const { decided, edited } = done;
6574
+ if (decided.length === 0 && edited.length === 0) return content;
6029
6575
  if (!hasImages && result !== null && typeof result === "object" && !Array.isArray(result)) {
6030
- return JSON.stringify({ ...result, decidedOnUsersBehalf: decisions });
6576
+ return JSON.stringify({
6577
+ ...result,
6578
+ ...decided.length ? { decidedOnUsersBehalf: decided } : {},
6579
+ ...edited.length ? { editedOnUsersBehalf: edited } : {}
6580
+ });
6031
6581
  }
6032
- return [content, ...decisions.map(describeDelegatedDecision)].join("\n\n");
6582
+ return [
6583
+ content,
6584
+ ...decided.map(describeDelegatedDecision),
6585
+ ...edited.map(describeDelegatedEdits)
6586
+ ].join("\n\n");
6587
+ }
6588
+ function describeDelegatedEdits(edited) {
6589
+ return `${edited.framing}: ${edited.edits.map((edit) => `"${edit}"`).join(", ")}.`;
6033
6590
  }
6034
6591
  function describeDelegatedDecision(decision) {
6035
6592
  const choice = decision.choice !== void 0 ? `: "${decision.choice}"` : "";
6036
6593
  const shown = decision.shownToUser ? ` What the user was shown, as your words: "${decision.shownToUser}"` : " Nothing was shown to the user about it.";
6037
- return `${decision.framing}${choice}.${shown}`;
6594
+ const edited = decision.edits?.length ? ` Before deciding it changed the widget: ${decision.edits.map((edit) => `"${edit}"`).join(", ")}.` : "";
6595
+ return `${decision.framing}${choice}.${shown}${edited}`;
6038
6596
  }
6039
6597
  function inFlightToolCallsAt(history) {
6040
6598
  for (let i = history.length - 1; i >= 0; i -= 1) {
@@ -6781,6 +7339,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
6781
7339
  * release, then the active agent.
6782
7340
  */
6783
7341
  dispatchAgentTransition(t) {
7342
+ if (t.flowOwnerChangedTo === null) this.interactions.clearCheckpoint("flow-ended");
6784
7343
  if (t.flowOwnerChangedTo !== void 0) {
6785
7344
  this.dispatchEvent(new CustomEvent("flow-owner-changed", { detail: t.flowOwnerChangedTo }));
6786
7345
  }
@@ -7365,7 +7924,11 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7365
7924
  // below, taking its per-message cost/token fields with it — so a host total
7366
7925
  // re-derived from the transcript would otherwise fall by exactly this much
7367
7926
  // on every compaction. Summarising a turn does not refund what it cost.
7368
- rolledUpUsage: sumUsage(toCompact)
7927
+ rolledUpUsage: sumUsage(toCompact),
7928
+ // The user's own words survive word for word, beside the paraphrase (GENC-1660, C2): this
7929
+ // range's own only. An earlier summary's kept messages are not carried forward; the
7930
+ // summariser read them with it and folded them into this paraphrase.
7931
+ preservedUserMessages: freshUserMessages(toCompact)
7369
7932
  }
7370
7933
  };
7371
7934
  this.loadHistory([summaryMessage, ...tail]);
@@ -7537,6 +8100,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7537
8100
  getInteractionContext(interactionId) {
7538
8101
  return this.interactions.context(interactionId);
7539
8102
  }
8103
+ /** {@inheritDoc AiDriver.getInteractionCheckpoint} */
8104
+ getInteractionCheckpoint() {
8105
+ return this.interactions.getCheckpoint();
8106
+ }
8107
+ /** {@inheritDoc AiDriver.restoreInteractionCheckpoint} */
8108
+ restoreInteractionCheckpoint(saved) {
8109
+ this.interactions.restoreCheckpoint(saved);
8110
+ }
7540
8111
  /**
7541
8112
  * Replace the history message owning `interactionId`. `false` when it is no longer in history.
7542
8113
  * Does not announce the change — the interaction paths differ on whether they should.
@@ -7579,7 +8150,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7579
8150
  role: "user",
7580
8151
  content: userInput,
7581
8152
  attachments,
7582
- ...delegate ? { delegate, delegateFraming } : {}
8153
+ // Who sent it, and what writing it cost when that was not the working agent's own spend.
8154
+ ...delegatedFields(options)
7583
8155
  });
7584
8156
  this.turnStartedAt = Date.now();
7585
8157
  recordMetaEvent(this.sessionKey, "turn.start", {
@@ -7645,7 +8217,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7645
8217
  * kept on a seven-way fan-out, reporting ~2.3x under the real cost, with the
7646
8218
  * error growing as the fan-out widens.
7647
8219
  */
7648
- buildHandlerContext(activeToolCallId, traceCapture, decisions) {
8220
+ buildHandlerContext(activeToolCallId, traceCapture, done) {
7649
8221
  let opened;
7650
8222
  let recorded = false;
7651
8223
  const capabilities2 = this.buildCapabilities();
@@ -7659,9 +8231,11 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7659
8231
  opened = interactionId;
7660
8232
  },
7661
8233
  void 0,
7662
- // An interaction an interceptor answered is reported in THIS call's result (GENC-1660).
8234
+ // What an interceptor did to an interaction is reported in THIS call's result
8235
+ // (GENC-1660): its answer, or the edits it made before the person answered.
7663
8236
  (settled) => {
7664
- if (settled.decision) decisions?.push(settled.decision);
8237
+ if (settled.decision) done?.decided.push(settled.decision);
8238
+ if (settled.edited) done?.edited.push(settled.edited);
7665
8239
  }
7666
8240
  ),
7667
8241
  recordInteractionOutcome: (outcome) => {
@@ -8498,17 +9072,17 @@ ${tailBody}
8498
9072
  }
8499
9073
  const traceCapture = { traces: [] };
8500
9074
  const capturedTrace = () => traceCapture.traces.length ? traceCapture.traces.flat() : void 0;
8501
- const decisions = [];
9075
+ const done = { decided: [], edited: [] };
8502
9076
  try {
8503
9077
  const result = await handler(
8504
9078
  tc.args,
8505
- this.buildHandlerContext(tc.id, traceCapture, decisions)
9079
+ this.buildHandlerContext(tc.id, traceCapture, done)
8506
9080
  );
8507
9081
  const resultImages = toolResultImages(result);
8508
9082
  const content = withDelegatedDecisions(
8509
9083
  result,
8510
9084
  resultImages ? typeof result.content === "string" ? result.content : JSON.stringify({ ...result, attachments: void 0 }) : typeof result === "string" ? result : JSON.stringify(result),
8511
- decisions,
9085
+ done,
8512
9086
  !!resultImages
8513
9087
  );
8514
9088
  executedById.set(tc.id, {
@@ -8718,8 +9292,10 @@ var AI_DRIVER_EVENT_TYPES = Object.keys({
8718
9292
  "sub-agent-stop": true,
8719
9293
  "interaction-start": true,
8720
9294
  "interaction-stop": true,
9295
+ "interaction-settled": true,
8721
9296
  "provider-changed": true,
8722
9297
  "external-cost-recorded": true,
9298
+ "checkpoint-saved": true,
8723
9299
  "agent-changed": true,
8724
9300
  "agent-released": true,
8725
9301
  "flow-owner-changed": true,
@@ -8815,6 +9391,12 @@ var BaseDriverDecorator = class _BaseDriverDecorator extends EventTarget {
8815
9391
  getFlowOwner() {
8816
9392
  return this.inner.getFlowOwner?.() ?? null;
8817
9393
  }
9394
+ getInteractionCheckpoint() {
9395
+ return this.inner.getInteractionCheckpoint?.();
9396
+ }
9397
+ restoreInteractionCheckpoint(saved) {
9398
+ this.inner.restoreInteractionCheckpoint?.(saved);
9399
+ }
8818
9400
  /**
8819
9401
  * Forwarded, so an interceptor installed on any layer lands on the driver that opens the
8820
9402
  * interactions. A decorator that answers interactions installs its own on `this.inner`.
@@ -9049,10 +9631,7 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
9049
9631
  return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
9050
9632
  }
9051
9633
  async sendMessage(input, attachments, options) {
9052
- const delegated = options?.delegate ? {
9053
- delegate: options.delegate,
9054
- delegateFraming: options.delegateFraming ?? DEFAULT_DELEGATE_FRAMING
9055
- } : {};
9634
+ const delegated = delegatedFields(options);
9056
9635
  try {
9057
9636
  return await this.runOrchestratedTurn(input, attachments, options, delegated);
9058
9637
  } catch (e) {
@@ -9185,7 +9764,25 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
9185
9764
  this.cancelled = false;
9186
9765
  await this.installAgent(agent);
9187
9766
  if (!resumeNote || agent.resumeOnRestore === false || this.cancelled) return void 0;
9188
- return this.chatDriver.continueFromHistory(void 0, { tailNote: resumeNote });
9767
+ const result = await this.chatDriver.continueFromHistory(void 0, { tailNote: resumeNote });
9768
+ await this.releaseIfRequested();
9769
+ return result;
9770
+ }
9771
+ /**
9772
+ * A continuation outside a routed turn — a decorator carrying the agent on, say — can finish the
9773
+ * flow as surely as a turn can, so a release it requests is acted on here too. Before
9774
+ * (GENC-1660), only `sendMessage` checked, and a flow finished by a continuation stayed locked.
9775
+ */
9776
+ async continueFromHistory(transientPrimer, options) {
9777
+ const result = await this.chatDriver.continueFromHistory(transientPrimer, options);
9778
+ await this.releaseIfRequested();
9779
+ return result;
9780
+ }
9781
+ /** Release the flow if the turn that just ended asked to, and an agent is still active to release. */
9782
+ async releaseIfRequested() {
9783
+ if (this.chatDriver.getAgentReleaseRequested() && this.agentSession.getActive()) {
9784
+ await this.releaseActiveAgent();
9785
+ }
9189
9786
  }
9190
9787
  /**
9191
9788
  * {@inheritDoc AiDriver.getContextReserve}
@@ -9648,6 +10245,13 @@ function defineStatefulAgent(opts) {
9648
10245
  notResumableMessage: opts.notResumableMessage,
9649
10246
  resumeOnRestore: opts.resumeOnRestore,
9650
10247
  contextReserve: opts.contextReserve,
10248
+ // Bound to the live state, so the autopilot's resolvers see what this agent's do (GENC-1660).
10249
+ ...opts.autopilot && {
10250
+ autopilot: {
10251
+ config: opts.autopilot,
10252
+ state: () => state
10253
+ }
10254
+ },
9651
10255
  onActivate: async (ctx) => {
9652
10256
  state = await opts.init(ctx);
9653
10257
  if (opts.toolHandlers) {
@@ -9688,6 +10292,9 @@ function defineStatefulAgent(opts) {
9688
10292
  excludeFromClassifier: opts.excludeFromClassifier
9689
10293
  };
9690
10294
  }
10295
+ function defineAutopilot(config) {
10296
+ return config;
10297
+ }
9691
10298
 
9692
10299
  // src/config/fallback-agents.ts
9693
10300
  var friendlyFallbackAgent = {
@@ -9905,6 +10512,7 @@ function withFreshMetaSnapshot(stored, fresh) {
9905
10512
  buildTimelineEntries,
9906
10513
  clearSession,
9907
10514
  defineAgent,
10515
+ defineAutopilot,
9908
10516
  defineStatefulAgent,
9909
10517
  emptyUsage,
9910
10518
  friendlyFallbackAgent,