@genesislcap/ai-assistant 15.58.2 → 15.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/ai-assistant.api.json +10096 -6986
  2. package/dist/ai-assistant.d.ts +545 -101
  3. package/dist/chat-driver.cjs +624 -207
  4. package/dist/chat-driver.cjs.map +4 -4
  5. package/dist/chat-driver.mjs +624 -207
  6. package/dist/chat-driver.mjs.map +4 -4
  7. package/dist/custom-elements.json +1836 -378
  8. package/dist/dts/components/agent-session/agent-session.d.ts +19 -3
  9. package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -1
  10. package/dist/dts/components/ai-driver/ai-driver.d.ts +120 -10
  11. package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
  12. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +103 -0
  13. package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -0
  14. package/dist/dts/components/ai-driver/index.d.ts +3 -1
  15. package/dist/dts/components/ai-driver/index.d.ts.map +1 -1
  16. package/dist/dts/components/chat-driver/chat-driver.d.ts +84 -8
  17. package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
  18. package/dist/dts/components/chat-driver/interaction-controller.d.ts +37 -3
  19. package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
  20. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +30 -93
  21. package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
  22. package/dist/dts/index.d.ts +4 -1
  23. package/dist/dts/index.d.ts.map +1 -1
  24. package/dist/dts/main/main.d.ts +4 -4
  25. package/dist/dts/main/main.d.ts.map +1 -1
  26. package/dist/dts/state/debug-event-log.d.ts +1 -1
  27. package/dist/dts/state/debug-event-log.d.ts.map +1 -1
  28. package/dist/dts/state/interaction-context.d.ts +7 -1
  29. package/dist/dts/state/interaction-context.d.ts.map +1 -1
  30. package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
  31. package/dist/dts/suggestions/generate-suggestions.d.ts.map +1 -1
  32. package/dist/dts/types/handler-capability.d.ts +28 -0
  33. package/dist/dts/types/handler-capability.d.ts.map +1 -0
  34. package/dist/dts/types/interaction-context.d.ts +16 -0
  35. package/dist/dts/types/interaction-context.d.ts.map +1 -1
  36. package/dist/dts/types/interaction-interceptor.d.ts +109 -0
  37. package/dist/dts/types/interaction-interceptor.d.ts.map +1 -0
  38. package/dist/dts/utils/delegated-message.d.ts +52 -0
  39. package/dist/dts/utils/delegated-message.d.ts.map +1 -0
  40. package/dist/dts/utils/history-transform.d.ts.map +1 -1
  41. package/dist/esm/components/agent-session/agent-session.js +17 -0
  42. package/dist/esm/components/ai-driver/ai-driver.js +21 -1
  43. package/dist/esm/components/ai-driver/base-driver-decorator.js +211 -0
  44. package/dist/esm/components/ai-driver/index.js +2 -1
  45. package/dist/esm/components/chat-driver/chat-driver.js +195 -14
  46. package/dist/esm/components/chat-driver/interaction-controller.js +168 -14
  47. package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +72 -196
  48. package/dist/esm/index.js +1 -0
  49. package/dist/esm/main/main.js +79 -77
  50. package/dist/esm/state/debug-event-log.js +7 -2
  51. package/dist/esm/state/interaction-context.js +11 -0
  52. package/dist/esm/state/persistence/session-persister.js +5 -6
  53. package/dist/esm/suggestions/generate-suggestions.js +8 -1
  54. package/dist/esm/types/handler-capability.js +1 -0
  55. package/dist/esm/types/interaction-interceptor.js +1 -0
  56. package/dist/esm/utils/delegated-message.js +25 -0
  57. package/dist/esm/utils/history-transform.js +12 -1
  58. package/package.json +17 -17
@@ -6,6 +6,7 @@ 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
10
  import { applyHistoryCap, buildCompactionSummaryPrompt, planCompaction, normalizeForProvider, } from '../../utils/history-transform';
10
11
  import { logger } from '../../utils/logger';
11
12
  import { resolveTurnInput } from '../../utils/resolve-turn-input';
@@ -13,7 +14,7 @@ import { messageUsage, sumUsage } from '../../utils/sum-usage';
13
14
  import { AgentSession } from '../agent-session/agent-session';
14
15
  import { DEFAULT_MAX_FOLD_OPERATIONS, DEFAULT_MAX_TOOL_ITERATIONS, DEFAULT_MAX_TURN_SNAPSHOTS, DEFAULT_MAX_UNKNOWN_TOOL_CALLS, EMPTY_RESPONSE_NUDGE, HANDOFF_TOOL_RESULT_PLACEHOLDER, HARVESTED_SUBAGENT_EVENTS, MALFORMED_CALL_NUDGE, MAX_EMPTY_RESPONSE_RETRIES, MAX_MALFORMED_RETRIES, MAX_SETUP_TRANSPORT_RETRIES, MAX_STALE_TOOL_CALLS, REQUEST_CONTINUATION_TOOL, } from './chat-driver.constants';
15
16
  import { FoldController } from './fold-controller';
16
- import { InteractionController } from './interaction-controller';
17
+ import { InteractionController, } from './interaction-controller';
17
18
  import { TURN_FAILURE_EXITS, turnDone } from './turn-failures';
18
19
  import { TurnRecorder } from './turn-recorder';
19
20
  /** Re-exported so `from './chat-driver'` keeps resolving these. */
@@ -101,6 +102,28 @@ const budgetDetailOf = (e) => {
101
102
  return undefined;
102
103
  return Object.assign(Object.assign({ budgetUsd: e.budgetUsd, spentUsd: e.spentUsd, vendorLabel: e.vendorLabel }, (vendor ? { vendor } : {})), (e.otherVendorAvailable != null ? { otherVendorAvailable: e.otherVendorAvailable } : {}));
103
104
  };
105
+ /**
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.
110
+ */
111
+ function withDelegatedDecisions(result, content, decisions, hasImages) {
112
+ if (decisions.length === 0)
113
+ return content;
114
+ if (!hasImages && result !== null && typeof result === 'object' && !Array.isArray(result)) {
115
+ return JSON.stringify(Object.assign(Object.assign({}, result), { decidedOnUsersBehalf: decisions }));
116
+ }
117
+ return [content, ...decisions.map(describeDelegatedDecision)].join('\n\n');
118
+ }
119
+ /** One decision as a sentence for a text tool result, e.g. `Decided on the user's behalf: "Skip".` */
120
+ function describeDelegatedDecision(decision) {
121
+ const choice = decision.choice !== undefined ? `: "${decision.choice}"` : '';
122
+ const shown = decision.shownToUser
123
+ ? ` What the user was shown, as your words: "${decision.shownToUser}"`
124
+ : ' Nothing was shown to the user about it.';
125
+ return `${decision.framing}${choice}.${shown}`;
126
+ }
104
127
  /**
105
128
  * Where the tool calls still waiting for their results start: the last assistant message with a
106
129
  * call no later tool result answers. The history's length when every call has its answer.
@@ -272,6 +295,8 @@ export class ChatDriver extends EventTarget {
272
295
  * on that one was reset by every handoff inside a single message.
273
296
  */
274
297
  this.messageController = new AbortController();
298
+ /** Capabilities provided on this driver by decorators (GENC-1660). Only the root's are used. */
299
+ this.handlerCapabilities = new Map();
275
300
  /**
276
301
  * Caches validated provider lookups per name within the current agent. Cleared
277
302
  * by `installAgent` so each new agent's static/function-resolved names are
@@ -295,9 +320,17 @@ export class ChatDriver extends EventTarget {
295
320
  * the point: two would let `getActiveAgent()` disagree between the outer and inner driver.
296
321
  * `@internal` rather than private so that reach is legal without six delegating one-liners.
297
322
  *
323
+ * **This driver raises the lifecycle events for every transition of the session**, whoever made
324
+ * it: itself, through {@link ChatDriver.activateAgent} or a release, or the orchestrator reaching
325
+ * through (GENC-1485). Before, only the orchestrator dispatched them, so a host on a bare
326
+ * `ChatDriver` never learned its flow owner.
327
+ *
298
328
  * @internal
299
329
  */
300
- this.agentSession = new AgentSession({ sessionKey: () => this.sessionKey });
330
+ this.agentSession = new AgentSession({
331
+ sessionKey: () => this.sessionKey,
332
+ onTransition: (t) => this.dispatchAgentTransition(t),
333
+ });
301
334
  /**
302
335
  * A provider refusal was observed this turn, so the tool loop must not call the model again
303
336
  * (GENC-1506).
@@ -902,6 +935,55 @@ export class ChatDriver extends EventTarget {
902
935
  this.installAgent(config);
903
936
  });
904
937
  }
938
+ /**
939
+ * {@inheritDoc AiDriver.resumeRestoredFlow}
940
+ *
941
+ * This driver holds no list of agents, so it resumes only the `agent` it is handed (GENC-1660):
942
+ * whoever runs a bare `ChatDriver` knows which agent that is, and after a reload only the name
943
+ * survived. Without one it does nothing, as a driver without this member did before; given one
944
+ * that is not the restored flow owner, it refuses.
945
+ *
946
+ * For headless callers: the shipped host restores and resumes through `OrchestratingDriver`,
947
+ * and a bare `ChatDriver` does not answer `canRestoreFlowOwner`, so the host never plans a resume
948
+ * for one.
949
+ *
950
+ * The orchestrator does not delegate here, because it installs a decorated config of its own;
951
+ * this is the bare-driver path.
952
+ *
953
+ * @beta
954
+ */
955
+ resumeRestoredFlow(resumeNote, agent) {
956
+ return __awaiter(this, void 0, void 0, function* () {
957
+ if (!agent)
958
+ return undefined;
959
+ // Only the flow owner's flow is restored, so only the flow owner may be resumed: a caller
960
+ // handing in some other config would otherwise install and run the wrong agent.
961
+ const owner = this.agentSession.getFlowOwner();
962
+ if (owner !== null && owner !== agent.name) {
963
+ logger.warn(`ChatDriver: resumeRestoredFlow was handed "${agent.name}", but the flow belongs to "${owner}" — not resuming.`);
964
+ return undefined;
965
+ }
966
+ yield this.activateAgent(agent);
967
+ if (!resumeNote || agent.resumeOnRestore === false)
968
+ return undefined;
969
+ return this.continueFromHistory(undefined, { tailNote: resumeNote });
970
+ });
971
+ }
972
+ /**
973
+ * Raise the host-facing lifecycle events for one session transition (GENC-1485), in the order
974
+ * the orchestrator raised them before it stopped doing so itself: the flow lock first, then the
975
+ * release, then the active agent.
976
+ */
977
+ dispatchAgentTransition(t) {
978
+ if (t.flowOwnerChangedTo !== undefined) {
979
+ this.dispatchEvent(new CustomEvent('flow-owner-changed', { detail: t.flowOwnerChangedTo }));
980
+ }
981
+ const active = this.agentSession.getActive();
982
+ if (!active && t.previousAgent) {
983
+ this.dispatchEvent(new CustomEvent('agent-released', { detail: t.previousAgent }));
984
+ }
985
+ this.dispatchEvent(new CustomEvent('agent-changed', { detail: active }));
986
+ }
905
987
  /**
906
988
  * {@inheritDoc AiDriver.getActiveAgent}
907
989
  *
@@ -937,6 +1019,14 @@ export class ChatDriver extends EventTarget {
937
1019
  setFlowOwner(name) {
938
1020
  this.agentSession.setFlowOwner(name);
939
1021
  }
1022
+ /**
1023
+ * {@inheritDoc AiDriver.getFlowOwner}
1024
+ *
1025
+ * @beta
1026
+ */
1027
+ getFlowOwner() {
1028
+ return this.agentSession.getFlowOwner();
1029
+ }
940
1030
  /**
941
1031
  * Install `config` as the active agent **without firing lifecycle hooks**.
942
1032
  *
@@ -1586,11 +1676,84 @@ export class ChatDriver extends EventTarget {
1586
1676
  * and `presentation` to control whether the host wraps the widget in a chat
1587
1677
  * bubble and/or shows the "Assistant" label.
1588
1678
  */
1589
- requestInteraction(componentName, data, options, onOpened) {
1679
+ requestInteraction(componentName, data, options, onOpened, innerPath, onSettled) {
1590
1680
  return __awaiter(this, void 0, void 0, function* () {
1591
- return this.interactions.request(componentName, data, options, onOpened);
1681
+ return this.interactions.request(componentName, data, options, onOpened, innerPath, onSettled);
1592
1682
  });
1593
1683
  }
1684
+ /**
1685
+ * {@inheritDoc AiDriver.addInteractionInterceptor}
1686
+ *
1687
+ * Consulted only on the root driver, where every interaction opens: a sub-agent's request
1688
+ * travels up to its root, carrying its owner path, and is offered there once.
1689
+ *
1690
+ * @beta
1691
+ */
1692
+ addInteractionInterceptor(interceptor) {
1693
+ return this.interactions.addInterceptor(interceptor);
1694
+ }
1695
+ /**
1696
+ * {@inheritDoc AiDriver.presentInteraction}
1697
+ *
1698
+ * @beta
1699
+ */
1700
+ presentInteraction(componentName, data, options) {
1701
+ return this.requestInteraction(componentName, data, options);
1702
+ }
1703
+ /**
1704
+ * {@inheritDoc AiDriver.provideHandlerCapability}
1705
+ *
1706
+ * Throws if `name` is already provided: two decorators answering to one name would leave a
1707
+ * handler unable to tell which it is talking to.
1708
+ *
1709
+ * @beta
1710
+ */
1711
+ provideHandlerCapability(name, factory) {
1712
+ if (this.handlerCapabilities.has(name)) {
1713
+ throw new Error(`provideHandlerCapability: "${name}" is already provided.`);
1714
+ }
1715
+ this.handlerCapabilities.set(name, factory);
1716
+ return () => {
1717
+ if (this.handlerCapabilities.get(name) === factory)
1718
+ this.handlerCapabilities.delete(name);
1719
+ };
1720
+ }
1721
+ /**
1722
+ * The capabilities this driver's handlers see, and who they see them as: the root's factories,
1723
+ * the root's flow owner, and the owner path from the root down to this driver's agent.
1724
+ */
1725
+ capabilityScope() {
1726
+ const own = this.activeAgentName ? [this.activeAgentName] : [];
1727
+ if (!this.capabilityParent) {
1728
+ return {
1729
+ factories: this.handlerCapabilities,
1730
+ scope: { ownerPath: own, flowOwner: this.agentSession.getFlowOwner() },
1731
+ };
1732
+ }
1733
+ const up = this.capabilityParent.capabilityScope();
1734
+ return {
1735
+ factories: up.factories,
1736
+ scope: { ownerPath: [...up.scope.ownerPath, ...own], flowOwner: up.scope.flowOwner },
1737
+ };
1738
+ }
1739
+ /** Build `context.capabilities` for one handler call, or `undefined` when nothing is provided. */
1740
+ buildCapabilities() {
1741
+ const { factories, scope } = this.capabilityScope();
1742
+ if (factories.size === 0)
1743
+ return undefined;
1744
+ const capabilities = {};
1745
+ for (const [name, factory] of factories) {
1746
+ try {
1747
+ const value = factory(scope);
1748
+ if (value !== undefined)
1749
+ capabilities[name] = value;
1750
+ }
1751
+ catch (e) {
1752
+ logger.warn(`ChatDriver: handler capability "${name}" threw — leaving it out:`, e);
1753
+ }
1754
+ }
1755
+ return capabilities;
1756
+ }
1594
1757
  /**
1595
1758
  * Record what came of an interaction, after it resolved, on its history message
1596
1759
  * (`interaction.outcome`): handed to its widget and persisted with the session, never sent to a
@@ -1662,10 +1825,17 @@ export class ChatDriver extends EventTarget {
1662
1825
  this.history = [...messages];
1663
1826
  this.notifyHistoryUpdated();
1664
1827
  }
1665
- sendMessage(userInput, attachments) {
1828
+ sendMessage(userInput, attachments, options) {
1666
1829
  return __awaiter(this, void 0, void 0, function* () {
1830
+ var _a;
1667
1831
  if (this.busy || (!userInput.trim() && !(attachments === null || attachments === void 0 ? void 0 : attachments.length)))
1668
1832
  return { reason: 'done' };
1833
+ // Sent on the user's behalf (GENC-1660): stamped here, the only place that may, and framed for
1834
+ // the model the same way on the first call (the input) as on every later one (history).
1835
+ const delegate = options === null || options === void 0 ? void 0 : options.delegate;
1836
+ const delegateFraming = delegate
1837
+ ? ((_a = options === null || options === void 0 ? void 0 : options.delegateFraming) !== null && _a !== void 0 ? _a : DEFAULT_DELEGATE_FRAMING)
1838
+ : undefined;
1669
1839
  this.busy = true;
1670
1840
  this.beginTurn();
1671
1841
  // A new user message. `continueFromHistory` keeps the scope, unless the message it would
@@ -1686,7 +1856,7 @@ export class ChatDriver extends EventTarget {
1686
1856
  this.aiServiceWall = undefined;
1687
1857
  this.aiServiceNoticesShown.clear();
1688
1858
  this.aiServiceNoticesQueued = [];
1689
- this.appendToHistory({ role: 'user', content: userInput, attachments });
1859
+ this.appendToHistory(Object.assign({ role: 'user', content: userInput, attachments }, (delegate ? { delegate, delegateFraming } : {})));
1690
1860
  this.turnStartedAt = Date.now();
1691
1861
  recordMetaEvent(this.sessionKey, 'turn.start', {
1692
1862
  phase: 'sendMessage',
@@ -1696,7 +1866,7 @@ export class ChatDriver extends EventTarget {
1696
1866
  // Captured so the `finally` can carry the turn's outcome onto `tool-loop-end`.
1697
1867
  let result = { reason: 'done' };
1698
1868
  try {
1699
- result = yield this.runToolLoop(userInput, attachments);
1869
+ result = yield this.runToolLoop(delegate ? frameDelegatedContent(userInput, delegateFraming) : userInput, attachments);
1700
1870
  return result;
1701
1871
  }
1702
1872
  catch (e) {
@@ -1753,14 +1923,20 @@ export class ChatDriver extends EventTarget {
1753
1923
  * kept on a seven-way fan-out, reporting ~2.3x under the real cost, with the
1754
1924
  * error growing as the fan-out widens.
1755
1925
  */
1756
- buildHandlerContext(activeToolCallId, traceCapture) {
1926
+ buildHandlerContext(activeToolCallId, traceCapture, decisions) {
1757
1927
  var _a;
1758
1928
  // The interaction THIS call opened, if any: the only one its `recordInteractionOutcome` can
1759
1929
  // reach, so a tool can never write an outcome onto a widget another tool opened.
1760
1930
  let opened;
1761
1931
  let recorded = false;
1762
- return Object.assign(Object.assign({ requestInteraction: (componentName, data, options) => this.requestInteraction(componentName, data, options, (interactionId) => {
1932
+ const capabilities = this.buildCapabilities();
1933
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, (capabilities && { capabilities })), { requestInteraction: (componentName, data, options) => this.requestInteraction(componentName, data, options, (interactionId) => {
1763
1934
  opened = interactionId;
1935
+ }, undefined,
1936
+ // An interaction an interceptor answered is reported in THIS call's result (GENC-1660).
1937
+ (settled) => {
1938
+ if (settled.decision)
1939
+ decisions === null || decisions === void 0 ? void 0 : decisions.push(settled.decision);
1764
1940
  }), recordInteractionOutcome: (outcome) => {
1765
1941
  var _a;
1766
1942
  if (!opened)
@@ -1776,7 +1952,7 @@ export class ChatDriver extends EventTarget {
1776
1952
  // The MESSAGE's scope: one object per `sendMessage`, kept across a handoff's continuation
1777
1953
  // and handed to a sub-agent, and aborted by `cancel()` as well as a dispose. Not the
1778
1954
  // per-request `turnController`, which every continuation replaces.
1779
- turnSignal: (_a = this.inheritedTurnSignal) !== null && _a !== void 0 ? _a : this.messageController.signal }, (this.subAgentsMap.size > 0 && {
1955
+ turnSignal: (_a = this.inheritedTurnSignal) !== null && _a !== void 0 ? _a : this.messageController.signal }), (this.subAgentsMap.size > 0 && {
1780
1956
  requestSubAgent: (name, options) => this.invokeSubAgent(name, options).then(({ outcome, trace }) => {
1781
1957
  // Append, never assign: see `traceCapture` on `buildHandlerContext`. Order
1782
1958
  // is completion order, which is fine — every message carries its own
@@ -1907,11 +2083,14 @@ export class ChatDriver extends EventTarget {
1907
2083
  // parent's (ultimately the root's) history and resolve via the same
1908
2084
  // pending map the main UI is wired to. Recurses naturally for nested
1909
2085
  // sub-agents.
1910
- child.setHostInteractionRequester((componentName, data, opts, onOpened) => this.requestInteraction(componentName, data, opts, onOpened),
2086
+ child.setHostInteractionRequester((componentName, data, opts, onOpened, ownerPath, onSettled) => this.requestInteraction(componentName, data, opts, onOpened, ownerPath, onSettled),
1911
2087
  // An outcome goes where its interaction went.
1912
2088
  (interactionId, outcome) => this.recordInteractionOutcome(interactionId, outcome));
1913
2089
  // The same route for a refusal the child absorbs, so the user sees it in the root transcript.
1914
2090
  child.hostAIServiceNotice = (code) => this.noticeAIServiceRefusal(code);
2091
+ // And for the capabilities decorators provide (GENC-1660): the child's handlers see this
2092
+ // driver's, scoped to this driver's flow, with the child's agent added to the owner path.
2093
+ child.capabilityParent = this;
1915
2094
  const forwardTrace = (e) => {
1916
2095
  this.dispatchEvent(new CustomEvent('sub-agent-history-updated', {
1917
2096
  detail: { agentName: subConfig.name, history: e.detail },
@@ -2944,8 +3123,10 @@ export class ChatDriver extends EventTarget {
2944
3123
  // `sumUsage` and `usageRows` would still agree with each other.
2945
3124
  const traceCapture = { traces: [] };
2946
3125
  const capturedTrace = () => traceCapture.traces.length ? traceCapture.traces.flat() : undefined;
3126
+ // Interactions this call awaited that an interceptor answered (GENC-1660).
3127
+ const decisions = [];
2947
3128
  try {
2948
- const result = yield handler(tc.args, this.buildHandlerContext(tc.id, traceCapture));
3129
+ const result = yield handler(tc.args, this.buildHandlerContext(tc.id, traceCapture, decisions));
2949
3130
  // A handler that returns images gets them carried as real attachments instead of
2950
3131
  // JSON-stringified into the text — the difference between a model reading
2951
3132
  // `{"kind":"image","data":"iVBOR…"}` as prose and actually SEEING the render.
@@ -2953,13 +3134,13 @@ export class ChatDriver extends EventTarget {
2953
3134
  // ordinary object return that happens to have an `attachments` key still
2954
3135
  // stringifies exactly as before.
2955
3136
  const resultImages = toolResultImages(result);
2956
- const content = resultImages
3137
+ const content = withDelegatedDecisions(result, resultImages
2957
3138
  ? typeof result.content === 'string'
2958
3139
  ? result.content
2959
3140
  : JSON.stringify(Object.assign(Object.assign({}, result), { attachments: undefined }))
2960
3141
  : typeof result === 'string'
2961
3142
  ? result
2962
- : JSON.stringify(result);
3143
+ : JSON.stringify(result), decisions, !!resultImages);
2963
3144
  executedById.set(tc.id, Object.assign(Object.assign({ toolCallId: tc.id, content }, (resultImages ? { attachments: resultImages } : {})), {
2964
3145
  // Concatenated when a handler invoked several children, so none is lost.
2965
3146
  // Stays `undefined` when nothing was captured — readers key off presence,
@@ -1,6 +1,8 @@
1
1
  import { recordMetaEvent } from '../../state/debug-event-log';
2
2
  import { createInteractionContext, } from '../../state/interaction-context';
3
3
  import { logger } from '../../utils/logger';
4
+ /** The framing of a {@link DelegatedDecision} whose claimant declares none. */
5
+ const DEFAULT_DECISION_FRAMING = "Decided on the user's behalf";
4
6
  /** What an interaction closed by its `signal` resolves with: unanswered, like a timeout. */
5
7
  const STOPPED = {
6
8
  status: 'timeout',
@@ -26,6 +28,101 @@ export class InteractionController {
26
28
  * one-interaction-at-a-time guard in `request`).
27
29
  */
28
30
  this.contexts = new Map();
31
+ /**
32
+ * Told about every interaction this controller opens (GENC-1660), in registration order. Only
33
+ * the root's are ever consulted: a sub-agent's request is opened by the root.
34
+ */
35
+ this.interceptors = [];
36
+ }
37
+ /** See `ChatDriver.addInteractionInterceptor`. Returns a remover. */
38
+ addInterceptor(interceptor) {
39
+ if (!interceptor.id || interceptor.id === 'user') {
40
+ throw new Error(`addInteractionInterceptor: an interceptor needs an id other than 'user' (got "${interceptor.id}").`);
41
+ }
42
+ this.interceptors.push(interceptor);
43
+ return () => {
44
+ const index = this.interceptors.indexOf(interceptor);
45
+ if (index >= 0)
46
+ this.interceptors.splice(index, 1);
47
+ };
48
+ }
49
+ /**
50
+ * Offer a new interaction to the interceptors and record who ends up holding it.
51
+ *
52
+ * **Outermost first.** A decorator installs its interceptor on the driver it wraps when it is
53
+ * constructed, and decorators are constructed innermost first, so registration order is
54
+ * innermost first and the walk runs it backwards. The first claim wins; later interceptors still
55
+ * see the interaction but their `hold` does nothing. A claim on a `userOnly` interaction is
56
+ * refused.
57
+ *
58
+ * Runs before the interaction's message is appended, so the widget's first render already knows
59
+ * its holder and nothing appears only to disappear.
60
+ */
61
+ offer(handle, opened) {
62
+ let claimant;
63
+ for (const interceptor of [...this.interceptors].reverse()) {
64
+ const hold = this.holdFor(handle, interceptor, () => claimant === interceptor);
65
+ let answer;
66
+ try {
67
+ answer = interceptor.onInteractionOpened(Object.assign(Object.assign({}, opened), { ctx: handle.context, hold }));
68
+ }
69
+ catch (e) {
70
+ logger.warn(`InteractionController: interceptor "${interceptor.id}" threw:`, e);
71
+ continue;
72
+ }
73
+ if (answer !== 'claim' || claimant)
74
+ continue;
75
+ if (opened.userOnly) {
76
+ recordMetaEvent(this.deps.sessionKey, 'interaction.claim-refused', {
77
+ interactionId: opened.interactionId,
78
+ claimant: interceptor.id,
79
+ reason: 'userOnly',
80
+ });
81
+ continue;
82
+ }
83
+ claimant = interceptor;
84
+ handle.setHeldBy(interceptor.id);
85
+ const pending = this.pending.get(opened.interactionId);
86
+ if (pending)
87
+ pending.claimant = interceptor;
88
+ }
89
+ return handle.context.heldBy;
90
+ }
91
+ /** The {@link InteractionHold} for one interceptor: it applies only while that one is the claimant. */
92
+ holdFor(handle, interceptor, isClaimant) {
93
+ return {
94
+ set: (holder) => {
95
+ if (!isClaimant())
96
+ return false;
97
+ const next = holder === 'user' ? 'user' : interceptor.id;
98
+ if (!handle.setHeldBy(next))
99
+ return false;
100
+ recordMetaEvent(this.deps.sessionKey, 'interaction.held', {
101
+ interactionId: handle.context.interactionId,
102
+ heldBy: next,
103
+ claimant: interceptor.id,
104
+ });
105
+ return true;
106
+ },
107
+ resolve: (result) => {
108
+ const { interactionId } = handle.context;
109
+ const pending = this.pending.get(interactionId);
110
+ if (!isClaimant() || !pending || handle.context.heldBy !== interceptor.id)
111
+ return false;
112
+ pending.settledBy = interceptor.id;
113
+ this.resolve(interactionId, result);
114
+ return true;
115
+ },
116
+ recordDecision: (decision) => {
117
+ if (!isClaimant())
118
+ return false;
119
+ const pending = this.pending.get(handle.context.interactionId);
120
+ if (!pending)
121
+ return false;
122
+ pending.decision = { choice: decision.choice, shownToUser: decision.shownToUser };
123
+ return true;
124
+ },
125
+ };
29
126
  }
30
127
  /** See `ChatDriver.setHostInteractionRequester`. */
31
128
  setHostRequester(fn, recordOutcome) {
@@ -35,10 +132,15 @@ export class InteractionController {
35
132
  /**
36
133
  * See `ChatDriver.requestInteraction`. `onOpened` is told the id of the interaction this call
37
134
  * opens, wherever it opens it, so the caller can later record its outcome and no other's.
135
+ * `innerPath` is the owner path a sub-agent's request has gathered on its way up, and
136
+ * `onSettled` is told who answered, wherever it resolves (GENC-1660).
38
137
  */
39
- request(componentName, data, options, onOpened) {
138
+ request(componentName, data, options, onOpened, innerPath = [], onSettled) {
139
+ // This driver's agent goes in front: the path reads outermost first, root to requester.
140
+ const agent = this.deps.activeAgentName();
141
+ const ownerPath = agent ? [agent, ...innerPath] : [...innerPath];
40
142
  if (this.hostRequester) {
41
- return this.hostRequester(componentName, data, options, onOpened);
143
+ return this.hostRequester(componentName, data, options, onOpened, ownerPath, onSettled);
42
144
  }
43
145
  if (this.pending.size > 0) {
44
146
  throw new Error('requestInteraction: another user interaction is already in flight. ' +
@@ -54,6 +156,7 @@ export class InteractionController {
54
156
  this.pending.set(interactionId, {
55
157
  resolve,
56
158
  reject,
159
+ onSettled,
57
160
  overrideId: chatInputDuringExecution ? interactionId : undefined,
58
161
  // Resolve the interaction with a timeout result if the user does not
59
162
  // respond in time. `resolve` runs the same teardown a normal resolution
@@ -68,11 +171,21 @@ export class InteractionController {
68
171
  }, timeoutMs)
69
172
  : undefined,
70
173
  });
71
- recordMetaEvent(this.deps.sessionKey, 'interaction.requested', {
174
+ // Create the live context (GENC-1390) BEFORE the widget can mount, so the
175
+ // wrapper finds it when it builds the pending `interaction` descriptor, and
176
+ // offer it to the interceptors (GENC-1660) before it is appended, so the
177
+ // widget's first render already knows who holds it.
178
+ const handle = createInteractionContext(interactionId);
179
+ this.contexts.set(interactionId, handle);
180
+ const heldBy = this.offer(handle, {
72
181
  interactionId,
73
- component: componentName,
74
- agent: this.deps.activeAgentName(),
182
+ componentName,
183
+ data,
184
+ ownerPath,
185
+ userOnly: (options === null || options === void 0 ? void 0 : options.userOnly) === true,
75
186
  });
187
+ recordMetaEvent(this.deps.sessionKey, 'interaction.requested', Object.assign({ interactionId, component: componentName, agent: this.deps.activeAgentName(), ownerPath,
188
+ heldBy }, ((options === null || options === void 0 ? void 0 : options.userOnly) ? { userOnly: true } : {})));
76
189
  // Signal the park boundary on the activity bus so turn-aware UI can tell
77
190
  // "actively computing" from "parked awaiting the user" — the latter is a
78
191
  // safe window for actions disallowed mid-request (e.g. switching provider
@@ -83,9 +196,6 @@ export class InteractionController {
83
196
  detail: { interactionId, chatInputDuringExecution },
84
197
  }));
85
198
  }
86
- // Create the live context (GENC-1390) BEFORE the widget can mount, so the
87
- // wrapper finds it when it builds the pending `interaction` descriptor.
88
- this.contexts.set(interactionId, createInteractionContext(interactionId));
89
199
  this.deps.appendToHistory({
90
200
  role: 'assistant',
91
201
  content: '',
@@ -93,6 +203,14 @@ export class InteractionController {
93
203
  componentName,
94
204
  data }, (presentation ? { presentation } : {})),
95
205
  });
206
+ // A resolution that arrived while the interceptors were being asked — before there was a
207
+ // message to patch — is applied now that there is one.
208
+ const opened = this.pending.get(interactionId);
209
+ if (opened) {
210
+ opened.appended = true;
211
+ if (opened.early)
212
+ this.resolve(interactionId, opened.early.result);
213
+ }
96
214
  // Closed by the caller's signal the way a timeout closes it: read-only, unanswered, and
97
215
  // through the same teardown. Wired after the widget is in history, so a signal that has
98
216
  // already aborted closes a widget that exists rather than one still to come.
@@ -117,17 +235,24 @@ export class InteractionController {
117
235
  * report twice, and the second report has nothing left to settle.
118
236
  */
119
237
  resolve(interactionId, result) {
120
- var _a, _b;
238
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
121
239
  const interaction = this.pending.get(interactionId);
122
240
  if (!interaction) {
123
241
  logger.warn(`Interaction with ID ${interactionId} not found.`);
124
242
  return;
125
243
  }
244
+ // Too early: an interceptor answered before the message was appended. Patching would find no
245
+ // message, and the append that follows would leave a widget pending forever, so hold it until
246
+ // the message is there. First answer wins, as it would have.
247
+ if (!interaction.appended) {
248
+ (_a = interaction.early) !== null && _a !== void 0 ? _a : (interaction.early = { result });
249
+ return;
250
+ }
126
251
  // Clear the timeout so a user resolution doesn't later fire a stale
127
252
  // timeout (and a timeout firing here clears its own already-fired handle).
128
253
  if (interaction.timeoutHandle)
129
254
  clearTimeout(interaction.timeoutHandle);
130
- (_a = interaction.unlinkSignal) === null || _a === void 0 ? void 0 : _a.call(interaction);
255
+ (_b = interaction.unlinkSignal) === null || _b === void 0 ? void 0 : _b.call(interaction);
131
256
  // Harvest any out-of-band diagnostic entries the widget collated (e.g. from a server-side
132
257
  // ChatDriver) so the host's debug log + persisted diagnostics include them. Independent of the
133
258
  // message patched below — they describe the widget's own work, not this message.
@@ -138,9 +263,20 @@ export class InteractionController {
138
263
  const costUsd = typeof (result === null || result === void 0 ? void 0 : result.costUsd) === 'number' && Number.isFinite(result.costUsd) && result.costUsd > 0
139
264
  ? result.costUsd
140
265
  : undefined;
266
+ // Who answered (GENC-1660), by the route the answer took, not by who held it: the claimant
267
+ // only when it settled through its hold, the person otherwise — so a click on a widget a
268
+ // claimant holds is still the person's answer. A timeout or a stop is nobody's.
269
+ const unanswered = (result === null || result === void 0 ? void 0 : result.status) === 'timeout';
270
+ 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;
141
277
  const patched = this.deps.patchInteractionMessage(interactionId, (message) => {
142
278
  var _a;
143
- return (Object.assign(Object.assign(Object.assign({}, message), { interaction: Object.assign(Object.assign({}, message.interaction), { resolved: result }) }), (costUsd != null ? { externalCostUsd: ((_a = message.externalCostUsd) !== null && _a !== void 0 ? _a : 0) + costUsd } : {})));
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 } : {})));
144
280
  });
145
281
  if (patched)
146
282
  this.deps.notifyHistoryUpdated();
@@ -151,22 +287,40 @@ export class InteractionController {
151
287
  // the model already learns the outcome from this handler's return value.
152
288
  // Appended before `interaction.resolve` so it precedes the tool result and
153
289
  // any follow-up turn the resumed handler produces.
290
+ //
291
+ // When a claimant answered (GENC-1660), the echo is not something the user said: it is kept
292
+ // as a delegated, display-only assistant message, so it renders on the assistant's side and
293
+ // never as a user bubble the user did not write.
154
294
  if (typeof (result === null || result === void 0 ? void 0 : result.userMessage) === 'string' && result.userMessage.trim()) {
155
- this.deps.appendToHistory({ role: 'synthetic-user', content: result.userMessage });
295
+ this.deps.appendToHistory(answeredBy && answeredBy !== 'user'
296
+ ? {
297
+ role: 'assistant',
298
+ content: result.userMessage,
299
+ delegate: answeredBy,
300
+ excludeFromProvider: true,
301
+ }
302
+ : { role: 'synthetic-user', content: result.userMessage });
156
303
  }
157
304
  if (interaction.overrideId) {
158
305
  this.deps.emit(new CustomEvent('interaction-stop', { detail: { interactionId } }));
159
306
  }
160
- recordMetaEvent(this.deps.sessionKey, 'interaction.resolved', { interactionId });
307
+ recordMetaEvent(this.deps.sessionKey, 'interaction.resolved', Object.assign({ interactionId }, (answeredBy ? { answeredBy } : {})));
161
308
  // The park is ending and the loop is about to resume computing — paired
162
309
  // with `interaction-requested`. Fires for every resolution path (user
163
310
  // completion, timeout, cancellation), since all route through here.
164
311
  this.deps.activityBus.publish('interaction-resolved', undefined);
312
+ // Before the promise settles, so the record is in place when the handler carries on.
313
+ try {
314
+ (_h = interaction.onSettled) === null || _h === void 0 ? void 0 : _h.call(interaction, { answeredBy, decision });
315
+ }
316
+ catch (e) {
317
+ logger.warn('InteractionController: onSettled threw:', e);
318
+ }
165
319
  interaction.resolve(result);
166
320
  this.pending.delete(interactionId);
167
321
  // Tear down the live context on RESOLVE (not on element unmount) — this closes
168
322
  // any sockets/timers the widget parked on it via `onDispose`. (GENC-1390)
169
- (_b = this.contexts.get(interactionId)) === null || _b === void 0 ? void 0 : _b.dispose();
323
+ (_j = this.contexts.get(interactionId)) === null || _j === void 0 ? void 0 : _j.dispose();
170
324
  this.contexts.delete(interactionId);
171
325
  }
172
326
  /**