@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.
- package/dist/ai-assistant.api.json +10096 -6986
- package/dist/ai-assistant.d.ts +545 -101
- package/dist/chat-driver.cjs +624 -207
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +624 -207
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +1836 -378
- package/dist/dts/components/agent-session/agent-session.d.ts +19 -3
- package/dist/dts/components/agent-session/agent-session.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/ai-driver.d.ts +120 -10
- package/dist/dts/components/ai-driver/ai-driver.d.ts.map +1 -1
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts +103 -0
- package/dist/dts/components/ai-driver/base-driver-decorator.d.ts.map +1 -0
- package/dist/dts/components/ai-driver/index.d.ts +3 -1
- package/dist/dts/components/ai-driver/index.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/chat-driver.d.ts +84 -8
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/interaction-controller.d.ts +37 -3
- package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -1
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts +30 -93
- package/dist/dts/components/orchestrating-driver/orchestrating-driver.d.ts.map +1 -1
- package/dist/dts/index.d.ts +4 -1
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +4 -4
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/state/debug-event-log.d.ts +1 -1
- package/dist/dts/state/debug-event-log.d.ts.map +1 -1
- package/dist/dts/state/interaction-context.d.ts +7 -1
- package/dist/dts/state/interaction-context.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persister.d.ts.map +1 -1
- package/dist/dts/suggestions/generate-suggestions.d.ts.map +1 -1
- package/dist/dts/types/handler-capability.d.ts +28 -0
- package/dist/dts/types/handler-capability.d.ts.map +1 -0
- package/dist/dts/types/interaction-context.d.ts +16 -0
- package/dist/dts/types/interaction-context.d.ts.map +1 -1
- package/dist/dts/types/interaction-interceptor.d.ts +109 -0
- package/dist/dts/types/interaction-interceptor.d.ts.map +1 -0
- package/dist/dts/utils/delegated-message.d.ts +52 -0
- package/dist/dts/utils/delegated-message.d.ts.map +1 -0
- package/dist/dts/utils/history-transform.d.ts.map +1 -1
- package/dist/esm/components/agent-session/agent-session.js +17 -0
- package/dist/esm/components/ai-driver/ai-driver.js +21 -1
- package/dist/esm/components/ai-driver/base-driver-decorator.js +211 -0
- package/dist/esm/components/ai-driver/index.js +2 -1
- package/dist/esm/components/chat-driver/chat-driver.js +195 -14
- package/dist/esm/components/chat-driver/interaction-controller.js +168 -14
- package/dist/esm/components/orchestrating-driver/orchestrating-driver.js +72 -196
- package/dist/esm/index.js +1 -0
- package/dist/esm/main/main.js +79 -77
- package/dist/esm/state/debug-event-log.js +7 -2
- package/dist/esm/state/interaction-context.js +11 -0
- package/dist/esm/state/persistence/session-persister.js +5 -6
- package/dist/esm/suggestions/generate-suggestions.js +8 -1
- package/dist/esm/types/handler-capability.js +1 -0
- package/dist/esm/types/interaction-interceptor.js +1 -0
- package/dist/esm/utils/delegated-message.js +25 -0
- package/dist/esm/utils/history-transform.js +12 -1
- 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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
(
|
|
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(
|
|
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
|
-
(
|
|
323
|
+
(_j = this.contexts.get(interactionId)) === null || _j === void 0 ? void 0 : _j.dispose();
|
|
170
324
|
this.contexts.delete(interactionId);
|
|
171
325
|
}
|
|
172
326
|
/**
|