@genesislcap/ai-assistant 15.26.0 → 15.27.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 +150 -7
- package/dist/ai-assistant.d.ts +82 -99
- package/dist/chat-driver.cjs +460 -373
- package/dist/chat-driver.cjs.map +4 -4
- package/dist/chat-driver.mjs +460 -373
- package/dist/chat-driver.mjs.map +4 -4
- package/dist/custom-elements.json +682 -202
- package/dist/dts/components/chat-driver/chat-driver.d.ts +28 -200
- package/dist/dts/components/chat-driver/chat-driver.d.ts.map +1 -1
- package/dist/dts/components/chat-driver/interaction-controller.d.ts +99 -0
- package/dist/dts/components/chat-driver/interaction-controller.d.ts.map +1 -0
- package/dist/dts/components/chat-driver/turn-recorder.d.ts +191 -0
- package/dist/dts/components/chat-driver/turn-recorder.d.ts.map +1 -0
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts +20 -0
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +32 -1
- package/dist/dts/main/main.d.ts.map +1 -1
- package/dist/dts/main/main.styles.d.ts.map +1 -1
- package/dist/dts/main/main.template.d.ts +19 -0
- package/dist/dts/main/main.template.d.ts.map +1 -1
- package/dist/dts/state/ai-assistant-slice.d.ts +7 -1
- package/dist/dts/state/ai-assistant-slice.d.ts.map +1 -1
- package/dist/dts/state/persistence/session-persistence-provider.d.ts +5 -0
- package/dist/dts/state/persistence/session-persistence-provider.d.ts.map +1 -1
- package/dist/dts/state/session-store.d.ts +1 -0
- package/dist/dts/state/session-store.d.ts.map +1 -1
- package/dist/dts/utils/format-message-timestamp.d.ts +77 -0
- package/dist/dts/utils/format-message-timestamp.d.ts.map +1 -0
- package/dist/dts/utils/resolve-preference-baseline.d.ts +18 -1
- package/dist/dts/utils/resolve-preference-baseline.d.ts.map +1 -1
- package/dist/esm/components/chat-driver/chat-driver.js +68 -404
- package/dist/esm/components/chat-driver/interaction-controller.js +235 -0
- package/dist/esm/components/chat-driver/turn-recorder.js +206 -0
- package/dist/esm/components/settings-modal/settings-modal.template.js +53 -2
- package/dist/esm/main/main.js +80 -17
- package/dist/esm/main/main.styles.js +75 -0
- package/dist/esm/main/main.template.js +43 -2
- package/dist/esm/state/ai-assistant-slice.js +5 -0
- package/dist/esm/utils/format-message-timestamp.js +126 -0
- package/dist/esm/utils/resolve-preference-baseline.js +54 -2
- package/package.json +16 -16
- package/sandbox/README.md +45 -2
- package/sandbox/sandbox.ts +95 -3
package/dist/chat-driver.cjs
CHANGED
|
@@ -3790,70 +3790,6 @@ function clearSession(key) {
|
|
|
3790
3790
|
// src/utils/logger.ts
|
|
3791
3791
|
var logger2 = createLogger("ai-assistant");
|
|
3792
3792
|
|
|
3793
|
-
// src/state/interaction-context.ts
|
|
3794
|
-
function createInteractionContext(interactionId) {
|
|
3795
|
-
const state = {};
|
|
3796
|
-
const onceCache = /* @__PURE__ */ new Map();
|
|
3797
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
3798
|
-
const disposers = /* @__PURE__ */ new Set();
|
|
3799
|
-
let disposed = false;
|
|
3800
|
-
const context = {
|
|
3801
|
-
interactionId,
|
|
3802
|
-
state,
|
|
3803
|
-
once(key, factory) {
|
|
3804
|
-
if (onceCache.has(key)) return onceCache.get(key);
|
|
3805
|
-
if (disposed) {
|
|
3806
|
-
logger2.warn(
|
|
3807
|
-
`InteractionContext.once("${key}") called after dispose; not creating a resource on an ended interaction.`
|
|
3808
|
-
);
|
|
3809
|
-
return null;
|
|
3810
|
-
}
|
|
3811
|
-
const value = factory();
|
|
3812
|
-
onceCache.set(key, value);
|
|
3813
|
-
return value;
|
|
3814
|
-
},
|
|
3815
|
-
notify() {
|
|
3816
|
-
for (const listener of listeners) {
|
|
3817
|
-
try {
|
|
3818
|
-
listener();
|
|
3819
|
-
} catch (e) {
|
|
3820
|
-
logger2.error("InteractionContext subscriber threw during notify()", e);
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
},
|
|
3824
|
-
subscribe(listener) {
|
|
3825
|
-
listeners.add(listener);
|
|
3826
|
-
return () => {
|
|
3827
|
-
listeners.delete(listener);
|
|
3828
|
-
};
|
|
3829
|
-
},
|
|
3830
|
-
onDispose(fn) {
|
|
3831
|
-
if (disposed) {
|
|
3832
|
-
fn();
|
|
3833
|
-
return;
|
|
3834
|
-
}
|
|
3835
|
-
disposers.add(fn);
|
|
3836
|
-
}
|
|
3837
|
-
};
|
|
3838
|
-
return {
|
|
3839
|
-
context,
|
|
3840
|
-
dispose() {
|
|
3841
|
-
if (disposed) return;
|
|
3842
|
-
disposed = true;
|
|
3843
|
-
for (const fn of disposers) {
|
|
3844
|
-
try {
|
|
3845
|
-
fn();
|
|
3846
|
-
} catch (e) {
|
|
3847
|
-
logger2.error("InteractionContext onDispose callback threw", e);
|
|
3848
|
-
}
|
|
3849
|
-
}
|
|
3850
|
-
disposers.clear();
|
|
3851
|
-
listeners.clear();
|
|
3852
|
-
onceCache.clear();
|
|
3853
|
-
}
|
|
3854
|
-
};
|
|
3855
|
-
}
|
|
3856
|
-
|
|
3857
3793
|
// src/suggestions/generate-suggestions.ts
|
|
3858
3794
|
var HISTORY_WINDOW = 8;
|
|
3859
3795
|
function capabilities(ctx, allAgentInfo) {
|
|
@@ -4826,6 +4762,250 @@ var FoldController = class {
|
|
|
4826
4762
|
}
|
|
4827
4763
|
};
|
|
4828
4764
|
|
|
4765
|
+
// src/state/interaction-context.ts
|
|
4766
|
+
function createInteractionContext(interactionId) {
|
|
4767
|
+
const state = {};
|
|
4768
|
+
const onceCache = /* @__PURE__ */ new Map();
|
|
4769
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4770
|
+
const disposers = /* @__PURE__ */ new Set();
|
|
4771
|
+
let disposed = false;
|
|
4772
|
+
const context = {
|
|
4773
|
+
interactionId,
|
|
4774
|
+
state,
|
|
4775
|
+
once(key, factory) {
|
|
4776
|
+
if (onceCache.has(key)) return onceCache.get(key);
|
|
4777
|
+
if (disposed) {
|
|
4778
|
+
logger2.warn(
|
|
4779
|
+
`InteractionContext.once("${key}") called after dispose; not creating a resource on an ended interaction.`
|
|
4780
|
+
);
|
|
4781
|
+
return null;
|
|
4782
|
+
}
|
|
4783
|
+
const value = factory();
|
|
4784
|
+
onceCache.set(key, value);
|
|
4785
|
+
return value;
|
|
4786
|
+
},
|
|
4787
|
+
notify() {
|
|
4788
|
+
for (const listener of listeners) {
|
|
4789
|
+
try {
|
|
4790
|
+
listener();
|
|
4791
|
+
} catch (e) {
|
|
4792
|
+
logger2.error("InteractionContext subscriber threw during notify()", e);
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
},
|
|
4796
|
+
subscribe(listener) {
|
|
4797
|
+
listeners.add(listener);
|
|
4798
|
+
return () => {
|
|
4799
|
+
listeners.delete(listener);
|
|
4800
|
+
};
|
|
4801
|
+
},
|
|
4802
|
+
onDispose(fn) {
|
|
4803
|
+
if (disposed) {
|
|
4804
|
+
fn();
|
|
4805
|
+
return;
|
|
4806
|
+
}
|
|
4807
|
+
disposers.add(fn);
|
|
4808
|
+
}
|
|
4809
|
+
};
|
|
4810
|
+
return {
|
|
4811
|
+
context,
|
|
4812
|
+
dispose() {
|
|
4813
|
+
if (disposed) return;
|
|
4814
|
+
disposed = true;
|
|
4815
|
+
for (const fn of disposers) {
|
|
4816
|
+
try {
|
|
4817
|
+
fn();
|
|
4818
|
+
} catch (e) {
|
|
4819
|
+
logger2.error("InteractionContext onDispose callback threw", e);
|
|
4820
|
+
}
|
|
4821
|
+
}
|
|
4822
|
+
disposers.clear();
|
|
4823
|
+
listeners.clear();
|
|
4824
|
+
onceCache.clear();
|
|
4825
|
+
}
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4828
|
+
|
|
4829
|
+
// src/components/chat-driver/interaction-controller.ts
|
|
4830
|
+
var InteractionController = class {
|
|
4831
|
+
constructor(deps) {
|
|
4832
|
+
this.deps = deps;
|
|
4833
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
4834
|
+
/**
|
|
4835
|
+
* Live per-interaction contexts (GENC-1390), keyed by interactionId. Created in `request`
|
|
4836
|
+
* alongside the pending entry, disposed + removed in `resolve`. Non-serialisable (sockets,
|
|
4837
|
+
* timers, subscriber sets), so they live here — not on the DOM, not in the persisted store — and
|
|
4838
|
+
* therefore survive the element remounts (layout reflow/drag, transcript re-render) that this
|
|
4839
|
+
* feature exists to make harmless. At most one is live at a time (see the
|
|
4840
|
+
* one-interaction-at-a-time guard in `request`).
|
|
4841
|
+
*/
|
|
4842
|
+
this.contexts = /* @__PURE__ */ new Map();
|
|
4843
|
+
}
|
|
4844
|
+
/** See `ChatDriver.setHostInteractionRequester`. */
|
|
4845
|
+
setHostRequester(fn) {
|
|
4846
|
+
this.hostRequester = fn;
|
|
4847
|
+
}
|
|
4848
|
+
/** See `ChatDriver.requestInteraction`. */
|
|
4849
|
+
request(componentName, data, options) {
|
|
4850
|
+
if (this.hostRequester) {
|
|
4851
|
+
return this.hostRequester(componentName, data, options);
|
|
4852
|
+
}
|
|
4853
|
+
if (this.pending.size > 0) {
|
|
4854
|
+
throw new Error(
|
|
4855
|
+
"requestInteraction: another user interaction is already in flight. Only one interaction may be active at a time \u2014 sequence them in a single tool handler rather than spawning widgets from parallel sub-agents or parallel tool calls."
|
|
4856
|
+
);
|
|
4857
|
+
}
|
|
4858
|
+
const interactionId = crypto.randomUUID();
|
|
4859
|
+
const chatInputDuringExecution = options?.chatInputDuringExecution;
|
|
4860
|
+
const timeoutMs = options?.timeoutMs;
|
|
4861
|
+
const presentation = options?.presentation;
|
|
4862
|
+
return new Promise((resolve, reject) => {
|
|
4863
|
+
this.pending.set(interactionId, {
|
|
4864
|
+
resolve,
|
|
4865
|
+
reject,
|
|
4866
|
+
overrideId: chatInputDuringExecution ? interactionId : void 0,
|
|
4867
|
+
// Resolve the interaction with a timeout result if the user does not
|
|
4868
|
+
// respond in time. `resolve` runs the same teardown a normal resolution
|
|
4869
|
+
// does (marks the widget read-only, reverts any input override) and
|
|
4870
|
+
// clears any still-pending handle.
|
|
4871
|
+
timeoutHandle: timeoutMs != null ? setTimeout(() => {
|
|
4872
|
+
this.resolve(interactionId, {
|
|
4873
|
+
status: "timeout",
|
|
4874
|
+
message: `Interaction timed out after ${timeoutMs}ms.`
|
|
4875
|
+
});
|
|
4876
|
+
}, timeoutMs) : void 0
|
|
4877
|
+
});
|
|
4878
|
+
recordMetaEvent(this.deps.sessionKey, "interaction.requested", {
|
|
4879
|
+
interactionId,
|
|
4880
|
+
component: componentName,
|
|
4881
|
+
agent: this.deps.activeAgentName()
|
|
4882
|
+
});
|
|
4883
|
+
this.deps.activityBus.publish("interaction-requested", void 0);
|
|
4884
|
+
if (chatInputDuringExecution) {
|
|
4885
|
+
this.deps.emit(
|
|
4886
|
+
new CustomEvent("interaction-start", {
|
|
4887
|
+
detail: { interactionId, chatInputDuringExecution }
|
|
4888
|
+
})
|
|
4889
|
+
);
|
|
4890
|
+
}
|
|
4891
|
+
this.contexts.set(interactionId, createInteractionContext(interactionId));
|
|
4892
|
+
this.deps.appendToHistory({
|
|
4893
|
+
role: "assistant",
|
|
4894
|
+
content: "",
|
|
4895
|
+
interaction: {
|
|
4896
|
+
interactionId,
|
|
4897
|
+
componentName,
|
|
4898
|
+
data,
|
|
4899
|
+
...presentation ? { presentation } : {}
|
|
4900
|
+
}
|
|
4901
|
+
});
|
|
4902
|
+
});
|
|
4903
|
+
}
|
|
4904
|
+
/**
|
|
4905
|
+
* See `ChatDriver.resolveInteraction`. Warns and returns when the id is unknown — a widget can
|
|
4906
|
+
* report twice, and the second report has nothing left to settle.
|
|
4907
|
+
*/
|
|
4908
|
+
resolve(interactionId, result) {
|
|
4909
|
+
const interaction = this.pending.get(interactionId);
|
|
4910
|
+
if (!interaction) {
|
|
4911
|
+
logger2.warn(`Interaction with ID ${interactionId} not found.`);
|
|
4912
|
+
return;
|
|
4913
|
+
}
|
|
4914
|
+
if (interaction.timeoutHandle) clearTimeout(interaction.timeoutHandle);
|
|
4915
|
+
this.deps.foldDiagnostics(
|
|
4916
|
+
interactionId,
|
|
4917
|
+
result?.diagnostics
|
|
4918
|
+
);
|
|
4919
|
+
const costUsd = typeof result?.costUsd === "number" && Number.isFinite(result.costUsd) && result.costUsd > 0 ? result.costUsd : void 0;
|
|
4920
|
+
const patched = this.deps.patchInteractionMessage(interactionId, (message) => ({
|
|
4921
|
+
...message,
|
|
4922
|
+
interaction: { ...message.interaction, resolved: result },
|
|
4923
|
+
...costUsd != null ? { externalCostUsd: (message.externalCostUsd ?? 0) + costUsd } : {}
|
|
4924
|
+
}));
|
|
4925
|
+
if (patched) this.deps.notifyHistoryUpdated();
|
|
4926
|
+
if (typeof result?.userMessage === "string" && result.userMessage.trim()) {
|
|
4927
|
+
this.deps.appendToHistory({ role: "synthetic-user", content: result.userMessage });
|
|
4928
|
+
}
|
|
4929
|
+
if (interaction.overrideId) {
|
|
4930
|
+
this.deps.emit(new CustomEvent("interaction-stop", { detail: { interactionId } }));
|
|
4931
|
+
}
|
|
4932
|
+
recordMetaEvent(this.deps.sessionKey, "interaction.resolved", { interactionId });
|
|
4933
|
+
this.deps.activityBus.publish("interaction-resolved", void 0);
|
|
4934
|
+
interaction.resolve(result);
|
|
4935
|
+
this.pending.delete(interactionId);
|
|
4936
|
+
this.contexts.get(interactionId)?.dispose();
|
|
4937
|
+
this.contexts.delete(interactionId);
|
|
4938
|
+
}
|
|
4939
|
+
/**
|
|
4940
|
+
* See `ChatDriver.recordExternalCost`.
|
|
4941
|
+
*
|
|
4942
|
+
* **Why this exists separately from `resolve`.** A widget's spend reaches the session total only
|
|
4943
|
+
* through `InteractionResult.costUsd`, which is read exactly once — `resolve` does its whole job
|
|
4944
|
+
* against a live pending entry and then deletes it. That is correct for the resolution itself,
|
|
4945
|
+
* but it assumes a widget stops costing money when it resolves, and several deliberately do not:
|
|
4946
|
+
* a widget that resolves EARLY so the assistant stops waiting (options shown, a preview
|
|
4947
|
+
* rendered) may stay interactive for minutes afterwards and do more paid work on the user's
|
|
4948
|
+
* behalf. Re-emitting `interaction-completed` does not help — with the interaction gone from the
|
|
4949
|
+
* map the call is a silent no-op — so that spend had nowhere to go and was simply missing from
|
|
4950
|
+
* the figure shown to the user.
|
|
4951
|
+
*
|
|
4952
|
+
* Touches only `externalCostUsd`: the recorded `interaction.resolved` payload is left exactly as
|
|
4953
|
+
* the resolution wrote it, because this is not a second resolution and must not read like one.
|
|
4954
|
+
*/
|
|
4955
|
+
recordExternalCost(interactionId, costUsd) {
|
|
4956
|
+
const drop = (reason) => {
|
|
4957
|
+
recordMetaEvent(this.deps.sessionKey, "interaction.external-cost-dropped", {
|
|
4958
|
+
interactionId,
|
|
4959
|
+
costUsd,
|
|
4960
|
+
reason
|
|
4961
|
+
});
|
|
4962
|
+
return false;
|
|
4963
|
+
};
|
|
4964
|
+
if (!interactionId) return drop("no interactionId");
|
|
4965
|
+
if (typeof costUsd !== "number" || !Number.isFinite(costUsd) || costUsd <= 0) {
|
|
4966
|
+
return drop("not a positive finite amount");
|
|
4967
|
+
}
|
|
4968
|
+
const patched = this.deps.patchInteractionMessage(interactionId, (message) => ({
|
|
4969
|
+
...message,
|
|
4970
|
+
externalCostUsd: (message.externalCostUsd ?? 0) + costUsd
|
|
4971
|
+
}));
|
|
4972
|
+
if (!patched) return drop("interaction message no longer in history");
|
|
4973
|
+
recordMetaEvent(this.deps.sessionKey, "interaction.external-cost", { interactionId, costUsd });
|
|
4974
|
+
this.deps.emit(
|
|
4975
|
+
new CustomEvent("external-cost-recorded", {
|
|
4976
|
+
detail: { interactionId, costUsd }
|
|
4977
|
+
})
|
|
4978
|
+
);
|
|
4979
|
+
return true;
|
|
4980
|
+
}
|
|
4981
|
+
/** See `ChatDriver.getInteractionContext`. */
|
|
4982
|
+
context(interactionId) {
|
|
4983
|
+
return this.contexts.get(interactionId)?.context;
|
|
4984
|
+
}
|
|
4985
|
+
/**
|
|
4986
|
+
* Resolve every in-flight interaction with a `'timeout'` result, closing its
|
|
4987
|
+
* widget read-only. Used when a sub-agent that owns the open widget times out:
|
|
4988
|
+
* the widget is parked in *this* driver's map (interactions resolve on the
|
|
4989
|
+
* host, not the child), so cancelling it here unblocks the sub-agent handler
|
|
4990
|
+
* awaiting it. The "one interaction at a time" invariant means there is at
|
|
4991
|
+
* most one.
|
|
4992
|
+
*/
|
|
4993
|
+
cancelAll(message) {
|
|
4994
|
+
for (const interactionId of [...this.pending.keys()]) {
|
|
4995
|
+
this.resolve(interactionId, { status: "timeout", message });
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
/**
|
|
4999
|
+
* Drop any live interaction contexts on driver teardown. Normally empty (a context is created
|
|
5000
|
+
* and destroyed with its pending interaction, and dispose is not reached mid-interaction);
|
|
5001
|
+
* disposed defensively so a stray context can never leak an open socket/timer.
|
|
5002
|
+
*/
|
|
5003
|
+
disposeContexts() {
|
|
5004
|
+
for (const handle of this.contexts.values()) handle.dispose();
|
|
5005
|
+
this.contexts.clear();
|
|
5006
|
+
}
|
|
5007
|
+
};
|
|
5008
|
+
|
|
4829
5009
|
// src/components/chat-driver/turn-failures.ts
|
|
4830
5010
|
var TURN_FAILURE_EXITS = {
|
|
4831
5011
|
contextGuard: {
|
|
@@ -4907,6 +5087,160 @@ function turnDone(failureReason, budget, providerRefused) {
|
|
|
4907
5087
|
};
|
|
4908
5088
|
}
|
|
4909
5089
|
|
|
5090
|
+
// src/components/chat-driver/turn-recorder.ts
|
|
5091
|
+
var TurnRecorder = class {
|
|
5092
|
+
constructor(sessionKey, maxSnapshots) {
|
|
5093
|
+
this.sessionKey = sessionKey;
|
|
5094
|
+
this.maxSnapshots = maxSnapshots;
|
|
5095
|
+
/**
|
|
5096
|
+
* Ring buffer of per-LLM-call snapshots, capped at `maxSnapshots`; older entries drop off as
|
|
5097
|
+
* new ones arrive.
|
|
5098
|
+
*/
|
|
5099
|
+
this.buffer = [];
|
|
5100
|
+
/** Monotonic counter that survives agent swaps — useful for cross-referencing with history. */
|
|
5101
|
+
this.turnIndex = 0;
|
|
5102
|
+
/**
|
|
5103
|
+
* `turn` + `message` diagnostic entries harvested from an out-of-band source. `event` entries
|
|
5104
|
+
* are NOT kept here — they are folded into the session's meta-event registry so they inherit
|
|
5105
|
+
* fresh host indices, and reach the log through `getMetaEvents`. Append-only, and never cleared:
|
|
5106
|
+
* it lives with the recorder, so a new chat (fresh driver) starts empty while a compaction (same
|
|
5107
|
+
* instance) keeps it. (GENC-1461.)
|
|
5108
|
+
*/
|
|
5109
|
+
this.externalEntries = [];
|
|
5110
|
+
/**
|
|
5111
|
+
* Monotonic count of external-diagnostics batches folded this driver-lifetime. Namespaces each
|
|
5112
|
+
* batch's re-keyed `turn` entries so distinct server generations in one chat session can't
|
|
5113
|
+
* collide on `turnIndex` in the forward-capture persister (which dedups turns by that key).
|
|
5114
|
+
*/
|
|
5115
|
+
this.externalBatches = 0;
|
|
5116
|
+
}
|
|
5117
|
+
/** Point at the incoming agent's `getDebugSnapshot`, or clear it when the agent supplies none. */
|
|
5118
|
+
setSnapshotter(snapshotter) {
|
|
5119
|
+
this.snapshotter = snapshotter;
|
|
5120
|
+
}
|
|
5121
|
+
/**
|
|
5122
|
+
* The per-turn snapshots captured so far — what the LLM saw on each turn: system prompt, tool
|
|
5123
|
+
* surface, and agent-supplied state (e.g. a machine snapshot).
|
|
5124
|
+
*/
|
|
5125
|
+
snapshots() {
|
|
5126
|
+
return this.buffer;
|
|
5127
|
+
}
|
|
5128
|
+
/**
|
|
5129
|
+
* The harvested `turn` + `message` diagnostic entries. Opaque, and chronologically sorted by the
|
|
5130
|
+
* host at assemble time.
|
|
5131
|
+
*/
|
|
5132
|
+
externalDiagnostics() {
|
|
5133
|
+
return this.externalEntries;
|
|
5134
|
+
}
|
|
5135
|
+
/**
|
|
5136
|
+
* Push one snapshot to the ring buffer. Called inside `runToolLoop` just
|
|
5137
|
+
* before each LLM call — that's the latest point where the prompt, tool
|
|
5138
|
+
* surface, and agent state line up with what the model is about to see.
|
|
5139
|
+
*
|
|
5140
|
+
* Returns the pushed object so the caller can back-fill what only the response
|
|
5141
|
+
* knows (`usage`). Mutating it after the fact is safe whether or not the ring
|
|
5142
|
+
* buffer has since evicted it — an evicted snapshot is simply no longer exported.
|
|
5143
|
+
*/
|
|
5144
|
+
record(turn) {
|
|
5145
|
+
let agentSnapshot;
|
|
5146
|
+
if (this.snapshotter) {
|
|
5147
|
+
try {
|
|
5148
|
+
agentSnapshot = this.snapshotter();
|
|
5149
|
+
} catch (e) {
|
|
5150
|
+
agentSnapshot = `<getDebugSnapshot threw: ${e instanceof Error ? e.message : String(e)}>`;
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
const turnIndex = String(this.turnIndex);
|
|
5154
|
+
this.turnIndex += 1;
|
|
5155
|
+
const snapshot = {
|
|
5156
|
+
...turn,
|
|
5157
|
+
turnIndex,
|
|
5158
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5159
|
+
agentSnapshot
|
|
5160
|
+
};
|
|
5161
|
+
this.buffer.push(snapshot);
|
|
5162
|
+
this.trim();
|
|
5163
|
+
return snapshot;
|
|
5164
|
+
}
|
|
5165
|
+
/**
|
|
5166
|
+
* Merge a sub-agent's turn snapshots into this driver's buffer so they surface
|
|
5167
|
+
* as `kind:'turn'` entries in the exported debug log. The child runs as a
|
|
5168
|
+
* separate, discarded driver, so its snapshots would otherwise be lost. Each is
|
|
5169
|
+
* re-labelled under the parent turn that activated the sub-agent: the child's
|
|
5170
|
+
* own (numeric) turns become `"<parentTurn>-1"`, `"-2"`, … (1-based, in order);
|
|
5171
|
+
* any already-forwarded grand-child labels (strings) have their leading segment
|
|
5172
|
+
* remapped the same way, so nesting composes (`"5-2"` → `"5-2-1"`).
|
|
5173
|
+
*
|
|
5174
|
+
* Note: two sub-agents invoked in the *same* parent turn share the prefix, so
|
|
5175
|
+
* their labels can repeat — `agentName` on each snapshot disambiguates them.
|
|
5176
|
+
*/
|
|
5177
|
+
forwardSubAgent(childSnapshots) {
|
|
5178
|
+
if (childSnapshots.length === 0) return;
|
|
5179
|
+
const parentTurn = Math.max(0, this.turnIndex - 1);
|
|
5180
|
+
const ownTurnLabel = /* @__PURE__ */ new Map();
|
|
5181
|
+
let ownPos = 0;
|
|
5182
|
+
for (const snap of childSnapshots) {
|
|
5183
|
+
let turnIndex;
|
|
5184
|
+
if (!snap.turnIndex.includes("-")) {
|
|
5185
|
+
ownPos += 1;
|
|
5186
|
+
turnIndex = `${parentTurn}-${ownPos}`;
|
|
5187
|
+
ownTurnLabel.set(snap.turnIndex, turnIndex);
|
|
5188
|
+
} else {
|
|
5189
|
+
const [lead, ...rest] = snap.turnIndex.split("-");
|
|
5190
|
+
const leadLabel = ownTurnLabel.get(lead) ?? `${parentTurn}-${lead}`;
|
|
5191
|
+
turnIndex = [leadLabel, ...rest].join("-");
|
|
5192
|
+
}
|
|
5193
|
+
this.buffer.push({ ...snap, turnIndex });
|
|
5194
|
+
}
|
|
5195
|
+
this.trim();
|
|
5196
|
+
}
|
|
5197
|
+
/**
|
|
5198
|
+
* Fold a widget's out-of-band diagnostics into this session — typically a server-side
|
|
5199
|
+
* ChatDriver's collated log, arriving on `InteractionResult.diagnostics` — so the host's debug
|
|
5200
|
+
* log and persisted diagnostics stream include them. Independent of the interaction's own
|
|
5201
|
+
* message: these describe the widget's work, not that message. A non-array or empty harvest is
|
|
5202
|
+
* a no-op.
|
|
5203
|
+
*/
|
|
5204
|
+
foldDiagnostics(interactionId, harvested) {
|
|
5205
|
+
if (!Array.isArray(harvested) || !harvested.length) return;
|
|
5206
|
+
const batch = this.externalBatches += 1;
|
|
5207
|
+
const externalEvents = [];
|
|
5208
|
+
let turnCount = 0;
|
|
5209
|
+
let messageCount = 0;
|
|
5210
|
+
for (const raw of harvested) {
|
|
5211
|
+
if (!raw || typeof raw !== "object" || typeof raw.kind !== "string") {
|
|
5212
|
+
continue;
|
|
5213
|
+
}
|
|
5214
|
+
const entry = raw;
|
|
5215
|
+
if (entry.kind === "event") {
|
|
5216
|
+
const { kind: _kind, ...event } = entry;
|
|
5217
|
+
externalEvents.push(event);
|
|
5218
|
+
} else if (entry.kind === "turn") {
|
|
5219
|
+
this.externalEntries.push({
|
|
5220
|
+
...entry,
|
|
5221
|
+
turnIndex: `server-generation.${batch}:${turnCount}`
|
|
5222
|
+
});
|
|
5223
|
+
turnCount += 1;
|
|
5224
|
+
} else if (entry.kind === "message") {
|
|
5225
|
+
messageCount += 1;
|
|
5226
|
+
this.externalEntries.push(entry);
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
if (externalEvents.length) mergeMetaEvents(this.sessionKey, externalEvents);
|
|
5230
|
+
recordMetaEvent(this.sessionKey, "external-diagnostics.folded", {
|
|
5231
|
+
interactionId,
|
|
5232
|
+
batch,
|
|
5233
|
+
counts: { turn: turnCount, event: externalEvents.length, message: messageCount }
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
5236
|
+
/** Hold the ring buffer at its cap, dropping oldest-first. */
|
|
5237
|
+
trim() {
|
|
5238
|
+
while (this.buffer.length > this.maxSnapshots) {
|
|
5239
|
+
this.buffer.shift();
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
};
|
|
5243
|
+
|
|
4910
5244
|
// src/components/chat-driver/chat-driver.ts
|
|
4911
5245
|
var toolResultImages = (result) => {
|
|
4912
5246
|
if (!result || typeof result !== "object") return void 0;
|
|
@@ -4944,17 +5278,6 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
4944
5278
|
this.busy = false;
|
|
4945
5279
|
/** Epoch ms when the current turn loop began — drives the `turn.end` duration. */
|
|
4946
5280
|
this.turnStartedAt = 0;
|
|
4947
|
-
this.pendingInteractions = /* @__PURE__ */ new Map();
|
|
4948
|
-
/**
|
|
4949
|
-
* Live per-interaction contexts (GENC-1390), keyed by interactionId. Created in
|
|
4950
|
-
* `requestInteraction` alongside the pending entry, disposed + removed in
|
|
4951
|
-
* `resolveInteraction`. Non-serialisable (sockets, timers, subscriber sets), so
|
|
4952
|
-
* they live here on the driver — not on the DOM, not in the persisted store — and
|
|
4953
|
-
* therefore survive the element remounts (layout reflow/drag, transcript
|
|
4954
|
-
* re-render) that this feature exists to make harmless. At most one is live at a
|
|
4955
|
-
* time (see the one-interaction-at-a-time guard in `requestInteraction`).
|
|
4956
|
-
*/
|
|
4957
|
-
this.interactionContexts = /* @__PURE__ */ new Map();
|
|
4958
5281
|
/**
|
|
4959
5282
|
* Tool-declared condensation policies, keyed by tool-call id. Populated by
|
|
4960
5283
|
* `condenseWhen` (first-wins per call); read by `applyCondensation` before each
|
|
@@ -5053,31 +5376,6 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5053
5376
|
* doesn't leak forward.
|
|
5054
5377
|
*/
|
|
5055
5378
|
this.agentReleaseRequested = false;
|
|
5056
|
-
/**
|
|
5057
|
-
* Ring buffer of per-LLM-call snapshots. Cap is configurable via
|
|
5058
|
-
* `chatConfig.agent.maxTurnSnapshots`; older entries drop off as new ones
|
|
5059
|
-
* arrive. See {@link TurnSnapshot} for the captured shape.
|
|
5060
|
-
*/
|
|
5061
|
-
this.turnSnapshots = [];
|
|
5062
|
-
/**
|
|
5063
|
-
* `turn` + `message` diagnostic-log entries harvested from an out-of-band source (a resolved
|
|
5064
|
-
* interaction whose widget returned `InteractionResult.diagnostics` — e.g. a server-side
|
|
5065
|
-
* ChatDriver's debug log). `event` entries are NOT kept here — they're folded into the session's
|
|
5066
|
-
* meta-event registry via `mergeMetaEvents` (see `resolveInteraction`) so they inherit fresh host
|
|
5067
|
-
* indices, and reach the log through `getMetaEvents`. Append-only; the host folds these into its
|
|
5068
|
-
* debug log alongside this driver's own timeline. Not cleared here — it lives with the driver
|
|
5069
|
-
* instance, like `turnSnapshots`, so a new chat (fresh driver) starts empty while a compaction
|
|
5070
|
-
* (same instance) keeps it. (GENC-1461.)
|
|
5071
|
-
*/
|
|
5072
|
-
this.externalDiagnostics = [];
|
|
5073
|
-
/**
|
|
5074
|
-
* Monotonic count of external-diagnostics batches harvested this driver-lifetime. Namespaces each
|
|
5075
|
-
* batch's re-keyed `turn` entries so distinct server generations in one chat session can't collide
|
|
5076
|
-
* on `turnIndex` in the forward-capture persister (which dedups turns by that key). (GENC-1461.)
|
|
5077
|
-
*/
|
|
5078
|
-
this.externalDiagnosticsBatches = 0;
|
|
5079
|
-
/** Monotonic counter that survives agent swaps — useful for cross-referencing with history. */
|
|
5080
|
-
this.globalTurnIndex = 0;
|
|
5081
5379
|
/**
|
|
5082
5380
|
* Aborted by `dispose()` on driver teardown (e.g. an agent-config swap).
|
|
5083
5381
|
* Threaded into every provider call as `ChatRequestOptions.signal`, so a
|
|
@@ -5187,7 +5485,17 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5187
5485
|
this.systemPrompt = systemPrompt;
|
|
5188
5486
|
this.primerHistory = primerHistory;
|
|
5189
5487
|
this.maxFoldOperations = maxFoldOperations;
|
|
5190
|
-
this.
|
|
5488
|
+
this.turnRecorder = new TurnRecorder(sessionKey, maxTurnSnapshots);
|
|
5489
|
+
this.interactions = new InteractionController({
|
|
5490
|
+
sessionKey,
|
|
5491
|
+
activityBus,
|
|
5492
|
+
activeAgentName: () => this.activeAgentName,
|
|
5493
|
+
appendToHistory: (message) => this.appendToHistory(message),
|
|
5494
|
+
patchInteractionMessage: (interactionId, patch) => this.patchInteractionMessage(interactionId, patch),
|
|
5495
|
+
notifyHistoryUpdated: () => this.notifyHistoryUpdated(),
|
|
5496
|
+
emit: (event) => this.dispatchEvent(event),
|
|
5497
|
+
foldDiagnostics: (interactionId, harvested) => this.turnRecorder.foldDiagnostics(interactionId, harvested)
|
|
5498
|
+
});
|
|
5191
5499
|
if (isObservableAIProviderRegistry(this.providerRegistry)) {
|
|
5192
5500
|
this.unsubscribeRegistry = this.providerRegistry.subscribe(() => {
|
|
5193
5501
|
this.resolvedProviderCache.clear();
|
|
@@ -5205,7 +5513,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5205
5513
|
* on to completion or the transport timeout. Called by the host on driver
|
|
5206
5514
|
* swap and by `OrchestratingDriver.dispose()`. Idempotent.
|
|
5207
5515
|
*
|
|
5208
|
-
* Deliberately does NOT settle
|
|
5516
|
+
* Deliberately does NOT settle pending interactions here: dispose is never
|
|
5209
5517
|
* reached while one is pending. The only host-level dispose is the agent-swap
|
|
5210
5518
|
* rebuild, which is guarded by `isBusy()` — a pending interaction keeps the
|
|
5211
5519
|
* tool loop suspended (`busy === true`), so the swap is deferred until the
|
|
@@ -5216,8 +5524,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5216
5524
|
*/
|
|
5217
5525
|
async dispose() {
|
|
5218
5526
|
this.lifecycleController.abort(new DOMException("AI assistant driver disposed", "AbortError"));
|
|
5219
|
-
|
|
5220
|
-
this.interactionContexts.clear();
|
|
5527
|
+
this.interactions.disposeContexts();
|
|
5221
5528
|
this.unsubscribeRegistry?.();
|
|
5222
5529
|
this.unsubscribeRegistry = void 0;
|
|
5223
5530
|
await this.agentSession.dispose();
|
|
@@ -5474,7 +5781,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5474
5781
|
this.activeAgentName = config.name;
|
|
5475
5782
|
this.displayName = config.displayName;
|
|
5476
5783
|
this.activeAgentLabel = typeof config.displayName === "string" ? config.displayName : config.name;
|
|
5477
|
-
this.
|
|
5784
|
+
this.turnRecorder.setSnapshotter(config.getDebugSnapshot);
|
|
5478
5785
|
this.subAgentsMap = new Map((config.subAgents ?? []).map((s) => [s.name, s]));
|
|
5479
5786
|
this.activeProviderInput = config.provider;
|
|
5480
5787
|
this.activeTemperatureInput = config.temperature;
|
|
@@ -5671,93 +5978,21 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5671
5978
|
* log exporter to show what the LLM saw on each turn — system prompt, tool
|
|
5672
5979
|
* surface, and agent-supplied state (e.g. a machine snapshot).
|
|
5673
5980
|
*
|
|
5674
|
-
* Ring-buffered at `
|
|
5981
|
+
* Ring-buffered at `maxTurnSnapshots`; older entries are dropped.
|
|
5675
5982
|
*/
|
|
5676
5983
|
getTurnSnapshots() {
|
|
5677
|
-
return this.
|
|
5984
|
+
return this.turnRecorder.snapshots();
|
|
5678
5985
|
}
|
|
5679
5986
|
/**
|
|
5680
5987
|
* The `turn` + `message` diagnostic-log entries harvested from resolved interactions'
|
|
5681
5988
|
* `InteractionResult.diagnostics` (e.g. a server-side ChatDriver's collated debug log). The host
|
|
5682
5989
|
* concatenates these into its own debug log so `downloadDebugLog`/the persisted diagnostics stream
|
|
5683
5990
|
* include external-engine logs; `event` entries are excluded here because they're merged into the
|
|
5684
|
-
* session's meta-event registry instead
|
|
5685
|
-
*
|
|
5991
|
+
* session's meta-event registry instead and reach the log via `getMetaEvents`. Opaque,
|
|
5992
|
+
* chronologically sorted by the host at assemble time. (GENC-1461.)
|
|
5686
5993
|
*/
|
|
5687
5994
|
getExternalDiagnostics() {
|
|
5688
|
-
return this.externalDiagnostics;
|
|
5689
|
-
}
|
|
5690
|
-
/**
|
|
5691
|
-
* Merge a sub-agent's turn snapshots into this driver's buffer so they surface
|
|
5692
|
-
* as `kind:'turn'` entries in the exported debug log. The child runs as a
|
|
5693
|
-
* separate, discarded driver, so its snapshots would otherwise be lost. Each is
|
|
5694
|
-
* re-labelled under the parent turn that activated the sub-agent: the child's
|
|
5695
|
-
* own (numeric) turns become `"<parentTurn>-1"`, `"-2"`, … (1-based, in order);
|
|
5696
|
-
* any already-forwarded grand-child labels (strings) have their leading segment
|
|
5697
|
-
* remapped the same way, so nesting composes (`"5-2"` → `"5-2-1"`).
|
|
5698
|
-
*
|
|
5699
|
-
* Note: two sub-agents invoked in the *same* parent turn share the prefix, so
|
|
5700
|
-
* their labels can repeat — `agentName` on each snapshot disambiguates them.
|
|
5701
|
-
*/
|
|
5702
|
-
forwardSubAgentSnapshots(childSnapshots) {
|
|
5703
|
-
if (childSnapshots.length === 0) return;
|
|
5704
|
-
const parentTurn = Math.max(0, this.globalTurnIndex - 1);
|
|
5705
|
-
const ownTurnLabel = /* @__PURE__ */ new Map();
|
|
5706
|
-
let ownPos = 0;
|
|
5707
|
-
for (const snap of childSnapshots) {
|
|
5708
|
-
let turnIndex;
|
|
5709
|
-
if (!snap.turnIndex.includes("-")) {
|
|
5710
|
-
ownPos += 1;
|
|
5711
|
-
turnIndex = `${parentTurn}-${ownPos}`;
|
|
5712
|
-
ownTurnLabel.set(snap.turnIndex, turnIndex);
|
|
5713
|
-
} else {
|
|
5714
|
-
const [lead, ...rest] = snap.turnIndex.split("-");
|
|
5715
|
-
const leadLabel = ownTurnLabel.get(lead) ?? `${parentTurn}-${lead}`;
|
|
5716
|
-
turnIndex = [leadLabel, ...rest].join("-");
|
|
5717
|
-
}
|
|
5718
|
-
this.turnSnapshots.push({ ...snap, turnIndex });
|
|
5719
|
-
}
|
|
5720
|
-
while (this.turnSnapshots.length > this.maxTurnSnapshots) {
|
|
5721
|
-
this.turnSnapshots.shift();
|
|
5722
|
-
}
|
|
5723
|
-
}
|
|
5724
|
-
/**
|
|
5725
|
-
* Push one snapshot to the ring buffer. Called inside `runToolLoop` just
|
|
5726
|
-
* before each LLM call — that's the latest point where the prompt, tool
|
|
5727
|
-
* surface, and agent state line up with what the model is about to see.
|
|
5728
|
-
*
|
|
5729
|
-
* Returns the pushed object so the caller can back-fill what only the response
|
|
5730
|
-
* knows (`usage`). Mutating it after the fact is safe whether or not the ring
|
|
5731
|
-
* buffer has since evicted it — an evicted snapshot is simply no longer exported.
|
|
5732
|
-
*/
|
|
5733
|
-
recordTurnSnapshot(resolvedSystemPrompt, temperature, toolChoice, tailContext) {
|
|
5734
|
-
let agentSnapshot;
|
|
5735
|
-
if (this.debugSnapshotter) {
|
|
5736
|
-
try {
|
|
5737
|
-
agentSnapshot = this.debugSnapshotter();
|
|
5738
|
-
} catch (e) {
|
|
5739
|
-
agentSnapshot = `<getDebugSnapshot threw: ${e instanceof Error ? e.message : String(e)}>`;
|
|
5740
|
-
}
|
|
5741
|
-
}
|
|
5742
|
-
const turnIndex = String(this.globalTurnIndex);
|
|
5743
|
-
this.globalTurnIndex += 1;
|
|
5744
|
-
const snapshot = {
|
|
5745
|
-
turnIndex,
|
|
5746
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5747
|
-
agentName: this.activeAgentName,
|
|
5748
|
-
agentLabel: this.activeAgentLabel,
|
|
5749
|
-
systemPrompt: resolvedSystemPrompt,
|
|
5750
|
-
tailContext,
|
|
5751
|
-
toolNames: this.toolDefinitions.map((t) => t.name),
|
|
5752
|
-
temperature,
|
|
5753
|
-
toolChoice,
|
|
5754
|
-
agentSnapshot
|
|
5755
|
-
};
|
|
5756
|
-
this.turnSnapshots.push(snapshot);
|
|
5757
|
-
if (this.turnSnapshots.length > this.maxTurnSnapshots) {
|
|
5758
|
-
this.turnSnapshots.shift();
|
|
5759
|
-
}
|
|
5760
|
-
return snapshot;
|
|
5995
|
+
return this.turnRecorder.externalDiagnostics();
|
|
5761
5996
|
}
|
|
5762
5997
|
/**
|
|
5763
5998
|
* Set the mid-loop context guard. Its margin is deliberately far smaller than
|
|
@@ -5802,6 +6037,10 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5802
6037
|
);
|
|
5803
6038
|
return { estimated, threshold };
|
|
5804
6039
|
}
|
|
6040
|
+
/**
|
|
6041
|
+
* Optional transform applied to conversation history immediately before each LLM request.
|
|
6042
|
+
* Cleared when `undefined`. Does not alter stored history.
|
|
6043
|
+
*/
|
|
5805
6044
|
setProviderHistoryTransform(transform) {
|
|
5806
6045
|
this.providerHistoryTransform = transform;
|
|
5807
6046
|
}
|
|
@@ -5955,7 +6194,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5955
6194
|
* naturally: a grandchild → child → root.
|
|
5956
6195
|
*/
|
|
5957
6196
|
setHostInteractionRequester(fn) {
|
|
5958
|
-
this.
|
|
6197
|
+
this.interactions.setHostRequester(fn);
|
|
5959
6198
|
}
|
|
5960
6199
|
/**
|
|
5961
6200
|
* Request a custom UI interaction. Emits a new message with the interaction.
|
|
@@ -5977,76 +6216,15 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5977
6216
|
* bubble and/or shows the "Assistant" label.
|
|
5978
6217
|
*/
|
|
5979
6218
|
async requestInteraction(componentName, data, options) {
|
|
5980
|
-
|
|
5981
|
-
return this.hostInteractionRequester(componentName, data, options);
|
|
5982
|
-
}
|
|
5983
|
-
if (this.pendingInteractions.size > 0) {
|
|
5984
|
-
throw new Error(
|
|
5985
|
-
"requestInteraction: another user interaction is already in flight. Only one interaction may be active at a time \u2014 sequence them in a single tool handler rather than spawning widgets from parallel sub-agents or parallel tool calls."
|
|
5986
|
-
);
|
|
5987
|
-
}
|
|
5988
|
-
const interactionId = crypto.randomUUID();
|
|
5989
|
-
const chatInputDuringExecution = options?.chatInputDuringExecution;
|
|
5990
|
-
const timeoutMs = options?.timeoutMs;
|
|
5991
|
-
const presentation = options?.presentation;
|
|
5992
|
-
return new Promise((resolve, reject) => {
|
|
5993
|
-
this.pendingInteractions.set(interactionId, {
|
|
5994
|
-
resolve,
|
|
5995
|
-
reject,
|
|
5996
|
-
overrideId: chatInputDuringExecution ? interactionId : void 0,
|
|
5997
|
-
// Resolve the interaction with a timeout result if the user does not
|
|
5998
|
-
// respond in time. `resolveInteraction` runs the same teardown a normal
|
|
5999
|
-
// resolution does (marks the widget read-only, reverts any input
|
|
6000
|
-
// override) and clears any still-pending handle.
|
|
6001
|
-
timeoutHandle: timeoutMs != null ? setTimeout(() => {
|
|
6002
|
-
this.resolveInteraction(interactionId, {
|
|
6003
|
-
status: "timeout",
|
|
6004
|
-
message: `Interaction timed out after ${timeoutMs}ms.`
|
|
6005
|
-
});
|
|
6006
|
-
}, timeoutMs) : void 0
|
|
6007
|
-
});
|
|
6008
|
-
recordMetaEvent(this.sessionKey, "interaction.requested", {
|
|
6009
|
-
interactionId,
|
|
6010
|
-
component: componentName,
|
|
6011
|
-
agent: this.activeAgentName
|
|
6012
|
-
});
|
|
6013
|
-
this.activityBus.publish("interaction-requested", void 0);
|
|
6014
|
-
if (chatInputDuringExecution) {
|
|
6015
|
-
this.dispatchEvent(
|
|
6016
|
-
new CustomEvent("interaction-start", {
|
|
6017
|
-
detail: { interactionId, chatInputDuringExecution }
|
|
6018
|
-
})
|
|
6019
|
-
);
|
|
6020
|
-
}
|
|
6021
|
-
this.interactionContexts.set(interactionId, createInteractionContext(interactionId));
|
|
6022
|
-
this.appendToHistory({
|
|
6023
|
-
role: "assistant",
|
|
6024
|
-
content: "",
|
|
6025
|
-
interaction: {
|
|
6026
|
-
interactionId,
|
|
6027
|
-
componentName,
|
|
6028
|
-
data,
|
|
6029
|
-
...presentation ? { presentation } : {}
|
|
6030
|
-
}
|
|
6031
|
-
});
|
|
6032
|
-
});
|
|
6219
|
+
return this.interactions.request(componentName, data, options);
|
|
6033
6220
|
}
|
|
6034
6221
|
/**
|
|
6035
|
-
* Add external (non-LLM) spend to an interaction's message AFTER it has resolved
|
|
6222
|
+
* Add external (non-LLM) spend to an interaction's message AFTER it has resolved — for a widget
|
|
6223
|
+
* that resolves early so the assistant stops waiting, then keeps doing paid work on the user's
|
|
6224
|
+
* behalf. `InteractionController.recordExternalCost` explains why the resolution path cannot
|
|
6225
|
+
* carry this.
|
|
6036
6226
|
*
|
|
6037
|
-
*
|
|
6038
|
-
* total only through `InteractionResult.costUsd`, which is read exactly once — `resolveInteraction`
|
|
6039
|
-
* does its whole job inside `if (interaction)` and then deletes the entry from
|
|
6040
|
-
* `pendingInteractions`. That is correct for the resolution itself, but it assumes a widget stops
|
|
6041
|
-
* costing money when it resolves, and several deliberately do not: a widget that resolves EARLY so
|
|
6042
|
-
* the assistant stops waiting (options shown, a preview rendered) may stay interactive for minutes
|
|
6043
|
-
* afterwards and do more paid work on the user's behalf. Re-emitting `interaction-completed` does
|
|
6044
|
-
* not help — with the interaction gone from the map the call is a silent no-op — so that spend had
|
|
6045
|
-
* nowhere to go and was simply missing from the figure shown to the user.
|
|
6046
|
-
*
|
|
6047
|
-
* Additive by design: the caller reports a DELTA, not a running total, so repeated calls sum. It
|
|
6048
|
-
* touches only `externalCostUsd`; the recorded `interaction.resolved` payload is left exactly as
|
|
6049
|
-
* the resolution wrote it, because this is not a second resolution and must not read like one.
|
|
6227
|
+
* Additive by design: the caller reports a DELTA, not a running total, so repeated calls sum.
|
|
6050
6228
|
*
|
|
6051
6229
|
* Deliberately does NOT require the interaction to be pending, does not resurrect it, and has no
|
|
6052
6230
|
* loading/turn side effects — this is bookkeeping about a turn that already happened.
|
|
@@ -6056,106 +6234,19 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6056
6234
|
* @returns `true` when the spend was recorded; `false` when it was not — either the value was
|
|
6057
6235
|
* not a positive finite amount, or the message is no longer in history (trimmed or condensed
|
|
6058
6236
|
* away). A real outcome a caller may want to log rather than a failure to throw on; every
|
|
6059
|
-
* `false` also records an `interaction.external-cost-dropped` meta event
|
|
6060
|
-
*
|
|
6237
|
+
* `false` also records an `interaction.external-cost-dropped` meta event, so money can never
|
|
6238
|
+
* disappear without a trace.
|
|
6061
6239
|
* @beta
|
|
6062
6240
|
*/
|
|
6063
6241
|
recordExternalCost(interactionId, costUsd) {
|
|
6064
|
-
|
|
6065
|
-
recordMetaEvent(this.sessionKey, "interaction.external-cost-dropped", {
|
|
6066
|
-
interactionId,
|
|
6067
|
-
costUsd,
|
|
6068
|
-
reason
|
|
6069
|
-
});
|
|
6070
|
-
return false;
|
|
6071
|
-
};
|
|
6072
|
-
if (!interactionId) return drop("no interactionId");
|
|
6073
|
-
if (typeof costUsd !== "number" || !Number.isFinite(costUsd) || costUsd <= 0) {
|
|
6074
|
-
return drop("not a positive finite amount");
|
|
6075
|
-
}
|
|
6076
|
-
const idx = this.history.findIndex((m) => m.interaction?.interactionId === interactionId);
|
|
6077
|
-
if (idx === -1) return drop("interaction message no longer in history");
|
|
6078
|
-
this.history[idx] = {
|
|
6079
|
-
...this.history[idx],
|
|
6080
|
-
externalCostUsd: (this.history[idx].externalCostUsd ?? 0) + costUsd
|
|
6081
|
-
};
|
|
6082
|
-
recordMetaEvent(this.sessionKey, "interaction.external-cost", { interactionId, costUsd });
|
|
6083
|
-
this.dispatchEvent(
|
|
6084
|
-
new CustomEvent("external-cost-recorded", {
|
|
6085
|
-
detail: { interactionId, costUsd }
|
|
6086
|
-
})
|
|
6087
|
-
);
|
|
6088
|
-
return true;
|
|
6242
|
+
return this.interactions.recordExternalCost(interactionId, costUsd);
|
|
6089
6243
|
}
|
|
6090
6244
|
/**
|
|
6091
6245
|
* Resolve a pending interaction. The wrapper component calls this on completion.
|
|
6092
6246
|
* Marks the interaction message as resolved so it renders read-only on re-render.
|
|
6093
6247
|
*/
|
|
6094
6248
|
resolveInteraction(interactionId, result) {
|
|
6095
|
-
|
|
6096
|
-
if (interaction) {
|
|
6097
|
-
if (interaction.timeoutHandle) clearTimeout(interaction.timeoutHandle);
|
|
6098
|
-
const harvestedDiagnostics = result?.diagnostics;
|
|
6099
|
-
if (Array.isArray(harvestedDiagnostics) && harvestedDiagnostics.length) {
|
|
6100
|
-
const batch = this.externalDiagnosticsBatches += 1;
|
|
6101
|
-
const externalEvents = [];
|
|
6102
|
-
let turnCount = 0;
|
|
6103
|
-
let messageCount = 0;
|
|
6104
|
-
for (const raw of harvestedDiagnostics) {
|
|
6105
|
-
if (!raw || typeof raw !== "object" || typeof raw.kind !== "string") {
|
|
6106
|
-
continue;
|
|
6107
|
-
}
|
|
6108
|
-
const entry = raw;
|
|
6109
|
-
if (entry.kind === "event") {
|
|
6110
|
-
const { kind: _kind, ...event } = entry;
|
|
6111
|
-
externalEvents.push(event);
|
|
6112
|
-
} else if (entry.kind === "turn") {
|
|
6113
|
-
this.externalDiagnostics.push({
|
|
6114
|
-
...entry,
|
|
6115
|
-
turnIndex: `server-generation.${batch}:${turnCount}`
|
|
6116
|
-
});
|
|
6117
|
-
turnCount += 1;
|
|
6118
|
-
} else if (entry.kind === "message") {
|
|
6119
|
-
messageCount += 1;
|
|
6120
|
-
this.externalDiagnostics.push(entry);
|
|
6121
|
-
}
|
|
6122
|
-
}
|
|
6123
|
-
if (externalEvents.length) mergeMetaEvents(this.sessionKey, externalEvents);
|
|
6124
|
-
recordMetaEvent(this.sessionKey, "external-diagnostics.folded", {
|
|
6125
|
-
interactionId,
|
|
6126
|
-
batch,
|
|
6127
|
-
counts: { turn: turnCount, event: externalEvents.length, message: messageCount }
|
|
6128
|
-
});
|
|
6129
|
-
}
|
|
6130
|
-
const idx = this.history.findIndex((m) => m.interaction?.interactionId === interactionId);
|
|
6131
|
-
if (idx !== -1) {
|
|
6132
|
-
const costUsd = typeof result?.costUsd === "number" && Number.isFinite(result.costUsd) && result.costUsd > 0 ? result.costUsd : void 0;
|
|
6133
|
-
this.history[idx] = {
|
|
6134
|
-
...this.history[idx],
|
|
6135
|
-
interaction: { ...this.history[idx].interaction, resolved: result },
|
|
6136
|
-
...costUsd != null ? { externalCostUsd: (this.history[idx].externalCostUsd ?? 0) + costUsd } : {}
|
|
6137
|
-
};
|
|
6138
|
-
this.dispatchEvent(
|
|
6139
|
-
new CustomEvent("history-updated", {
|
|
6140
|
-
detail: this.history
|
|
6141
|
-
})
|
|
6142
|
-
);
|
|
6143
|
-
}
|
|
6144
|
-
if (typeof result?.userMessage === "string" && result.userMessage.trim()) {
|
|
6145
|
-
this.appendToHistory({ role: "synthetic-user", content: result.userMessage });
|
|
6146
|
-
}
|
|
6147
|
-
if (interaction.overrideId) {
|
|
6148
|
-
this.dispatchEvent(new CustomEvent("interaction-stop", { detail: { interactionId } }));
|
|
6149
|
-
}
|
|
6150
|
-
recordMetaEvent(this.sessionKey, "interaction.resolved", { interactionId });
|
|
6151
|
-
this.activityBus.publish("interaction-resolved", void 0);
|
|
6152
|
-
interaction.resolve(result);
|
|
6153
|
-
this.pendingInteractions.delete(interactionId);
|
|
6154
|
-
this.interactionContexts.get(interactionId)?.dispose();
|
|
6155
|
-
this.interactionContexts.delete(interactionId);
|
|
6156
|
-
} else {
|
|
6157
|
-
logger2.warn(`Interaction with ID ${interactionId} not found.`);
|
|
6158
|
-
}
|
|
6249
|
+
this.interactions.resolve(interactionId, result);
|
|
6159
6250
|
}
|
|
6160
6251
|
/**
|
|
6161
6252
|
* The live {@link InteractionContext} for a pending interaction (GENC-1390), or
|
|
@@ -6163,23 +6254,17 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6163
6254
|
* host) to build the widget's pending `interaction` descriptor.
|
|
6164
6255
|
*/
|
|
6165
6256
|
getInteractionContext(interactionId) {
|
|
6166
|
-
return this.
|
|
6257
|
+
return this.interactions.context(interactionId);
|
|
6167
6258
|
}
|
|
6168
6259
|
/**
|
|
6169
|
-
*
|
|
6170
|
-
*
|
|
6171
|
-
* the widget is parked in *this* driver's map (interactions resolve on the
|
|
6172
|
-
* host, not the child), so cancelling it here unblocks the sub-agent handler
|
|
6173
|
-
* awaiting it. The "one interaction at a time" invariant means there is at
|
|
6174
|
-
* most one.
|
|
6260
|
+
* Replace the history message owning `interactionId`. `false` when it is no longer in history.
|
|
6261
|
+
* Does not announce the change — the interaction paths differ on whether they should.
|
|
6175
6262
|
*/
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
});
|
|
6182
|
-
}
|
|
6263
|
+
patchInteractionMessage(interactionId, patch) {
|
|
6264
|
+
const idx = this.history.findIndex((m) => m.interaction?.interactionId === interactionId);
|
|
6265
|
+
if (idx === -1) return false;
|
|
6266
|
+
this.history[idx] = patch(this.history[idx]);
|
|
6267
|
+
return true;
|
|
6183
6268
|
}
|
|
6184
6269
|
/**
|
|
6185
6270
|
* Seeds the driver with a prior conversation history so a new component instance
|
|
@@ -6187,11 +6272,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6187
6272
|
*/
|
|
6188
6273
|
loadHistory(messages) {
|
|
6189
6274
|
this.history = [...messages];
|
|
6190
|
-
this.
|
|
6191
|
-
new CustomEvent("history-updated", {
|
|
6192
|
-
detail: this.history
|
|
6193
|
-
})
|
|
6194
|
-
);
|
|
6275
|
+
this.notifyHistoryUpdated();
|
|
6195
6276
|
}
|
|
6196
6277
|
async sendMessage(userInput, attachments) {
|
|
6197
6278
|
if (this.busy || !userInput.trim() && !attachments?.length) return { reason: "done" };
|
|
@@ -6411,7 +6492,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6411
6492
|
if (winner === "timeout") {
|
|
6412
6493
|
timedOut = true;
|
|
6413
6494
|
child.dispose();
|
|
6414
|
-
this.
|
|
6495
|
+
this.interactions.cancelAll(`Sub-agent "${name}" timed out after ${timeoutMs}ms.`);
|
|
6415
6496
|
await runPromise.catch(() => void 0);
|
|
6416
6497
|
}
|
|
6417
6498
|
} finally {
|
|
@@ -6424,7 +6505,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6424
6505
|
child.removeEventListener("provider-changed", forwardProviderChanged);
|
|
6425
6506
|
child.dispose();
|
|
6426
6507
|
this.dispatchEvent(new CustomEvent("sub-agent-stop", { detail: lifecycleDetail }));
|
|
6427
|
-
this.
|
|
6508
|
+
this.turnRecorder.forwardSubAgent(child.getTurnSnapshots());
|
|
6428
6509
|
const parentName = this.activeAgentName ?? "?";
|
|
6429
6510
|
const harvested = getMetaEvents(childSessionKey).filter((e) => HARVESTED_SUBAGENT_EVENTS.has(e.type)).map((e) => {
|
|
6430
6511
|
const existing = e.detail?.subAgent;
|
|
@@ -6512,7 +6593,10 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6512
6593
|
// ---------------------------------------------------------------------------
|
|
6513
6594
|
// Tool loop
|
|
6514
6595
|
// ---------------------------------------------------------------------------
|
|
6515
|
-
//
|
|
6596
|
+
// Over both ceilings deliberately: the per-iteration phases share a dozen accumulators, so
|
|
6597
|
+
// splitting threads that state through signatures rather than removing it. `turn-setup.ts`
|
|
6598
|
+
// (docs/code-health.md §11.3) is the intended carve-up.
|
|
6599
|
+
// oxlint-disable-next-line complexity, max-lines-per-function
|
|
6516
6600
|
async runToolLoop(userInput, attachments, transientPrimer) {
|
|
6517
6601
|
if (!this.systemPrompt) {
|
|
6518
6602
|
logger2.warn(
|
|
@@ -6668,12 +6752,15 @@ ${EMPTY_RESPONSE_NUDGE}` : "";
|
|
|
6668
6752
|
${tailBody}
|
|
6669
6753
|
</system-reminder>` : void 0;
|
|
6670
6754
|
const effectiveToolChoice = resolvedToolChoice ?? (this.isSubAgent ? "required" : void 0);
|
|
6671
|
-
const turnSnapshot = this.
|
|
6755
|
+
const turnSnapshot = this.turnRecorder.record({
|
|
6756
|
+
agentName: this.activeAgentName,
|
|
6757
|
+
agentLabel: this.activeAgentLabel,
|
|
6672
6758
|
systemPrompt,
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6759
|
+
tailContext,
|
|
6760
|
+
toolNames: this.toolDefinitions.map((t) => t.name),
|
|
6761
|
+
temperature: resolvedTemperature,
|
|
6762
|
+
toolChoice: effectiveToolChoice
|
|
6763
|
+
});
|
|
6677
6764
|
const userInputForCall = currentInput;
|
|
6678
6765
|
const attachmentsForCall = currentAttachments;
|
|
6679
6766
|
currentInput = "";
|
|
@@ -7175,11 +7262,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
|
|
|
7175
7262
|
};
|
|
7176
7263
|
});
|
|
7177
7264
|
this.history[tcMsgIdx] = { ...tcMsg, toolCalls: annotatedCalls };
|
|
7178
|
-
this.
|
|
7179
|
-
new CustomEvent("history-updated", {
|
|
7180
|
-
detail: this.history
|
|
7181
|
-
})
|
|
7182
|
-
);
|
|
7265
|
+
this.notifyHistoryUpdated();
|
|
7183
7266
|
}
|
|
7184
7267
|
if (hitUnknownToolLimit) {
|
|
7185
7268
|
logger2.error(
|
|
@@ -7262,6 +7345,10 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
|
|
|
7262
7345
|
} : {}
|
|
7263
7346
|
};
|
|
7264
7347
|
this.history = [...this.history, tagged];
|
|
7348
|
+
this.notifyHistoryUpdated();
|
|
7349
|
+
}
|
|
7350
|
+
/** Announce the current history to the host. */
|
|
7351
|
+
notifyHistoryUpdated() {
|
|
7265
7352
|
this.dispatchEvent(
|
|
7266
7353
|
new CustomEvent("history-updated", {
|
|
7267
7354
|
detail: this.history
|