@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
@@ -14,6 +14,11 @@ const genesisIconTemplate = html `
14
14
  `;
15
15
  export const AiChatBubbleTemplate = () => {
16
16
  return html `
17
+ <!-- Mounted once, before it has anything to say, and only its text changes: a status region
18
+ inserted already holding its text is widely missed by screen readers. It announces the badge. -->
19
+ <span class="visually-hidden" role="status">
20
+ ${(x) => (x.needsUser && !x.dialogOpen ? x.needsInputLabel : '')}
21
+ </span>
17
22
  ${when((x) => !x.dialogOpen, html `
18
23
  <div
19
24
  class="bubble ${(x) => (x.hovered ? 'hovered' : '')}"
@@ -29,6 +34,14 @@ export const AiChatBubbleTemplate = () => {
29
34
  `)}
30
35
  ${when((x) => !x.imageSrc, genesisIconTemplate)}
31
36
  <div class="pulse-ring" part="pulse-ring"></div>
37
+ ${when((x) => x.needsUser, html `
38
+ <span
39
+ class="needs-you-badge"
40
+ part="needs-you-badge"
41
+ aria-hidden="true"
42
+ title="${(x) => x.needsInputLabel}"
43
+ ></span>
44
+ `)}
32
45
  <div class="tooltip" part="tooltip" style="${(x) => x.tooltipStyle}" ${ref('tooltipEl')}>
33
46
  ${(x) => x.title}
34
47
  </div>
@@ -6,9 +6,10 @@ import { clearSession, getMetaEvents, mergeMetaEvents, recordMetaEvent, recordTu
6
6
  import { generateSuggestions } from '../../suggestions/generate-suggestions';
7
7
  import { applyCondensation, condenseForPersistence, isValidCondenseTrigger, parsePersistedCondensePolicy, rebaseRestoredCondensePolicy, snapshotCondensePolicies, toolCallIdsIn, } from '../../utils/condense-history';
8
8
  import { estimatePreambleTokens, estimateRequestTokens, } from '../../utils/context-tokens';
9
- import { DEFAULT_DELEGATE_FRAMING, frameDelegatedContent, } from '../../utils/delegated-message';
9
+ import { DEFAULT_DELEGATE_FRAMING, delegatedFields, frameDelegatedContent, } from '../../utils/delegated-message';
10
10
  import { applyHistoryCap, buildCompactionSummaryPrompt, planCompaction, normalizeForProvider, } from '../../utils/history-transform';
11
11
  import { logger } from '../../utils/logger';
12
+ import { freshUserMessages } from '../../utils/preserved-user-messages';
12
13
  import { resolveTurnInput } from '../../utils/resolve-turn-input';
13
14
  import { messageUsage, sumUsage } from '../../utils/sum-usage';
14
15
  import { AgentSession } from '../agent-session/agent-session';
@@ -103,26 +104,42 @@ const budgetDetailOf = (e) => {
103
104
  return Object.assign(Object.assign({ budgetUsd: e.budgetUsd, spentUsd: e.spentUsd, vendorLabel: e.vendorLabel }, (vendor ? { vendor } : {})), (e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}));
104
105
  };
105
106
  /**
106
- * Add the decisions an interceptor made on the user's behalf to a tool result (GENC-1660), so the
107
- * working agent is told the truth about who chose: an object result gains `decidedOnUsersBehalf`,
108
- * anything else a sentence per decision. Unchanged when there are none, which is every call that
109
- * the person answered.
107
+ * Add what an interceptor did on the user's behalf to a tool result (GENC-1660), so the working
108
+ * agent is told the truth about who chose: an object result gains `decidedOnUsersBehalf` and
109
+ * `editedOnUsersBehalf`, anything else a sentence per record. Each decision is `{ framing, choice?,
110
+ * shownToUser?, edits? }`; `edits` lists what the claimant changed in the widget before deciding,
111
+ * and is omitted when it changed nothing. Each edited record is `{ framing, edits }`: the person
112
+ * answered, but only after the claimant had changed the widget. Unchanged when there are none,
113
+ * which is every call the person answered on a widget nobody else touched.
110
114
  */
111
- function withDelegatedDecisions(result, content, decisions, hasImages) {
112
- if (decisions.length === 0)
115
+ function withDelegatedDecisions(result, content, done, hasImages) {
116
+ const { decided, edited } = done;
117
+ if (decided.length === 0 && edited.length === 0)
113
118
  return content;
114
119
  if (!hasImages && result !== null && typeof result === 'object' && !Array.isArray(result)) {
115
- return JSON.stringify(Object.assign(Object.assign({}, result), { decidedOnUsersBehalf: decisions }));
120
+ return JSON.stringify(Object.assign(Object.assign(Object.assign({}, result), (decided.length ? { decidedOnUsersBehalf: decided } : {})), (edited.length ? { editedOnUsersBehalf: edited } : {})));
116
121
  }
117
- return [content, ...decisions.map(describeDelegatedDecision)].join('\n\n');
122
+ return [
123
+ content,
124
+ ...decided.map(describeDelegatedDecision),
125
+ ...edited.map(describeDelegatedEdits),
126
+ ].join('\n\n');
127
+ }
128
+ /** Edits the person then confirmed, as a sentence for a text tool result. */
129
+ function describeDelegatedEdits(edited) {
130
+ return `${edited.framing}: ${edited.edits.map((edit) => `"${edit}"`).join(', ')}.`;
118
131
  }
119
132
  /** One decision as a sentence for a text tool result, e.g. `Decided on the user's behalf: "Skip".` */
120
133
  function describeDelegatedDecision(decision) {
134
+ var _a;
121
135
  const choice = decision.choice !== undefined ? `: "${decision.choice}"` : '';
122
136
  const shown = decision.shownToUser
123
137
  ? ` What the user was shown, as your words: "${decision.shownToUser}"`
124
138
  : ' Nothing was shown to the user about it.';
125
- return `${decision.framing}${choice}.${shown}`;
139
+ const edited = ((_a = decision.edits) === null || _a === void 0 ? void 0 : _a.length)
140
+ ? ` Before deciding it changed the widget: ${decision.edits.map((edit) => `"${edit}"`).join(', ')}.`
141
+ : '';
142
+ return `${decision.framing}${choice}.${shown}${edited}`;
126
143
  }
127
144
  /**
128
145
  * Where the tool calls still waiting for their results start: the last assistant message with a
@@ -975,6 +992,9 @@ export class ChatDriver extends EventTarget {
975
992
  * release, then the active agent.
976
993
  */
977
994
  dispatchAgentTransition(t) {
995
+ // The flow ended, so a checkpoint nothing took over has nothing left to resume (§4.12).
996
+ if (t.flowOwnerChangedTo === null)
997
+ this.interactions.clearCheckpoint('flow-ended');
978
998
  if (t.flowOwnerChangedTo !== undefined) {
979
999
  this.dispatchEvent(new CustomEvent('flow-owner-changed', { detail: t.flowOwnerChangedTo }));
980
1000
  }
@@ -1635,6 +1655,10 @@ export class ChatDriver extends EventTarget {
1635
1655
  // re-derived from the transcript would otherwise fall by exactly this much
1636
1656
  // on every compaction. Summarising a turn does not refund what it cost.
1637
1657
  rolledUpUsage: sumUsage(toCompact),
1658
+ // The user's own words survive word for word, beside the paraphrase (GENC-1660, C2): this
1659
+ // range's own only. An earlier summary's kept messages are not carried forward; the
1660
+ // summariser read them with it and folded them into this paraphrase.
1661
+ preservedUserMessages: freshUserMessages(toCompact),
1638
1662
  },
1639
1663
  };
1640
1664
  this.loadHistory([summaryMessage, ...tail]);
@@ -1806,6 +1830,14 @@ export class ChatDriver extends EventTarget {
1806
1830
  getInteractionContext(interactionId) {
1807
1831
  return this.interactions.context(interactionId);
1808
1832
  }
1833
+ /** {@inheritDoc AiDriver.getInteractionCheckpoint} */
1834
+ getInteractionCheckpoint() {
1835
+ return this.interactions.getCheckpoint();
1836
+ }
1837
+ /** {@inheritDoc AiDriver.restoreInteractionCheckpoint} */
1838
+ restoreInteractionCheckpoint(saved) {
1839
+ this.interactions.restoreCheckpoint(saved);
1840
+ }
1809
1841
  /**
1810
1842
  * Replace the history message owning `interactionId`. `false` when it is no longer in history.
1811
1843
  * Does not announce the change — the interaction paths differ on whether they should.
@@ -1856,7 +1888,7 @@ export class ChatDriver extends EventTarget {
1856
1888
  this.aiServiceWall = undefined;
1857
1889
  this.aiServiceNoticesShown.clear();
1858
1890
  this.aiServiceNoticesQueued = [];
1859
- this.appendToHistory(Object.assign({ role: 'user', content: userInput, attachments }, (delegate ? { delegate, delegateFraming } : {})));
1891
+ this.appendToHistory(Object.assign({ role: 'user', content: userInput, attachments }, delegatedFields(options)));
1860
1892
  this.turnStartedAt = Date.now();
1861
1893
  recordMetaEvent(this.sessionKey, 'turn.start', {
1862
1894
  phase: 'sendMessage',
@@ -1923,7 +1955,7 @@ export class ChatDriver extends EventTarget {
1923
1955
  * kept on a seven-way fan-out, reporting ~2.3x under the real cost, with the
1924
1956
  * error growing as the fan-out widens.
1925
1957
  */
1926
- buildHandlerContext(activeToolCallId, traceCapture, decisions) {
1958
+ buildHandlerContext(activeToolCallId, traceCapture, done) {
1927
1959
  var _a;
1928
1960
  // The interaction THIS call opened, if any: the only one its `recordInteractionOutcome` can
1929
1961
  // reach, so a tool can never write an outcome onto a widget another tool opened.
@@ -1933,10 +1965,13 @@ export class ChatDriver extends EventTarget {
1933
1965
  return Object.assign(Object.assign(Object.assign(Object.assign({}, (capabilities && { capabilities })), { requestInteraction: (componentName, data, options) => this.requestInteraction(componentName, data, options, (interactionId) => {
1934
1966
  opened = interactionId;
1935
1967
  }, undefined,
1936
- // An interaction an interceptor answered is reported in THIS call's result (GENC-1660).
1968
+ // What an interceptor did to an interaction is reported in THIS call's result
1969
+ // (GENC-1660): its answer, or the edits it made before the person answered.
1937
1970
  (settled) => {
1938
1971
  if (settled.decision)
1939
- decisions === null || decisions === void 0 ? void 0 : decisions.push(settled.decision);
1972
+ done === null || done === void 0 ? void 0 : done.decided.push(settled.decision);
1973
+ if (settled.edited)
1974
+ done === null || done === void 0 ? void 0 : done.edited.push(settled.edited);
1940
1975
  }), recordInteractionOutcome: (outcome) => {
1941
1976
  var _a;
1942
1977
  if (!opened)
@@ -3123,10 +3158,10 @@ export class ChatDriver extends EventTarget {
3123
3158
  // `sumUsage` and `usageRows` would still agree with each other.
3124
3159
  const traceCapture = { traces: [] };
3125
3160
  const capturedTrace = () => traceCapture.traces.length ? traceCapture.traces.flat() : undefined;
3126
- // Interactions this call awaited that an interceptor answered (GENC-1660).
3127
- const decisions = [];
3161
+ // What an interceptor did to the interactions this call awaited (GENC-1660).
3162
+ const done = { decided: [], edited: [] };
3128
3163
  try {
3129
- const result = yield handler(tc.args, this.buildHandlerContext(tc.id, traceCapture, decisions));
3164
+ const result = yield handler(tc.args, this.buildHandlerContext(tc.id, traceCapture, done));
3130
3165
  // A handler that returns images gets them carried as real attachments instead of
3131
3166
  // JSON-stringified into the text — the difference between a model reading
3132
3167
  // `{"kind":"image","data":"iVBOR…"}` as prose and actually SEEING the render.
@@ -3140,7 +3175,7 @@ export class ChatDriver extends EventTarget {
3140
3175
  : JSON.stringify(Object.assign(Object.assign({}, result), { attachments: undefined }))
3141
3176
  : typeof result === 'string'
3142
3177
  ? result
3143
- : JSON.stringify(result), decisions, !!resultImages);
3178
+ : JSON.stringify(result), done, !!resultImages);
3144
3179
  executedById.set(tc.id, Object.assign(Object.assign({ toolCallId: tc.id, content }, (resultImages ? { attachments: resultImages } : {})), {
3145
3180
  // Concatenated when a handler invoked several children, so none is lost.
3146
3181
  // Stays `undefined` when nothing was captured — readers key off presence,
@@ -1,8 +1,69 @@
1
+ import { __awaiter } from "tslib";
1
2
  import { recordMetaEvent } from '../../state/debug-event-log';
2
3
  import { createInteractionContext, } from '../../state/interaction-context';
4
+ import { getInteractionRegistration } from '../../state/interaction-registry';
5
+ import { bindStepRecorder } from '../../state/run-steps';
3
6
  import { logger } from '../../utils/logger';
4
7
  /** The framing of a {@link DelegatedDecision} whose claimant declares none. */
5
8
  const DEFAULT_DECISION_FRAMING = "Decided on the user's behalf";
9
+ /** The framing of {@link DelegatedEdits} whose claimant declares none. */
10
+ const DEFAULT_EDIT_FRAMING = "Changed on the user's behalf before the user answered; the user then finished it, confirming the changes";
11
+ /** Why a `hold.invoke` was refused while another of the claimant's actions was still running. */
12
+ const BUSY = {
13
+ error: 'busy',
14
+ 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.',
15
+ };
16
+ /** Whether `value` is a promise, or anything else awaiting it would wait on. */
17
+ const isThenable = (value) => value !== null &&
18
+ (typeof value === 'object' || typeof value === 'function') &&
19
+ typeof value.then === 'function';
20
+ /** One editing action a claimant ran, as it is recorded on the interaction (GENC-1660). */
21
+ function editOf(by, action, choice, args) {
22
+ const copy = jsonCopy(args);
23
+ return Object.assign({ by, action, choice }, (copy !== undefined && { args: copy }));
24
+ }
25
+ /**
26
+ * A JSON copy of an action's arguments, so the record cannot change under a widget that mutates
27
+ * what it was given. `undefined` when there are none (nothing, an empty object, list or string),
28
+ * or when they cannot be copied.
29
+ */
30
+ function jsonCopy(args) {
31
+ if (args === undefined || args === null)
32
+ return undefined;
33
+ try {
34
+ const copy = JSON.parse(JSON.stringify(args));
35
+ const empty = copy === '' ||
36
+ (Array.isArray(copy) && copy.length === 0) ||
37
+ (copy !== null && typeof copy === 'object' && Object.keys(copy).length === 0);
38
+ return empty ? undefined : copy;
39
+ }
40
+ catch (_a) {
41
+ return undefined;
42
+ }
43
+ }
44
+ /**
45
+ * What the requester is told a claimant did (GENC-1660): its decision, when it answered; or, when
46
+ * the person answered one it had changed first, those edits — the answer is theirs, but the agent
47
+ * still learns the widget was changed on their behalf before they answered.
48
+ */
49
+ function doneOnUsersBehalf(interaction, answeredBy) {
50
+ var _a, _b, _c, _d, _e;
51
+ const edits = ((_a = interaction.edits) === null || _a === void 0 ? void 0 : _a.length)
52
+ ? interaction.edits.map((edit) => edit.choice)
53
+ : undefined;
54
+ if (answeredBy && answeredBy !== 'user') {
55
+ const recorded = interaction.decision;
56
+ return {
57
+ decision: Object.assign(Object.assign(Object.assign({ framing: (_c = (_b = interaction.claimant) === null || _b === void 0 ? void 0 : _b.decisionFraming) !== null && _c !== void 0 ? _c : DEFAULT_DECISION_FRAMING }, (edits && { edits })), ((recorded === null || recorded === void 0 ? void 0 : recorded.choice) !== undefined && { choice: recorded.choice })), ((recorded === null || recorded === void 0 ? void 0 : recorded.shownToUser) !== undefined && { shownToUser: recorded.shownToUser })),
58
+ };
59
+ }
60
+ if (answeredBy === 'user' && edits) {
61
+ return {
62
+ edited: { framing: (_e = (_d = interaction.claimant) === null || _d === void 0 ? void 0 : _d.editFraming) !== null && _e !== void 0 ? _e : DEFAULT_EDIT_FRAMING, edits },
63
+ };
64
+ }
65
+ return {};
66
+ }
6
67
  /** What an interaction closed by its `signal` resolves with: unanswered, like a timeout. */
7
68
  const STOPPED = {
8
69
  status: 'timeout',
@@ -34,6 +95,70 @@ export class InteractionController {
34
95
  */
35
96
  this.interceptors = [];
36
97
  }
98
+ /** See `ChatDriver.getInteractionCheckpoint`. */
99
+ getCheckpoint() {
100
+ const cp = this.checkpoint;
101
+ return cp ? { componentName: cp.componentName, value: cp.value } : undefined;
102
+ }
103
+ /** See `ChatDriver.restoreInteractionCheckpoint`. Restored means cut: its interaction is gone. */
104
+ restoreCheckpoint(saved) {
105
+ this.checkpoint = saved
106
+ ? { componentName: saved.componentName, value: saved.value, cut: true }
107
+ : undefined;
108
+ }
109
+ /** Drop the checkpoint, used or not, when its flow ends. */
110
+ clearCheckpoint(why) {
111
+ const cp = this.checkpoint;
112
+ if (!cp)
113
+ return;
114
+ this.checkpoint = undefined;
115
+ recordMetaEvent(this.deps.sessionKey, 'interaction.checkpoint', Object.assign({ action: 'cleared', component: cp.componentName, why }, (cp.interactionId ? { interactionId: cp.interactionId } : {})));
116
+ }
117
+ /**
118
+ * Hand a cut checkpoint to a new request of the same widget type, if its registration's
119
+ * `sameWork` agrees. Only the first such request is asked: a refused checkpoint is dropped, and
120
+ * the work starts fresh as it would have without one.
121
+ */
122
+ takeOverCheckpoint(componentName, data, interactionId) {
123
+ var _a;
124
+ const cut = this.checkpoint;
125
+ if (!(cut === null || cut === void 0 ? void 0 : cut.cut) || cut.componentName !== componentName)
126
+ return undefined;
127
+ this.checkpoint = undefined;
128
+ const sameWork = (_a = getInteractionRegistration(componentName)) === null || _a === void 0 ? void 0 : _a.sameWork;
129
+ let agrees = true;
130
+ if (sameWork) {
131
+ try {
132
+ agrees = sameWork(cut.value, data);
133
+ }
134
+ catch (e) {
135
+ logger.warn(`InteractionController: "${componentName}" sameWork threw; starting fresh:`, e);
136
+ agrees = false;
137
+ }
138
+ }
139
+ recordMetaEvent(this.deps.sessionKey, 'interaction.checkpoint', Object.assign({ action: agrees ? 'resumed' : 'refused', component: componentName, interactionId }, (agrees ? {} : { saved: cut.value })));
140
+ if (!agrees)
141
+ return undefined;
142
+ this.checkpoint = { componentName, value: cut.value, interactionId };
143
+ return cut.value;
144
+ }
145
+ /** A controller's `ctx.saveCheckpoint`: kept as JSON, the form it is persisted in. */
146
+ saveCheckpoint(interactionId, componentName, value) {
147
+ var _a;
148
+ if (!this.pending.has(interactionId))
149
+ return;
150
+ let copy;
151
+ try {
152
+ copy = JSON.parse((_a = JSON.stringify(value)) !== null && _a !== void 0 ? _a : 'null');
153
+ }
154
+ catch (e) {
155
+ logger.warn(`InteractionController: a "${componentName}" checkpoint is not storable as JSON:`, e);
156
+ return;
157
+ }
158
+ this.checkpoint = { componentName, value: copy, interactionId };
159
+ // A finished step changes no message, so autosave would not see it: ask for a save.
160
+ this.deps.emit(new CustomEvent('checkpoint-saved', { detail: { interactionId } }));
161
+ }
37
162
  /** See `ChatDriver.addInteractionInterceptor`. Returns a remover. */
38
163
  addInterceptor(interceptor) {
39
164
  if (!interceptor.id || interceptor.id === 'user') {
@@ -92,11 +217,21 @@ export class InteractionController {
92
217
  holdFor(handle, interceptor, isClaimant) {
93
218
  return {
94
219
  set: (holder) => {
220
+ var _a;
95
221
  if (!isClaimant())
96
222
  return false;
97
223
  const next = holder === 'user' ? 'user' : interceptor.id;
98
224
  if (!handle.setHeldBy(next))
99
225
  return false;
226
+ // With the person now, so whatever settles it next is their answer — even if an editing
227
+ // action the claimant started is still unwinding. A finishing action still in flight keeps
228
+ // its credit: it is the claimant's choice, already made, and a pause must not pass it off as
229
+ // the person's. If that run does not settle it after all, its own restore clears the credit.
230
+ if (next === 'user') {
231
+ const pending = this.pending.get(handle.context.interactionId);
232
+ if (pending && !((_a = pending.running) === null || _a === void 0 ? void 0 : _a.finishes))
233
+ pending.settledBy = undefined;
234
+ }
100
235
  recordMetaEvent(this.deps.sessionKey, 'interaction.held', {
101
236
  interactionId: handle.context.interactionId,
102
237
  heldBy: next,
@@ -122,8 +257,114 @@ export class InteractionController {
122
257
  pending.decision = { choice: decision.choice, shownToUser: decision.shownToUser };
123
258
  return true;
124
259
  },
260
+ invoke: (action, args, options) => __awaiter(this, void 0, void 0, function* () {
261
+ var _a, _b, _c;
262
+ const { interactionId } = handle.context;
263
+ const pending = this.pending.get(interactionId);
264
+ if (!isClaimant() || !pending || handle.context.heldBy !== interceptor.id) {
265
+ return {
266
+ ok: false,
267
+ error: 'not-held',
268
+ message: pending
269
+ ? 'This interaction is with the person now, so it cannot be acted on.'
270
+ : 'This interaction has already ended.',
271
+ };
272
+ }
273
+ const { surface } = handle;
274
+ if (!surface) {
275
+ return { ok: false, error: 'no-surface', message: 'Nothing has been exposed here yet.' };
276
+ }
277
+ // One action at a time: the claimant's calls may run in parallel, and a second run would
278
+ // overwrite the first's record and attribution while it is still in flight. Refused, with
279
+ // nothing run, so the caller can try again once the first has returned.
280
+ if (pending.running) {
281
+ return Object.assign(Object.assign({ ok: false }, BUSY), { actions: surface.actions() });
282
+ }
283
+ const prepared = surface.prepare(action, args);
284
+ if ('error' in prepared) {
285
+ return Object.assign(Object.assign({ ok: false }, prepared), { actions: surface.actions() });
286
+ }
287
+ // What a finishing run records before it runs, put back unless the run settles it: one that
288
+ // throws, or one that leaves it open, must not leave the next settle credited to it. An
289
+ // editing action records neither, so it has nothing to put back.
290
+ const previous = { decision: pending.decision, settledBy: pending.settledBy };
291
+ const restore = () => {
292
+ if (!prepared.finishes)
293
+ return;
294
+ pending.decision = previous.decision;
295
+ // Handed to the person meanwhile: whatever settles it next is theirs (see `set`).
296
+ pending.settledBy =
297
+ handle.context.heldBy === interceptor.id ? previous.settledBy : undefined;
298
+ };
299
+ // Recorded BEFORE running: a finishing action resolves inside `run`, and the record and the
300
+ // attribution have to be in place when it does. A finishing action is the claimant's
301
+ // answer, as `hold.resolve` is; handing the interaction back clears it. An editing action
302
+ // is never an answer, so a settle while one is in flight is not credited to it.
303
+ if (prepared.finishes) {
304
+ pending.decision = { choice: prepared.choice, shownToUser: options === null || options === void 0 ? void 0 : options.note };
305
+ pending.settledBy = interceptor.id;
306
+ }
307
+ // The note is what the person is shown about the choice, in the assistant's voice: a
308
+ // delegated, display-only message. The platform stamps `delegate`, so it cannot be claimed
309
+ // falsely. Shown only once the action has run: when it settles, `resolve` appends it so it
310
+ // precedes the outcome; otherwise it is appended below.
311
+ const content = (_a = options === null || options === void 0 ? void 0 : options.note) === null || _a === void 0 ? void 0 : _a.trim();
312
+ const note = content ? { content, delegate: interceptor.id } : undefined;
313
+ const token = {};
314
+ pending.running = { token, finishes: prepared.finishes, note };
315
+ let output;
316
+ try {
317
+ // A run that returns at once is over at once: the claimant's parallel calls start in the
318
+ // same tick, so awaiting it anyway would refuse the next one as busy for nothing.
319
+ const ran = prepared.run();
320
+ output = isThenable(ran) ? yield ran : ran;
321
+ }
322
+ catch (e) {
323
+ logger.warn(`InteractionController: action "${action}" threw:`, e);
324
+ if (pending.settledInRun !== token)
325
+ restore();
326
+ return {
327
+ ok: false,
328
+ error: 'threw',
329
+ message: e instanceof Error ? e.message : String(e),
330
+ };
331
+ }
332
+ finally {
333
+ if (((_b = pending.running) === null || _b === void 0 ? void 0 : _b.token) === token)
334
+ pending.running = undefined;
335
+ }
336
+ // This run settled it — not merely "it is no longer pending", which a settle by someone
337
+ // else while the action was in flight would also satisfy.
338
+ const finished = pending.settledInRun === token;
339
+ // An editing action is not an answer: whoever finishes it later is credited with that. It
340
+ // is remembered as an edit instead, so the resolved view can mark it, and the working agent
341
+ // is told of it whoever finishes: in the claimant's decision when the claimant finishes it,
342
+ // or as edits made on the user's behalf that the person then confirmed.
343
+ if (!finished) {
344
+ restore();
345
+ if (note && this.pending.has(interactionId))
346
+ this.appendNote(note);
347
+ ((_c = pending.edits) !== null && _c !== void 0 ? _c : (pending.edits = [])).push(editOf(interceptor.id, action, prepared.choice, args));
348
+ }
349
+ recordMetaEvent(this.deps.sessionKey, 'interaction.invoked', {
350
+ interactionId,
351
+ claimant: interceptor.id,
352
+ action,
353
+ finished,
354
+ });
355
+ return Object.assign({ ok: true, finished, actions: finished ? [] : surface.actions() }, (output !== undefined && !finished ? { output } : {}));
356
+ }),
125
357
  };
126
358
  }
359
+ /** A claimant's note about its choice: a delegated, display-only message in the assistant's voice. */
360
+ appendNote(note) {
361
+ this.deps.appendToHistory({
362
+ role: 'assistant',
363
+ content: note.content,
364
+ delegate: note.delegate,
365
+ excludeFromProvider: true,
366
+ });
367
+ }
127
368
  /** See `ChatDriver.setHostInteractionRequester`. */
128
369
  setHostRequester(fn, recordOutcome) {
129
370
  this.hostRequester = fn;
@@ -175,8 +416,39 @@ export class InteractionController {
175
416
  // wrapper finds it when it builds the pending `interaction` descriptor, and
176
417
  // offer it to the interceptors (GENC-1660) before it is appended, so the
177
418
  // widget's first render already knows who holds it.
178
- const handle = createInteractionContext(interactionId);
419
+ // `ctx.resolve` settles through the same path a widget's `interaction-completed` takes —
420
+ // including a controller resolving in its constructor, before its message is appended,
421
+ // which `resolve` holds back until there is a message to patch.
422
+ const handle = createInteractionContext(interactionId, {
423
+ resolve: (result) => this.resolve(interactionId, result),
424
+ checkpoint: this.takeOverCheckpoint(componentName, data, interactionId),
425
+ saveCheckpoint: (value) => this.saveCheckpoint(interactionId, componentName, value),
426
+ });
179
427
  this.contexts.set(interactionId, handle);
428
+ bindStepRecorder(handle.context, (step, outcome) => recordMetaEvent(this.deps.sessionKey, 'interaction.step', {
429
+ interactionId,
430
+ component: componentName,
431
+ step,
432
+ outcome,
433
+ }), (reason, saved) => recordMetaEvent(this.deps.sessionKey, 'interaction.checkpoint', {
434
+ action: 'refused',
435
+ component: componentName,
436
+ interactionId,
437
+ reason,
438
+ saved,
439
+ }));
440
+ // Build the widget's controller from its registration (GENC-1660) BEFORE the interceptors are
441
+ // asked, so a pure question's surface is ready at the handoff.
442
+ const registration = getInteractionRegistration(componentName);
443
+ if (registration === null || registration === void 0 ? void 0 : registration.controller) {
444
+ try {
445
+ handle.setController(registration.controller(data, handle.context));
446
+ }
447
+ catch (e) {
448
+ // The widget still renders and the person can still answer it; it just has no surface.
449
+ logger.error(`InteractionController: the "${componentName}" controller threw:`, e);
450
+ }
451
+ }
180
452
  const heldBy = this.offer(handle, {
181
453
  interactionId,
182
454
  componentName,
@@ -235,7 +507,7 @@ export class InteractionController {
235
507
  * report twice, and the second report has nothing left to settle.
236
508
  */
237
509
  resolve(interactionId, result) {
238
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
510
+ var _a, _b, _c, _d, _e, _f, _g;
239
511
  const interaction = this.pending.get(interactionId);
240
512
  if (!interaction) {
241
513
  logger.warn(`Interaction with ID ${interactionId} not found.`);
@@ -268,15 +540,19 @@ export class InteractionController {
268
540
  // claimant holds is still the person's answer. A timeout or a stop is nobody's.
269
541
  const unanswered = (result === null || result === void 0 ? void 0 : result.status) === 'timeout';
270
542
  const answeredBy = unanswered ? undefined : ((_c = interaction.settledBy) !== null && _c !== void 0 ? _c : 'user');
271
- const decision = answeredBy && answeredBy !== 'user'
272
- ? Object.assign(Object.assign({ framing: (_e = (_d = interaction.claimant) === null || _d === void 0 ? void 0 : _d.decisionFraming) !== null && _e !== void 0 ? _e : DEFAULT_DECISION_FRAMING }, (((_f = interaction.decision) === null || _f === void 0 ? void 0 : _f.choice) !== undefined && {
273
- choice: interaction.decision.choice,
274
- })), (((_g = interaction.decision) === null || _g === void 0 ? void 0 : _g.shownToUser) !== undefined && {
275
- shownToUser: interaction.decision.shownToUser,
276
- })) : undefined;
543
+ // Settled under a claimant's `hold.invoke` run, credited to it: that run's note goes first, so
544
+ // it precedes the outcome, and the run learns that it was the one that finished it.
545
+ const run = answeredBy && answeredBy !== 'user' ? interaction.running : undefined;
546
+ if (run) {
547
+ interaction.settledInRun = run.token;
548
+ if (run.note)
549
+ this.appendNote(run.note);
550
+ }
551
+ const edits = ((_d = interaction.edits) === null || _d === void 0 ? void 0 : _d.length) ? interaction.edits : undefined;
552
+ const { decision, edited } = doneOnUsersBehalf(interaction, answeredBy);
277
553
  const patched = this.deps.patchInteractionMessage(interactionId, (message) => {
278
554
  var _a;
279
- return (Object.assign(Object.assign(Object.assign({}, message), { interaction: Object.assign(Object.assign(Object.assign({}, message.interaction), { resolved: result }), (answeredBy ? { answeredBy } : {})) }), (costUsd != null ? { externalCostUsd: ((_a = message.externalCostUsd) !== null && _a !== void 0 ? _a : 0) + costUsd } : {})));
555
+ return (Object.assign(Object.assign(Object.assign({}, message), { interaction: Object.assign(Object.assign(Object.assign(Object.assign({}, message.interaction), { resolved: result }), (answeredBy ? { answeredBy } : {})), (edits ? { edits } : {})) }), (costUsd != null ? { externalCostUsd: ((_a = message.externalCostUsd) !== null && _a !== void 0 ? _a : 0) + costUsd } : {})));
280
556
  });
281
557
  if (patched)
282
558
  this.deps.notifyHistoryUpdated();
@@ -304,23 +580,32 @@ export class InteractionController {
304
580
  if (interaction.overrideId) {
305
581
  this.deps.emit(new CustomEvent('interaction-stop', { detail: { interactionId } }));
306
582
  }
307
- recordMetaEvent(this.deps.sessionKey, 'interaction.resolved', Object.assign({ interactionId }, (answeredBy ? { answeredBy } : {})));
583
+ recordMetaEvent(this.deps.sessionKey, 'interaction.resolved', Object.assign(Object.assign({ interactionId }, (answeredBy ? { answeredBy } : {})), (edits ? { edits: edits.length } : {})));
308
584
  // The park is ending and the loop is about to resume computing — paired
309
585
  // with `interaction-requested`. Fires for every resolution path (user
310
586
  // completion, timeout, cancellation), since all route through here.
311
587
  this.deps.activityBus.publish('interaction-resolved', undefined);
312
588
  // Before the promise settles, so the record is in place when the handler carries on.
313
589
  try {
314
- (_h = interaction.onSettled) === null || _h === void 0 ? void 0 : _h.call(interaction, { answeredBy, decision });
590
+ (_e = interaction.onSettled) === null || _e === void 0 ? void 0 : _e.call(interaction, Object.assign({ answeredBy, decision }, (edited ? { edited } : {})));
315
591
  }
316
592
  catch (e) {
317
593
  logger.warn('InteractionController: onSettled threw:', e);
318
594
  }
595
+ // For the host, whoever settled it (GENC-1660): a controller resolving through `ctx.resolve`
596
+ // never passes through the host's `interaction-completed` handler, which is where it started
597
+ // its loading indicator.
598
+ this.deps.emit(new CustomEvent('interaction-settled', {
599
+ detail: Object.assign({ interactionId }, (answeredBy ? { answeredBy } : {})),
600
+ }));
319
601
  interaction.resolve(result);
320
602
  this.pending.delete(interactionId);
603
+ // Settled, so its work is finished with: nothing is left to resume.
604
+ if (((_f = this.checkpoint) === null || _f === void 0 ? void 0 : _f.interactionId) === interactionId)
605
+ this.checkpoint = undefined;
321
606
  // Tear down the live context on RESOLVE (not on element unmount) — this closes
322
607
  // any sockets/timers the widget parked on it via `onDispose`. (GENC-1390)
323
- (_j = this.contexts.get(interactionId)) === null || _j === void 0 ? void 0 : _j.dispose();
608
+ (_g = this.contexts.get(interactionId)) === null || _g === void 0 ? void 0 : _g.dispose();
324
609
  this.contexts.delete(interactionId);
325
610
  }
326
611
  /**
@@ -22,6 +22,12 @@ let AiChatInteractionWrapper = class AiChatInteractionWrapper extends GenesisEle
22
22
  this.resolved = undefined;
23
23
  /** See `AiChatInteraction`'s resolved `outcome`: recorded after the answer, by the tool. */
24
24
  this.outcome = undefined;
25
+ /**
26
+ * See `AiChatInteraction`'s resolved `edits`: what a claimant changed before it was settled. The
27
+ * template binds the message's `edits` straight over this, so `undefined` arrives whenever there
28
+ * were none: it means "no edits", never an error.
29
+ */
30
+ this.edits = undefined;
25
31
  }
26
32
  connectedCallback() {
27
33
  super.connectedCallback();
@@ -67,9 +73,14 @@ let AiChatInteractionWrapper = class AiChatInteractionWrapper extends GenesisEle
67
73
  outcomeChanged() {
68
74
  this.resolvedChanged();
69
75
  }
70
- /** The resolved descriptor, with the outcome when one has been recorded. */
76
+ /** Edits a claimant made reach a widget already showing the answer, the same way. */
77
+ editsChanged() {
78
+ this.resolvedChanged();
79
+ }
80
+ /** The resolved descriptor, with the outcome and the claimant's edits when there are any. */
71
81
  resolvedInteraction() {
72
- return Object.assign({ status: 'resolved', data: this.data, result: this.resolved }, (this.outcome !== undefined ? { outcome: this.outcome } : {}));
82
+ var _a;
83
+ return Object.assign(Object.assign({ status: 'resolved', data: this.data, result: this.resolved }, (this.outcome !== undefined ? { outcome: this.outcome } : {})), (((_a = this.edits) === null || _a === void 0 ? void 0 : _a.length) ? { edits: this.edits } : {}));
73
84
  }
74
85
  /**
75
86
  * Watch the rendered widget element for box-size changes. When the widget
@@ -213,6 +224,9 @@ __decorate([
213
224
  __decorate([
214
225
  observable
215
226
  ], AiChatInteractionWrapper.prototype, "outcome", void 0);
227
+ __decorate([
228
+ observable
229
+ ], AiChatInteractionWrapper.prototype, "edits", void 0);
216
230
  AiChatInteractionWrapper = __decorate([
217
231
  customElement({
218
232
  name: 'ai-chat-interaction-wrapper',