@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
@@ -3879,6 +3879,7 @@ var AgenticActivityBus = class {
3879
3879
  const inBrowser = typeof window !== "undefined" && typeof BroadcastChannel !== "undefined";
3880
3880
  if (this.crossTabEvents.size === 0 || !this.channelNameFactory || !inBrowser) return;
3881
3881
  this.channel = new BroadcastChannel(this.channelNameFactory());
3882
+ this.channel.unref?.();
3882
3883
  this.channel.onmessage = (ev) => {
3883
3884
  const { type, detail } = ev.data ?? {};
3884
3885
  if (type) this.notifyLocal(type, detail);
@@ -3955,10 +3956,10 @@ var agenticActivityBus = new AgenticActivityBus({
3955
3956
  });
3956
3957
 
3957
3958
  // src/config/validate-providers.ts
3958
- function resolveChatProvider(registry2, providerName, agentName) {
3959
- const provider = registry2.get(providerName);
3959
+ function resolveChatProvider(registry3, providerName, agentName) {
3960
+ const provider = registry3.get(providerName);
3960
3961
  if (!provider) {
3961
- const available = registry2.names().join(", ") || "(none)";
3962
+ const available = registry3.names().join(", ") || "(none)";
3962
3963
  throw new Error(
3963
3964
  `Agent "${agentName}" references unknown provider "${providerName}". Registered: ${available}`
3964
3965
  );
@@ -3970,13 +3971,13 @@ function resolveChatProvider(registry2, providerName, agentName) {
3970
3971
  }
3971
3972
  return provider;
3972
3973
  }
3973
- function validateStaticAgentProviders(agents, registry2) {
3974
+ function validateStaticAgentProviders(agents, registry3) {
3974
3975
  const visited = /* @__PURE__ */ new Set();
3975
3976
  const walk = (agent) => {
3976
3977
  if (visited.has(agent)) return;
3977
3978
  visited.add(agent);
3978
3979
  if (typeof agent.provider === "string") {
3979
- resolveChatProvider(registry2, agent.provider, agent.name);
3980
+ resolveChatProvider(registry3, agent.provider, agent.name);
3980
3981
  }
3981
3982
  for (const sub of agent.subAgents ?? []) walk(sub);
3982
3983
  };
@@ -4027,10 +4028,36 @@ var META_EVENT_IMPORTANCE = {
4027
4028
  "interaction.outcome": "normal",
4028
4029
  // An outcome that could not be recorded: the widget goes on showing only the answer.
4029
4030
  "interaction.outcome-dropped": "high",
4031
+ // A long action's step starting, finishing, or skipped on resume because it was already done.
4032
+ "interaction.step": "low",
4033
+ // A cut checkpoint taken over after a reload, refused (by sameWork, or as malformed), or dropped
4034
+ // at flow end.
4035
+ "interaction.checkpoint": "normal",
4030
4036
  // A claimant handing a pending interaction to the person, or taking it back.
4031
4037
  "interaction.held": "normal",
4032
4038
  // A claim on a `userOnly` interaction, refused. Expected when an interceptor claims broadly.
4033
4039
  "interaction.claim-refused": "low",
4040
+ // A claimant ran one of the surface's actions: what it did, and whether that settled it.
4041
+ "interaction.invoked": "normal",
4042
+ "autopilot.claimed": "normal",
4043
+ // One per interaction the autopilot does not take, so frequent and mostly expected.
4044
+ "autopilot.declined": "low",
4045
+ "autopilot.decision-sent": "low",
4046
+ "autopilot.acted": "normal",
4047
+ "autopilot.handed-back": "normal",
4048
+ "autopilot.paused": "normal",
4049
+ "autopilot.resumed": "normal",
4050
+ // The wrapper itself failed: the journey stops being automatic without anyone choosing that.
4051
+ "autopilot.error": "high",
4052
+ // A written answer counted against the step's limit; reaching it withdraws `reply`.
4053
+ "autopilot.refinement": "normal",
4054
+ // The autopilot showed its own widget because a working turn ended needing the person.
4055
+ "autopilot.handback-presented": "normal",
4056
+ // The autopilot was given the whole visible transcript: its first decision, a rebuild, or after
4057
+ // a compaction removed the last message it saw.
4058
+ "autopilot.catch-up": "normal",
4059
+ // Autopilot spend that reached no message, so it is missing from the session total.
4060
+ "autopilot.cost-dropped": "high",
4034
4061
  "external-diagnostics.folded": "normal",
4035
4062
  "session.cleared": "normal",
4036
4063
  "session.switched": "low",
@@ -4109,6 +4136,45 @@ function clearSession(key) {
4109
4136
  registry.delete(key);
4110
4137
  }
4111
4138
 
4139
+ // src/utils/delegated-message.ts
4140
+ var DEFAULT_DELEGATE_FRAMING = "Sent on the user's behalf.";
4141
+ function delegatedFields(options) {
4142
+ if (!options?.delegate) return {};
4143
+ const cost = options.externalCostUsd;
4144
+ return {
4145
+ delegate: options.delegate,
4146
+ delegateFraming: options.delegateFraming ?? DEFAULT_DELEGATE_FRAMING,
4147
+ ...typeof cost === "number" && Number.isFinite(cost) && cost > 0 ? { externalCostUsd: cost } : {}
4148
+ };
4149
+ }
4150
+ function frameDelegatedContent(content, framing) {
4151
+ return `(${framing ?? DEFAULT_DELEGATE_FRAMING})
4152
+
4153
+ ${content}`;
4154
+ }
4155
+
4156
+ // src/utils/preserved-user-messages.ts
4157
+ function freshUserMessages(toCompact) {
4158
+ return toCompact.flatMap(
4159
+ (m) => m.role === "user" && m.content?.trim() ? [
4160
+ {
4161
+ content: m.content,
4162
+ ...m.timestamp ? { timestamp: m.timestamp } : {},
4163
+ ...m.delegate ? { delegate: m.delegate, delegateFraming: m.delegateFraming } : {}
4164
+ }
4165
+ ] : []
4166
+ );
4167
+ }
4168
+ var preservedUserLine = (u) => `User: ${u.delegate ? frameDelegatedContent(u.content, u.delegateFraming) : u.content}`;
4169
+ function verbatimUserMessagesBlock(preserved) {
4170
+ if (!preserved?.length) return "";
4171
+ return `
4172
+
4173
+ <user_messages_verbatim>
4174
+ ${preserved.map(preservedUserLine).join("\n")}
4175
+ </user_messages_verbatim>`;
4176
+ }
4177
+
4112
4178
  // src/utils/context-tokens.ts
4113
4179
  var APPROX_CHARS_PER_TOKEN = 4;
4114
4180
  var IMAGE_TOKEN_ESTIMATE = 1600;
@@ -4142,7 +4208,7 @@ function imageCount(m) {
4142
4208
  }
4143
4209
  var TEXT_ATTACHMENT_FRAMING_CHARS = 9;
4144
4210
  function wireChars(m) {
4145
- let chars = m.content?.length ?? 0;
4211
+ let chars = (m.content?.length ?? 0) + verbatimUserMessagesBlock(m.compaction?.preservedUserMessages).length;
4146
4212
  for (const attachment of m.attachments ?? []) {
4147
4213
  if (attachment.kind === "image") continue;
4148
4214
  chars += TEXT_ATTACHMENT_FRAMING_CHARS + attachment.name.length + (attachment.content?.length ?? 0);
@@ -4231,8 +4297,11 @@ function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKE
4231
4297
  const { estimates, overhead, total } = calibrate(history, preambleTokens);
4232
4298
  let tail = 0;
4233
4299
  for (let i = cut; i < estimates.length; i += 1) tail += estimates[i];
4300
+ const kept = Math.ceil(
4301
+ verbatimUserMessagesBlock(freshUserMessages(history.slice(0, cut))).length / APPROX_CHARS_PER_TOKEN
4302
+ );
4234
4303
  const tokensBefore = total;
4235
- const tokensAfter = overhead + summaryTokens + tail;
4304
+ const tokensAfter = overhead + summaryTokens + kept + tail;
4236
4305
  return {
4237
4306
  cut,
4238
4307
  compactedCount: cut,
@@ -4242,14 +4311,6 @@ function projectCompaction(history, cut, summaryTokens = COMPACTION_SUMMARY_TOKE
4242
4311
  };
4243
4312
  }
4244
4313
 
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
-
4253
4314
  // src/utils/logger.ts
4254
4315
  var logger2 = createLogger("ai-assistant");
4255
4316
 
@@ -4691,7 +4752,12 @@ function planCompaction(history, options = {}) {
4691
4752
  function renderMessageForSummary(m) {
4692
4753
  switch (m.role) {
4693
4754
  case "compacted-summary":
4694
- return `[Earlier summary] ${m.content}`;
4755
+ return [
4756
+ `[Earlier summary] ${m.content}`,
4757
+ ...(m.compaction?.preservedUserMessages ?? []).map(
4758
+ (u) => `[Kept word for word with it] ${preservedUserLine(u)}`
4759
+ )
4760
+ ].join("\n");
4695
4761
  case "user":
4696
4762
  if (!m.content) return "";
4697
4763
  return `User: ${m.delegate ? frameDelegatedContent(m.content, m.delegateFraming) : m.content}`;
@@ -4730,14 +4796,16 @@ function normalizeForProvider(history) {
4730
4796
  return { ...m, content: frameDelegatedContent(m.content ?? "", m.delegateFraming) };
4731
4797
  }
4732
4798
  if (m.role !== "compacted-summary") return m;
4799
+ const preserved = m.compaction?.preservedUserMessages ?? [];
4800
+ const verbatim = verbatimUserMessagesBlock(preserved);
4733
4801
  return {
4734
4802
  ...m,
4735
4803
  role: "user",
4736
4804
  content: `<conversation_summary>
4737
4805
  ${m.content}
4738
- </conversation_summary>
4806
+ </conversation_summary>${verbatim}
4739
4807
 
4740
- The above summarizes earlier parts of this conversation that were compacted. Continue naturally from it.`,
4808
+ 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.",
4741
4809
  compaction: void 0
4742
4810
  };
4743
4811
  });
@@ -5198,19 +5266,222 @@ var FoldController = class {
5198
5266
  }
5199
5267
  };
5200
5268
 
5269
+ // src/state/decision-surface.ts
5270
+ var SELECT_ACTION = "select";
5271
+ var REPLY_ACTION = "reply";
5272
+ var normalise = (option) => typeof option === "string" ? { id: option, label: option } : { id: option.id ?? option.label, label: option.label, enabledWhen: option.enabledWhen };
5273
+ var isEnabled = (option) => {
5274
+ try {
5275
+ return option.enabledWhen?.() ?? true;
5276
+ } catch {
5277
+ return false;
5278
+ }
5279
+ };
5280
+ function buildDecisionSurface(spec) {
5281
+ const options = spec.options.map(normalise);
5282
+ const ids = /* @__PURE__ */ new Set();
5283
+ for (const { id } of options) {
5284
+ if (ids.has(id)) throw new Error(`exposeDecision: two options share the id "${id}".`);
5285
+ if (spec.multi && id === SELECT_ACTION || spec.freeText && id === REPLY_ACTION) {
5286
+ throw new Error(`exposeDecision: an option cannot use the reserved id "${id}".`);
5287
+ }
5288
+ ids.add(id);
5289
+ }
5290
+ const byIdOrLabel = (key) => typeof key === "string" ? options.find((o) => o.id === key || o.label === key) : void 0;
5291
+ const actions = () => {
5292
+ const enabled = options.filter(isEnabled);
5293
+ return [
5294
+ ...options.map((o) => ({
5295
+ name: o.id,
5296
+ label: o.label,
5297
+ finishes: true,
5298
+ enabled: isEnabled(o)
5299
+ })),
5300
+ ...spec.multi ? [
5301
+ {
5302
+ name: SELECT_ACTION,
5303
+ finishes: true,
5304
+ enabled: enabled.length > 0,
5305
+ args: "options"
5306
+ }
5307
+ ] : [],
5308
+ ...spec.freeText ? [{ name: REPLY_ACTION, finishes: true, enabled: true, args: "text" }] : []
5309
+ ];
5310
+ };
5311
+ const answer = (a) => () => spec.choose(a);
5312
+ return {
5313
+ prompt: spec.prompt,
5314
+ facts: () => spec.facts?.() ?? {},
5315
+ actions,
5316
+ isReady: () => actions().some((a) => a.finishes && a.enabled),
5317
+ prepare(name, args) {
5318
+ if (spec.multi && name === SELECT_ACTION) {
5319
+ const picked = args?.options;
5320
+ if (!Array.isArray(picked) || picked.length === 0) {
5321
+ return {
5322
+ error: "invalid-args",
5323
+ message: "`select` takes { options: string[] }, not empty."
5324
+ };
5325
+ }
5326
+ const chosen = picked.map(byIdOrLabel);
5327
+ const unknown = picked.filter((_, i) => !chosen[i]);
5328
+ if (unknown.length) {
5329
+ return { error: "invalid-args", message: `Not options here: ${unknown.join(", ")}.` };
5330
+ }
5331
+ const disabled = chosen.filter((o) => !isEnabled(o));
5332
+ if (disabled.length) {
5333
+ return {
5334
+ error: "disabled",
5335
+ message: `Not available right now: ${disabled.map((o) => o.label).join(", ")}.`
5336
+ };
5337
+ }
5338
+ const picks = chosen;
5339
+ return {
5340
+ choice: picks.map((o) => o.label).join(", "),
5341
+ finishes: true,
5342
+ run: answer({ kind: "options", options: picks.map((o) => o.id) })
5343
+ };
5344
+ }
5345
+ if (spec.freeText && name === REPLY_ACTION) {
5346
+ const text = args?.text;
5347
+ if (typeof text !== "string" || !text.trim()) {
5348
+ return { error: "invalid-args", message: "`reply` takes { text: string }, not empty." };
5349
+ }
5350
+ return { choice: text, finishes: true, run: answer({ kind: "text", text }) };
5351
+ }
5352
+ const option = options.find((o) => o.id === name);
5353
+ if (!option) {
5354
+ return { error: "unknown-action", message: `No action "${name}" here.` };
5355
+ }
5356
+ if (!isEnabled(option)) {
5357
+ return { error: "disabled", message: `"${option.label}" is not available right now.` };
5358
+ }
5359
+ return {
5360
+ choice: option.label,
5361
+ finishes: true,
5362
+ run: answer({ kind: "option", option: option.id })
5363
+ };
5364
+ }
5365
+ };
5366
+ }
5367
+ var CHOICE_CHARS = 300;
5368
+ var matchesType = (value, type) => {
5369
+ switch (type) {
5370
+ case "string":
5371
+ return typeof value === "string";
5372
+ case "number":
5373
+ case "integer":
5374
+ return typeof value === "number" && (type === "number" || Number.isInteger(value));
5375
+ case "boolean":
5376
+ return typeof value === "boolean";
5377
+ case "array":
5378
+ return Array.isArray(value);
5379
+ case "object":
5380
+ return !!value && typeof value === "object" && !Array.isArray(value);
5381
+ default:
5382
+ return true;
5383
+ }
5384
+ };
5385
+ function checkArgs(args, schema) {
5386
+ if (!schema) return void 0;
5387
+ if (!matchesType(args, "object")) return "its arguments must be an object.";
5388
+ const record = args;
5389
+ const required = schema.required ?? [];
5390
+ const missing = required.filter((key) => record[key] === void 0);
5391
+ if (missing.length) return `missing: ${missing.join(", ")}.`;
5392
+ const properties = schema.properties ?? {};
5393
+ 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)}`);
5394
+ return wrong.length ? `${wrong.join("; ")}.` : void 0;
5395
+ }
5396
+ function buildFullSurface(spec) {
5397
+ const guard = (enabledWhen) => {
5398
+ try {
5399
+ return enabledWhen?.() ?? true;
5400
+ } catch {
5401
+ return false;
5402
+ }
5403
+ };
5404
+ const actions = () => Object.entries(spec.actions).map(([name, a]) => ({
5405
+ name,
5406
+ finishes: !!a.finishes,
5407
+ enabled: guard(a.enabledWhen),
5408
+ description: a.description,
5409
+ ...a.argsSchema ? { args: "fields", argsSchema: a.argsSchema } : {}
5410
+ }));
5411
+ return {
5412
+ prompt: spec.prompt,
5413
+ facts: () => ({}),
5414
+ describe: () => spec.describe(),
5415
+ views: spec.views,
5416
+ read: (view) => spec.read(view),
5417
+ actions,
5418
+ isReady: () => actions().some((a) => a.enabled),
5419
+ prepare(name, args) {
5420
+ const action = Object.prototype.hasOwnProperty.call(spec.actions, name) ? spec.actions[name] : void 0;
5421
+ if (!action) return { error: "unknown-action", message: `No action "${name}" here.` };
5422
+ if (!guard(action.enabledWhen)) {
5423
+ return { error: "disabled", message: `"${name}" is not available right now.` };
5424
+ }
5425
+ const problem = checkArgs(args ?? {}, action.argsSchema);
5426
+ if (problem) return { error: "invalid-args", message: `"${name}": ${problem}` };
5427
+ const shown = args && Object.keys(args).length ? ` ${JSON.stringify(args)}` : "";
5428
+ return {
5429
+ choice: `${name}${shown}`.slice(0, CHOICE_CHARS),
5430
+ finishes: !!action.finishes,
5431
+ run: () => action.run(args ?? {})
5432
+ };
5433
+ }
5434
+ };
5435
+ }
5436
+
5201
5437
  // src/state/interaction-context.ts
5202
- function createInteractionContext(interactionId) {
5438
+ function createInteractionContext(interactionId, deps = {}) {
5203
5439
  const state = {};
5204
5440
  const onceCache = /* @__PURE__ */ new Map();
5205
5441
  const listeners = /* @__PURE__ */ new Set();
5206
5442
  const disposers = /* @__PURE__ */ new Set();
5443
+ const readyListeners = /* @__PURE__ */ new Set();
5207
5444
  let disposed = false;
5445
+ let settled = false;
5208
5446
  let heldBy = "user";
5447
+ let controller;
5448
+ let surface;
5449
+ let lastActivityAt = Date.now();
5450
+ let checkpoint = deps.checkpoint;
5451
+ const checkReady = () => {
5452
+ if (disposed || !surface || readyListeners.size === 0 || !surface.isReady()) return;
5453
+ const waiting = [...readyListeners];
5454
+ readyListeners.clear();
5455
+ for (const fn of waiting) {
5456
+ try {
5457
+ fn();
5458
+ } catch (e) {
5459
+ logger2.error("InteractionContext onSurfaceReady callback threw", e);
5460
+ }
5461
+ }
5462
+ };
5209
5463
  const context = {
5210
5464
  interactionId,
5211
5465
  get heldBy() {
5212
5466
  return heldBy;
5213
5467
  },
5468
+ get controller() {
5469
+ return controller;
5470
+ },
5471
+ get surface() {
5472
+ return surface;
5473
+ },
5474
+ get lastActivityAt() {
5475
+ return lastActivityAt;
5476
+ },
5477
+ get checkpoint() {
5478
+ return checkpoint;
5479
+ },
5480
+ saveCheckpoint(next) {
5481
+ if (disposed) return;
5482
+ checkpoint = next;
5483
+ deps.saveCheckpoint?.(next);
5484
+ },
5214
5485
  state,
5215
5486
  once(key, factory) {
5216
5487
  if (onceCache.has(key)) return onceCache.get(key);
@@ -5225,6 +5496,7 @@ function createInteractionContext(interactionId) {
5225
5496
  return value;
5226
5497
  },
5227
5498
  notify() {
5499
+ lastActivityAt = Date.now();
5228
5500
  for (const listener of listeners) {
5229
5501
  try {
5230
5502
  listener();
@@ -5232,6 +5504,7 @@ function createInteractionContext(interactionId) {
5232
5504
  logger2.error("InteractionContext subscriber threw during notify()", e);
5233
5505
  }
5234
5506
  }
5507
+ checkReady();
5235
5508
  },
5236
5509
  subscribe(listener) {
5237
5510
  listeners.add(listener);
@@ -5245,6 +5518,36 @@ function createInteractionContext(interactionId) {
5245
5518
  return;
5246
5519
  }
5247
5520
  disposers.add(fn);
5521
+ },
5522
+ resolve(result) {
5523
+ if (settled || disposed) return;
5524
+ settled = true;
5525
+ deps.resolve?.(result);
5526
+ },
5527
+ exposeDecision(decision) {
5528
+ if (disposed) return;
5529
+ surface = buildDecisionSurface(decision);
5530
+ context.notify();
5531
+ },
5532
+ exposeSurface(spec) {
5533
+ if (disposed) return;
5534
+ surface = buildFullSurface(spec);
5535
+ context.notify();
5536
+ },
5537
+ onSurfaceReady(fn) {
5538
+ if (disposed) return () => void 0;
5539
+ if (surface?.isReady()) {
5540
+ fn();
5541
+ return () => void 0;
5542
+ }
5543
+ readyListeners.add(fn);
5544
+ return () => {
5545
+ readyListeners.delete(fn);
5546
+ };
5547
+ },
5548
+ claimantOwns(action) {
5549
+ if (heldBy === "user" || !surface) return false;
5550
+ return surface.actions().some((a) => a.name === action && a.enabled);
5248
5551
  }
5249
5552
  };
5250
5553
  return {
@@ -5261,6 +5564,7 @@ function createInteractionContext(interactionId) {
5261
5564
  }
5262
5565
  disposers.clear();
5263
5566
  listeners.clear();
5567
+ readyListeners.clear();
5264
5568
  onceCache.clear();
5265
5569
  },
5266
5570
  setHeldBy(holder) {
@@ -5268,12 +5572,70 @@ function createInteractionContext(interactionId) {
5268
5572
  heldBy = holder;
5269
5573
  context.notify();
5270
5574
  return true;
5575
+ },
5576
+ setController(value) {
5577
+ controller = value;
5578
+ },
5579
+ get surface() {
5580
+ return surface;
5271
5581
  }
5272
5582
  };
5273
5583
  }
5274
5584
 
5585
+ // src/state/interaction-registry.ts
5586
+ var registry2 = /* @__PURE__ */ new Map();
5587
+ function getInteractionRegistration(name) {
5588
+ return registry2.get(name);
5589
+ }
5590
+
5591
+ // src/state/run-steps.ts
5592
+ var recorders = /* @__PURE__ */ new WeakMap();
5593
+ function bindStepRecorder(ctx, record, refused) {
5594
+ recorders.set(ctx, { step: record, refused });
5595
+ }
5596
+
5275
5597
  // src/components/chat-driver/interaction-controller.ts
5276
5598
  var DEFAULT_DECISION_FRAMING = "Decided on the user's behalf";
5599
+ var DEFAULT_EDIT_FRAMING = "Changed on the user's behalf before the user answered; the user then finished it, confirming the changes";
5600
+ var BUSY = {
5601
+ error: "busy",
5602
+ 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."
5603
+ };
5604
+ var isThenable = (value) => value !== null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
5605
+ function editOf(by, action, choice, args) {
5606
+ const copy = jsonCopy(args);
5607
+ return { by, action, choice, ...copy !== void 0 && { args: copy } };
5608
+ }
5609
+ function jsonCopy(args) {
5610
+ if (args === void 0 || args === null) return void 0;
5611
+ try {
5612
+ const copy = JSON.parse(JSON.stringify(args));
5613
+ const empty = copy === "" || Array.isArray(copy) && copy.length === 0 || copy !== null && typeof copy === "object" && Object.keys(copy).length === 0;
5614
+ return empty ? void 0 : copy;
5615
+ } catch {
5616
+ return void 0;
5617
+ }
5618
+ }
5619
+ function doneOnUsersBehalf(interaction, answeredBy) {
5620
+ const edits = interaction.edits?.length ? interaction.edits.map((edit) => edit.choice) : void 0;
5621
+ if (answeredBy && answeredBy !== "user") {
5622
+ const recorded = interaction.decision;
5623
+ return {
5624
+ decision: {
5625
+ framing: interaction.claimant?.decisionFraming ?? DEFAULT_DECISION_FRAMING,
5626
+ ...edits && { edits },
5627
+ ...recorded?.choice !== void 0 && { choice: recorded.choice },
5628
+ ...recorded?.shownToUser !== void 0 && { shownToUser: recorded.shownToUser }
5629
+ }
5630
+ };
5631
+ }
5632
+ if (answeredBy === "user" && edits) {
5633
+ return {
5634
+ edited: { framing: interaction.claimant?.editFraming ?? DEFAULT_EDIT_FRAMING, edits }
5635
+ };
5636
+ }
5637
+ return {};
5638
+ }
5277
5639
  var STOPPED = {
5278
5640
  status: "timeout",
5279
5641
  message: "The turn was stopped before this was answered."
@@ -5297,6 +5659,72 @@ var InteractionController = class {
5297
5659
  */
5298
5660
  this.interceptors = [];
5299
5661
  }
5662
+ /** See `ChatDriver.getInteractionCheckpoint`. */
5663
+ getCheckpoint() {
5664
+ const cp = this.checkpoint;
5665
+ return cp ? { componentName: cp.componentName, value: cp.value } : void 0;
5666
+ }
5667
+ /** See `ChatDriver.restoreInteractionCheckpoint`. Restored means cut: its interaction is gone. */
5668
+ restoreCheckpoint(saved) {
5669
+ this.checkpoint = saved ? { componentName: saved.componentName, value: saved.value, cut: true } : void 0;
5670
+ }
5671
+ /** Drop the checkpoint, used or not, when its flow ends. */
5672
+ clearCheckpoint(why) {
5673
+ const cp = this.checkpoint;
5674
+ if (!cp) return;
5675
+ this.checkpoint = void 0;
5676
+ recordMetaEvent(this.deps.sessionKey, "interaction.checkpoint", {
5677
+ action: "cleared",
5678
+ component: cp.componentName,
5679
+ why,
5680
+ ...cp.interactionId ? { interactionId: cp.interactionId } : {}
5681
+ });
5682
+ }
5683
+ /**
5684
+ * Hand a cut checkpoint to a new request of the same widget type, if its registration's
5685
+ * `sameWork` agrees. Only the first such request is asked: a refused checkpoint is dropped, and
5686
+ * the work starts fresh as it would have without one.
5687
+ */
5688
+ takeOverCheckpoint(componentName, data, interactionId) {
5689
+ const cut = this.checkpoint;
5690
+ if (!cut?.cut || cut.componentName !== componentName) return void 0;
5691
+ this.checkpoint = void 0;
5692
+ const sameWork = getInteractionRegistration(componentName)?.sameWork;
5693
+ let agrees = true;
5694
+ if (sameWork) {
5695
+ try {
5696
+ agrees = sameWork(cut.value, data);
5697
+ } catch (e) {
5698
+ logger2.warn(`InteractionController: "${componentName}" sameWork threw; starting fresh:`, e);
5699
+ agrees = false;
5700
+ }
5701
+ }
5702
+ recordMetaEvent(this.deps.sessionKey, "interaction.checkpoint", {
5703
+ action: agrees ? "resumed" : "refused",
5704
+ component: componentName,
5705
+ interactionId,
5706
+ ...agrees ? {} : { saved: cut.value }
5707
+ });
5708
+ if (!agrees) return void 0;
5709
+ this.checkpoint = { componentName, value: cut.value, interactionId };
5710
+ return cut.value;
5711
+ }
5712
+ /** A controller's `ctx.saveCheckpoint`: kept as JSON, the form it is persisted in. */
5713
+ saveCheckpoint(interactionId, componentName, value) {
5714
+ if (!this.pending.has(interactionId)) return;
5715
+ let copy;
5716
+ try {
5717
+ copy = JSON.parse(JSON.stringify(value) ?? "null");
5718
+ } catch (e) {
5719
+ logger2.warn(
5720
+ `InteractionController: a "${componentName}" checkpoint is not storable as JSON:`,
5721
+ e
5722
+ );
5723
+ return;
5724
+ }
5725
+ this.checkpoint = { componentName, value: copy, interactionId };
5726
+ this.deps.emit(new CustomEvent("checkpoint-saved", { detail: { interactionId } }));
5727
+ }
5300
5728
  /** See `ChatDriver.addInteractionInterceptor`. Returns a remover. */
5301
5729
  addInterceptor(interceptor) {
5302
5730
  if (!interceptor.id || interceptor.id === "user") {
@@ -5356,6 +5784,10 @@ var InteractionController = class {
5356
5784
  if (!isClaimant()) return false;
5357
5785
  const next = holder === "user" ? "user" : interceptor.id;
5358
5786
  if (!handle.setHeldBy(next)) return false;
5787
+ if (next === "user") {
5788
+ const pending = this.pending.get(handle.context.interactionId);
5789
+ if (pending && !pending.running?.finishes) pending.settledBy = void 0;
5790
+ }
5359
5791
  recordMetaEvent(this.deps.sessionKey, "interaction.held", {
5360
5792
  interactionId: handle.context.interactionId,
5361
5793
  heldBy: next,
@@ -5377,9 +5809,87 @@ var InteractionController = class {
5377
5809
  if (!pending) return false;
5378
5810
  pending.decision = { choice: decision.choice, shownToUser: decision.shownToUser };
5379
5811
  return true;
5812
+ },
5813
+ invoke: async (action, args, options) => {
5814
+ const { interactionId } = handle.context;
5815
+ const pending = this.pending.get(interactionId);
5816
+ if (!isClaimant() || !pending || handle.context.heldBy !== interceptor.id) {
5817
+ return {
5818
+ ok: false,
5819
+ error: "not-held",
5820
+ message: pending ? "This interaction is with the person now, so it cannot be acted on." : "This interaction has already ended."
5821
+ };
5822
+ }
5823
+ const { surface } = handle;
5824
+ if (!surface) {
5825
+ return { ok: false, error: "no-surface", message: "Nothing has been exposed here yet." };
5826
+ }
5827
+ if (pending.running) {
5828
+ return { ok: false, ...BUSY, actions: surface.actions() };
5829
+ }
5830
+ const prepared = surface.prepare(action, args);
5831
+ if ("error" in prepared) {
5832
+ return { ok: false, ...prepared, actions: surface.actions() };
5833
+ }
5834
+ const previous = { decision: pending.decision, settledBy: pending.settledBy };
5835
+ const restore = () => {
5836
+ if (!prepared.finishes) return;
5837
+ pending.decision = previous.decision;
5838
+ pending.settledBy = handle.context.heldBy === interceptor.id ? previous.settledBy : void 0;
5839
+ };
5840
+ if (prepared.finishes) {
5841
+ pending.decision = { choice: prepared.choice, shownToUser: options?.note };
5842
+ pending.settledBy = interceptor.id;
5843
+ }
5844
+ const content = options?.note?.trim();
5845
+ const note = content ? { content, delegate: interceptor.id } : void 0;
5846
+ const token = {};
5847
+ pending.running = { token, finishes: prepared.finishes, note };
5848
+ let output;
5849
+ try {
5850
+ const ran = prepared.run();
5851
+ output = isThenable(ran) ? await ran : ran;
5852
+ } catch (e) {
5853
+ logger2.warn(`InteractionController: action "${action}" threw:`, e);
5854
+ if (pending.settledInRun !== token) restore();
5855
+ return {
5856
+ ok: false,
5857
+ error: "threw",
5858
+ message: e instanceof Error ? e.message : String(e)
5859
+ };
5860
+ } finally {
5861
+ if (pending.running?.token === token) pending.running = void 0;
5862
+ }
5863
+ const finished = pending.settledInRun === token;
5864
+ if (!finished) {
5865
+ restore();
5866
+ if (note && this.pending.has(interactionId)) this.appendNote(note);
5867
+ (pending.edits ??= []).push(editOf(interceptor.id, action, prepared.choice, args));
5868
+ }
5869
+ recordMetaEvent(this.deps.sessionKey, "interaction.invoked", {
5870
+ interactionId,
5871
+ claimant: interceptor.id,
5872
+ action,
5873
+ finished
5874
+ });
5875
+ return {
5876
+ ok: true,
5877
+ finished,
5878
+ actions: finished ? [] : surface.actions(),
5879
+ ...output !== void 0 && !finished ? { output } : {}
5880
+ };
5380
5881
  }
5381
5882
  };
5382
5883
  }
5884
+ /** A claimant's note about its choice: a delegated, display-only message in the assistant's voice. */
5885
+ appendNote(note) {
5886
+ this.deps.appendToHistory({
5887
+ role: "assistant",
5888
+ content: note.content,
5889
+ delegate: note.delegate,
5890
+ excludeFromProvider: true
5891
+ });
5892
+ }
5383
5893
  /** See `ChatDriver.setHostInteractionRequester`. */
5384
5894
  setHostRequester(fn, recordOutcome) {
5385
5895
  this.hostRequester = fn;
@@ -5424,8 +5934,36 @@ var InteractionController = class {
5424
5934
  });
5425
5935
  }, timeoutMs) : void 0
5426
5936
  });
5427
- const handle = createInteractionContext(interactionId);
5937
+ const handle = createInteractionContext(interactionId, {
5938
+ resolve: (result) => this.resolve(interactionId, result),
5939
+ checkpoint: this.takeOverCheckpoint(componentName, data, interactionId),
5940
+ saveCheckpoint: (value) => this.saveCheckpoint(interactionId, componentName, value)
5941
+ });
5428
5942
  this.contexts.set(interactionId, handle);
5943
+ bindStepRecorder(
5944
+ handle.context,
5945
+ (step, outcome) => recordMetaEvent(this.deps.sessionKey, "interaction.step", {
5946
+ interactionId,
5947
+ component: componentName,
5948
+ step,
5949
+ outcome
5950
+ }),
5951
+ (reason, saved) => recordMetaEvent(this.deps.sessionKey, "interaction.checkpoint", {
5952
+ action: "refused",
5953
+ component: componentName,
5954
+ interactionId,
5955
+ reason,
5956
+ saved
5957
+ })
5958
+ );
5959
+ const registration = getInteractionRegistration(componentName);
5960
+ if (registration?.controller) {
5961
+ try {
5962
+ handle.setController(registration.controller(data, handle.context));
5963
+ } catch (e) {
5964
+ logger2.error(`InteractionController: the "${componentName}" controller threw:`, e);
5965
+ }
5966
+ }
5429
5967
  const heldBy = this.offer(handle, {
5430
5968
  interactionId,
5431
5969
  componentName,
@@ -5502,21 +6040,20 @@ var InteractionController = class {
5502
6040
  const costUsd = typeof result?.costUsd === "number" && Number.isFinite(result.costUsd) && result.costUsd > 0 ? result.costUsd : void 0;
5503
6041
  const unanswered = result?.status === "timeout";
5504
6042
  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;
6043
+ const run = answeredBy && answeredBy !== "user" ? interaction.running : void 0;
6044
+ if (run) {
6045
+ interaction.settledInRun = run.token;
6046
+ if (run.note) this.appendNote(run.note);
6047
+ }
6048
+ const edits = interaction.edits?.length ? interaction.edits : void 0;
6049
+ const { decision, edited } = doneOnUsersBehalf(interaction, answeredBy);
5514
6050
  const patched = this.deps.patchInteractionMessage(interactionId, (message) => ({
5515
6051
  ...message,
5516
6052
  interaction: {
5517
6053
  ...message.interaction,
5518
6054
  resolved: result,
5519
- ...answeredBy ? { answeredBy } : {}
6055
+ ...answeredBy ? { answeredBy } : {},
6056
+ ...edits ? { edits } : {}
5520
6057
  },
5521
6058
  ...costUsd != null ? { externalCostUsd: (message.externalCostUsd ?? 0) + costUsd } : {}
5522
6059
  }));
@@ -5536,16 +6073,23 @@ var InteractionController = class {
5536
6073
  }
5537
6074
  recordMetaEvent(this.deps.sessionKey, "interaction.resolved", {
5538
6075
  interactionId,
5539
- ...answeredBy ? { answeredBy } : {}
6076
+ ...answeredBy ? { answeredBy } : {},
6077
+ ...edits ? { edits: edits.length } : {}
5540
6078
  });
5541
6079
  this.deps.activityBus.publish("interaction-resolved", void 0);
5542
6080
  try {
5543
- interaction.onSettled?.({ answeredBy, decision });
6081
+ interaction.onSettled?.({ answeredBy, decision, ...edited ? { edited } : {} });
5544
6082
  } catch (e) {
5545
6083
  logger2.warn("InteractionController: onSettled threw:", e);
5546
6084
  }
6085
+ this.deps.emit(
6086
+ new CustomEvent("interaction-settled", {
6087
+ detail: { interactionId, ...answeredBy ? { answeredBy } : {} }
6088
+ })
6089
+ );
5547
6090
  interaction.resolve(result);
5548
6091
  this.pending.delete(interactionId);
6092
+ if (this.checkpoint?.interactionId === interactionId) this.checkpoint = void 0;
5549
6093
  this.contexts.get(interactionId)?.dispose();
5550
6094
  this.contexts.delete(interactionId);
5551
6095
  }
@@ -5942,17 +6486,30 @@ var budgetDetailOf = (e) => {
5942
6486
  ...e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}
5943
6487
  };
5944
6488
  };
5945
- function withDelegatedDecisions(result, content, decisions, hasImages) {
5946
- if (decisions.length === 0) return content;
6489
+ function withDelegatedDecisions(result, content, done, hasImages) {
6490
+ const { decided, edited } = done;
6491
+ if (decided.length === 0 && edited.length === 0) return content;
5947
6492
  if (!hasImages && result !== null && typeof result === "object" && !Array.isArray(result)) {
5948
- return JSON.stringify({ ...result, decidedOnUsersBehalf: decisions });
6493
+ return JSON.stringify({
6494
+ ...result,
6495
+ ...decided.length ? { decidedOnUsersBehalf: decided } : {},
6496
+ ...edited.length ? { editedOnUsersBehalf: edited } : {}
6497
+ });
5949
6498
  }
5950
- return [content, ...decisions.map(describeDelegatedDecision)].join("\n\n");
6499
+ return [
6500
+ content,
6501
+ ...decided.map(describeDelegatedDecision),
6502
+ ...edited.map(describeDelegatedEdits)
6503
+ ].join("\n\n");
6504
+ }
6505
+ function describeDelegatedEdits(edited) {
6506
+ return `${edited.framing}: ${edited.edits.map((edit) => `"${edit}"`).join(", ")}.`;
5951
6507
  }
5952
6508
  function describeDelegatedDecision(decision) {
5953
6509
  const choice = decision.choice !== void 0 ? `: "${decision.choice}"` : "";
5954
6510
  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}`;
6511
+ const edited = decision.edits?.length ? ` Before deciding it changed the widget: ${decision.edits.map((edit) => `"${edit}"`).join(", ")}.` : "";
6512
+ return `${decision.framing}${choice}.${shown}${edited}`;
5956
6513
  }
5957
6514
  function inFlightToolCallsAt(history) {
5958
6515
  for (let i = history.length - 1; i >= 0; i -= 1) {
@@ -6699,6 +7256,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
6699
7256
  * release, then the active agent.
6700
7257
  */
6701
7258
  dispatchAgentTransition(t) {
7259
+ if (t.flowOwnerChangedTo === null) this.interactions.clearCheckpoint("flow-ended");
6702
7260
  if (t.flowOwnerChangedTo !== void 0) {
6703
7261
  this.dispatchEvent(new CustomEvent("flow-owner-changed", { detail: t.flowOwnerChangedTo }));
6704
7262
  }
@@ -7283,7 +7841,11 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7283
7841
  // below, taking its per-message cost/token fields with it — so a host total
7284
7842
  // re-derived from the transcript would otherwise fall by exactly this much
7285
7843
  // on every compaction. Summarising a turn does not refund what it cost.
7286
- rolledUpUsage: sumUsage(toCompact)
7844
+ rolledUpUsage: sumUsage(toCompact),
7845
+ // The user's own words survive word for word, beside the paraphrase (GENC-1660, C2): this
7846
+ // range's own only. An earlier summary's kept messages are not carried forward; the
7847
+ // summariser read them with it and folded them into this paraphrase.
7848
+ preservedUserMessages: freshUserMessages(toCompact)
7287
7849
  }
7288
7850
  };
7289
7851
  this.loadHistory([summaryMessage, ...tail]);
@@ -7455,6 +8017,14 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7455
8017
  getInteractionContext(interactionId) {
7456
8018
  return this.interactions.context(interactionId);
7457
8019
  }
8020
+ /** {@inheritDoc AiDriver.getInteractionCheckpoint} */
8021
+ getInteractionCheckpoint() {
8022
+ return this.interactions.getCheckpoint();
8023
+ }
8024
+ /** {@inheritDoc AiDriver.restoreInteractionCheckpoint} */
8025
+ restoreInteractionCheckpoint(saved) {
8026
+ this.interactions.restoreCheckpoint(saved);
8027
+ }
7458
8028
  /**
7459
8029
  * Replace the history message owning `interactionId`. `false` when it is no longer in history.
7460
8030
  * Does not announce the change — the interaction paths differ on whether they should.
@@ -7497,7 +8067,8 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7497
8067
  role: "user",
7498
8068
  content: userInput,
7499
8069
  attachments,
7500
- ...delegate ? { delegate, delegateFraming } : {}
8070
+ // Who sent it, and what writing it cost when that was not the working agent's own spend.
8071
+ ...delegatedFields(options)
7501
8072
  });
7502
8073
  this.turnStartedAt = Date.now();
7503
8074
  recordMetaEvent(this.sessionKey, "turn.start", {
@@ -7563,7 +8134,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7563
8134
  * kept on a seven-way fan-out, reporting ~2.3x under the real cost, with the
7564
8135
  * error growing as the fan-out widens.
7565
8136
  */
7566
- buildHandlerContext(activeToolCallId, traceCapture, decisions) {
8137
+ buildHandlerContext(activeToolCallId, traceCapture, done) {
7567
8138
  let opened;
7568
8139
  let recorded = false;
7569
8140
  const capabilities2 = this.buildCapabilities();
@@ -7577,9 +8148,11 @@ var ChatDriver = class _ChatDriver extends EventTarget {
7577
8148
  opened = interactionId;
7578
8149
  },
7579
8150
  void 0,
7580
- // An interaction an interceptor answered is reported in THIS call's result (GENC-1660).
8151
+ // What an interceptor did to an interaction is reported in THIS call's result
8152
+ // (GENC-1660): its answer, or the edits it made before the person answered.
7581
8153
  (settled) => {
7582
- if (settled.decision) decisions?.push(settled.decision);
8154
+ if (settled.decision) done?.decided.push(settled.decision);
8155
+ if (settled.edited) done?.edited.push(settled.edited);
7583
8156
  }
7584
8157
  ),
7585
8158
  recordInteractionOutcome: (outcome) => {
@@ -8416,17 +8989,17 @@ ${tailBody}
8416
8989
  }
8417
8990
  const traceCapture = { traces: [] };
8418
8991
  const capturedTrace = () => traceCapture.traces.length ? traceCapture.traces.flat() : void 0;
8419
- const decisions = [];
8992
+ const done = { decided: [], edited: [] };
8420
8993
  try {
8421
8994
  const result = await handler(
8422
8995
  tc.args,
8423
- this.buildHandlerContext(tc.id, traceCapture, decisions)
8996
+ this.buildHandlerContext(tc.id, traceCapture, done)
8424
8997
  );
8425
8998
  const resultImages = toolResultImages(result);
8426
8999
  const content = withDelegatedDecisions(
8427
9000
  result,
8428
9001
  resultImages ? typeof result.content === "string" ? result.content : JSON.stringify({ ...result, attachments: void 0 }) : typeof result === "string" ? result : JSON.stringify(result),
8429
- decisions,
9002
+ done,
8430
9003
  !!resultImages
8431
9004
  );
8432
9005
  executedById.set(tc.id, {
@@ -8636,8 +9209,10 @@ var AI_DRIVER_EVENT_TYPES = Object.keys({
8636
9209
  "sub-agent-stop": true,
8637
9210
  "interaction-start": true,
8638
9211
  "interaction-stop": true,
9212
+ "interaction-settled": true,
8639
9213
  "provider-changed": true,
8640
9214
  "external-cost-recorded": true,
9215
+ "checkpoint-saved": true,
8641
9216
  "agent-changed": true,
8642
9217
  "agent-released": true,
8643
9218
  "flow-owner-changed": true,
@@ -8733,6 +9308,12 @@ var BaseDriverDecorator = class _BaseDriverDecorator extends EventTarget {
8733
9308
  getFlowOwner() {
8734
9309
  return this.inner.getFlowOwner?.() ?? null;
8735
9310
  }
9311
+ getInteractionCheckpoint() {
9312
+ return this.inner.getInteractionCheckpoint?.();
9313
+ }
9314
+ restoreInteractionCheckpoint(saved) {
9315
+ this.inner.restoreInteractionCheckpoint?.(saved);
9316
+ }
8736
9317
  /**
8737
9318
  * Forwarded, so an interceptor installed on any layer lands on the driver that opens the
8738
9319
  * interactions. A decorator that answers interactions installs its own on `this.inner`.
@@ -8967,10 +9548,7 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
8967
9548
  return this.chatDriver.getSuggestions(history, prompt2, count, agentInfo);
8968
9549
  }
8969
9550
  async sendMessage(input, attachments, options) {
8970
- const delegated = options?.delegate ? {
8971
- delegate: options.delegate,
8972
- delegateFraming: options.delegateFraming ?? DEFAULT_DELEGATE_FRAMING
8973
- } : {};
9551
+ const delegated = delegatedFields(options);
8974
9552
  try {
8975
9553
  return await this.runOrchestratedTurn(input, attachments, options, delegated);
8976
9554
  } catch (e) {
@@ -9103,7 +9681,25 @@ var OrchestratingDriver = class _OrchestratingDriver extends BaseDriverDecorator
9103
9681
  this.cancelled = false;
9104
9682
  await this.installAgent(agent);
9105
9683
  if (!resumeNote || agent.resumeOnRestore === false || this.cancelled) return void 0;
9106
- return this.chatDriver.continueFromHistory(void 0, { tailNote: resumeNote });
9684
+ const result = await this.chatDriver.continueFromHistory(void 0, { tailNote: resumeNote });
9685
+ await this.releaseIfRequested();
9686
+ return result;
9687
+ }
9688
+ /**
9689
+ * A continuation outside a routed turn — a decorator carrying the agent on, say — can finish the
9690
+ * flow as surely as a turn can, so a release it requests is acted on here too. Before
9691
+ * (GENC-1660), only `sendMessage` checked, and a flow finished by a continuation stayed locked.
9692
+ */
9693
+ async continueFromHistory(transientPrimer, options) {
9694
+ const result = await this.chatDriver.continueFromHistory(transientPrimer, options);
9695
+ await this.releaseIfRequested();
9696
+ return result;
9697
+ }
9698
+ /** Release the flow if the turn that just ended asked to, and an agent is still active to release. */
9699
+ async releaseIfRequested() {
9700
+ if (this.chatDriver.getAgentReleaseRequested() && this.agentSession.getActive()) {
9701
+ await this.releaseActiveAgent();
9702
+ }
9107
9703
  }
9108
9704
  /**
9109
9705
  * {@inheritDoc AiDriver.getContextReserve}
@@ -9566,6 +10162,13 @@ function defineStatefulAgent(opts) {
9566
10162
  notResumableMessage: opts.notResumableMessage,
9567
10163
  resumeOnRestore: opts.resumeOnRestore,
9568
10164
  contextReserve: opts.contextReserve,
10165
+ // Bound to the live state, so the autopilot's resolvers see what this agent's do (GENC-1660).
10166
+ ...opts.autopilot && {
10167
+ autopilot: {
10168
+ config: opts.autopilot,
10169
+ state: () => state
10170
+ }
10171
+ },
9569
10172
  onActivate: async (ctx) => {
9570
10173
  state = await opts.init(ctx);
9571
10174
  if (opts.toolHandlers) {
@@ -9606,6 +10209,9 @@ function defineStatefulAgent(opts) {
9606
10209
  excludeFromClassifier: opts.excludeFromClassifier
9607
10210
  };
9608
10211
  }
10212
+ function defineAutopilot(config) {
10213
+ return config;
10214
+ }
9609
10215
 
9610
10216
  // src/config/fallback-agents.ts
9611
10217
  var friendlyFallbackAgent = {
@@ -9822,6 +10428,7 @@ export {
9822
10428
  buildTimelineEntries,
9823
10429
  clearSession,
9824
10430
  defineAgent,
10431
+ defineAutopilot,
9825
10432
  defineStatefulAgent,
9826
10433
  emptyUsage,
9827
10434
  friendlyFallbackAgent,