@genesislcap/ai-assistant 15.26.0 → 15.27.0-agent-log-submit.3
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 +1014 -110
- package/dist/ai-assistant.d.ts +236 -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 +1171 -248
- 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.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.styles.d.ts.map +1 -1
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts +28 -0
- package/dist/dts/components/settings-modal/settings-modal.template.d.ts.map +1 -1
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/dts/main/main.d.ts +180 -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-debug-log-note.d.ts +25 -0
- package/dist/dts/utils/resolve-debug-log-note.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.js +1 -0
- package/dist/esm/components/settings-modal/settings-modal.styles.js +85 -5
- package/dist/esm/components/settings-modal/settings-modal.template.js +220 -25
- package/dist/esm/main/main.js +510 -23
- 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-debug-log-note.js +37 -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.mjs
CHANGED
|
@@ -3719,70 +3719,6 @@ function clearSession(key) {
|
|
|
3719
3719
|
// src/utils/logger.ts
|
|
3720
3720
|
var logger2 = createLogger("ai-assistant");
|
|
3721
3721
|
|
|
3722
|
-
// src/state/interaction-context.ts
|
|
3723
|
-
function createInteractionContext(interactionId) {
|
|
3724
|
-
const state = {};
|
|
3725
|
-
const onceCache = /* @__PURE__ */ new Map();
|
|
3726
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
3727
|
-
const disposers = /* @__PURE__ */ new Set();
|
|
3728
|
-
let disposed = false;
|
|
3729
|
-
const context = {
|
|
3730
|
-
interactionId,
|
|
3731
|
-
state,
|
|
3732
|
-
once(key, factory) {
|
|
3733
|
-
if (onceCache.has(key)) return onceCache.get(key);
|
|
3734
|
-
if (disposed) {
|
|
3735
|
-
logger2.warn(
|
|
3736
|
-
`InteractionContext.once("${key}") called after dispose; not creating a resource on an ended interaction.`
|
|
3737
|
-
);
|
|
3738
|
-
return null;
|
|
3739
|
-
}
|
|
3740
|
-
const value = factory();
|
|
3741
|
-
onceCache.set(key, value);
|
|
3742
|
-
return value;
|
|
3743
|
-
},
|
|
3744
|
-
notify() {
|
|
3745
|
-
for (const listener of listeners) {
|
|
3746
|
-
try {
|
|
3747
|
-
listener();
|
|
3748
|
-
} catch (e) {
|
|
3749
|
-
logger2.error("InteractionContext subscriber threw during notify()", e);
|
|
3750
|
-
}
|
|
3751
|
-
}
|
|
3752
|
-
},
|
|
3753
|
-
subscribe(listener) {
|
|
3754
|
-
listeners.add(listener);
|
|
3755
|
-
return () => {
|
|
3756
|
-
listeners.delete(listener);
|
|
3757
|
-
};
|
|
3758
|
-
},
|
|
3759
|
-
onDispose(fn) {
|
|
3760
|
-
if (disposed) {
|
|
3761
|
-
fn();
|
|
3762
|
-
return;
|
|
3763
|
-
}
|
|
3764
|
-
disposers.add(fn);
|
|
3765
|
-
}
|
|
3766
|
-
};
|
|
3767
|
-
return {
|
|
3768
|
-
context,
|
|
3769
|
-
dispose() {
|
|
3770
|
-
if (disposed) return;
|
|
3771
|
-
disposed = true;
|
|
3772
|
-
for (const fn of disposers) {
|
|
3773
|
-
try {
|
|
3774
|
-
fn();
|
|
3775
|
-
} catch (e) {
|
|
3776
|
-
logger2.error("InteractionContext onDispose callback threw", e);
|
|
3777
|
-
}
|
|
3778
|
-
}
|
|
3779
|
-
disposers.clear();
|
|
3780
|
-
listeners.clear();
|
|
3781
|
-
onceCache.clear();
|
|
3782
|
-
}
|
|
3783
|
-
};
|
|
3784
|
-
}
|
|
3785
|
-
|
|
3786
3722
|
// src/suggestions/generate-suggestions.ts
|
|
3787
3723
|
var HISTORY_WINDOW = 8;
|
|
3788
3724
|
function capabilities(ctx, allAgentInfo) {
|
|
@@ -4755,6 +4691,250 @@ var FoldController = class {
|
|
|
4755
4691
|
}
|
|
4756
4692
|
};
|
|
4757
4693
|
|
|
4694
|
+
// src/state/interaction-context.ts
|
|
4695
|
+
function createInteractionContext(interactionId) {
|
|
4696
|
+
const state = {};
|
|
4697
|
+
const onceCache = /* @__PURE__ */ new Map();
|
|
4698
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4699
|
+
const disposers = /* @__PURE__ */ new Set();
|
|
4700
|
+
let disposed = false;
|
|
4701
|
+
const context = {
|
|
4702
|
+
interactionId,
|
|
4703
|
+
state,
|
|
4704
|
+
once(key, factory) {
|
|
4705
|
+
if (onceCache.has(key)) return onceCache.get(key);
|
|
4706
|
+
if (disposed) {
|
|
4707
|
+
logger2.warn(
|
|
4708
|
+
`InteractionContext.once("${key}") called after dispose; not creating a resource on an ended interaction.`
|
|
4709
|
+
);
|
|
4710
|
+
return null;
|
|
4711
|
+
}
|
|
4712
|
+
const value = factory();
|
|
4713
|
+
onceCache.set(key, value);
|
|
4714
|
+
return value;
|
|
4715
|
+
},
|
|
4716
|
+
notify() {
|
|
4717
|
+
for (const listener of listeners) {
|
|
4718
|
+
try {
|
|
4719
|
+
listener();
|
|
4720
|
+
} catch (e) {
|
|
4721
|
+
logger2.error("InteractionContext subscriber threw during notify()", e);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
},
|
|
4725
|
+
subscribe(listener) {
|
|
4726
|
+
listeners.add(listener);
|
|
4727
|
+
return () => {
|
|
4728
|
+
listeners.delete(listener);
|
|
4729
|
+
};
|
|
4730
|
+
},
|
|
4731
|
+
onDispose(fn) {
|
|
4732
|
+
if (disposed) {
|
|
4733
|
+
fn();
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4736
|
+
disposers.add(fn);
|
|
4737
|
+
}
|
|
4738
|
+
};
|
|
4739
|
+
return {
|
|
4740
|
+
context,
|
|
4741
|
+
dispose() {
|
|
4742
|
+
if (disposed) return;
|
|
4743
|
+
disposed = true;
|
|
4744
|
+
for (const fn of disposers) {
|
|
4745
|
+
try {
|
|
4746
|
+
fn();
|
|
4747
|
+
} catch (e) {
|
|
4748
|
+
logger2.error("InteractionContext onDispose callback threw", e);
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
disposers.clear();
|
|
4752
|
+
listeners.clear();
|
|
4753
|
+
onceCache.clear();
|
|
4754
|
+
}
|
|
4755
|
+
};
|
|
4756
|
+
}
|
|
4757
|
+
|
|
4758
|
+
// src/components/chat-driver/interaction-controller.ts
|
|
4759
|
+
var InteractionController = class {
|
|
4760
|
+
constructor(deps) {
|
|
4761
|
+
this.deps = deps;
|
|
4762
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
4763
|
+
/**
|
|
4764
|
+
* Live per-interaction contexts (GENC-1390), keyed by interactionId. Created in `request`
|
|
4765
|
+
* alongside the pending entry, disposed + removed in `resolve`. Non-serialisable (sockets,
|
|
4766
|
+
* timers, subscriber sets), so they live here — not on the DOM, not in the persisted store — and
|
|
4767
|
+
* therefore survive the element remounts (layout reflow/drag, transcript re-render) that this
|
|
4768
|
+
* feature exists to make harmless. At most one is live at a time (see the
|
|
4769
|
+
* one-interaction-at-a-time guard in `request`).
|
|
4770
|
+
*/
|
|
4771
|
+
this.contexts = /* @__PURE__ */ new Map();
|
|
4772
|
+
}
|
|
4773
|
+
/** See `ChatDriver.setHostInteractionRequester`. */
|
|
4774
|
+
setHostRequester(fn) {
|
|
4775
|
+
this.hostRequester = fn;
|
|
4776
|
+
}
|
|
4777
|
+
/** See `ChatDriver.requestInteraction`. */
|
|
4778
|
+
request(componentName, data, options) {
|
|
4779
|
+
if (this.hostRequester) {
|
|
4780
|
+
return this.hostRequester(componentName, data, options);
|
|
4781
|
+
}
|
|
4782
|
+
if (this.pending.size > 0) {
|
|
4783
|
+
throw new Error(
|
|
4784
|
+
"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."
|
|
4785
|
+
);
|
|
4786
|
+
}
|
|
4787
|
+
const interactionId = crypto.randomUUID();
|
|
4788
|
+
const chatInputDuringExecution = options?.chatInputDuringExecution;
|
|
4789
|
+
const timeoutMs = options?.timeoutMs;
|
|
4790
|
+
const presentation = options?.presentation;
|
|
4791
|
+
return new Promise((resolve, reject) => {
|
|
4792
|
+
this.pending.set(interactionId, {
|
|
4793
|
+
resolve,
|
|
4794
|
+
reject,
|
|
4795
|
+
overrideId: chatInputDuringExecution ? interactionId : void 0,
|
|
4796
|
+
// Resolve the interaction with a timeout result if the user does not
|
|
4797
|
+
// respond in time. `resolve` runs the same teardown a normal resolution
|
|
4798
|
+
// does (marks the widget read-only, reverts any input override) and
|
|
4799
|
+
// clears any still-pending handle.
|
|
4800
|
+
timeoutHandle: timeoutMs != null ? setTimeout(() => {
|
|
4801
|
+
this.resolve(interactionId, {
|
|
4802
|
+
status: "timeout",
|
|
4803
|
+
message: `Interaction timed out after ${timeoutMs}ms.`
|
|
4804
|
+
});
|
|
4805
|
+
}, timeoutMs) : void 0
|
|
4806
|
+
});
|
|
4807
|
+
recordMetaEvent(this.deps.sessionKey, "interaction.requested", {
|
|
4808
|
+
interactionId,
|
|
4809
|
+
component: componentName,
|
|
4810
|
+
agent: this.deps.activeAgentName()
|
|
4811
|
+
});
|
|
4812
|
+
this.deps.activityBus.publish("interaction-requested", void 0);
|
|
4813
|
+
if (chatInputDuringExecution) {
|
|
4814
|
+
this.deps.emit(
|
|
4815
|
+
new CustomEvent("interaction-start", {
|
|
4816
|
+
detail: { interactionId, chatInputDuringExecution }
|
|
4817
|
+
})
|
|
4818
|
+
);
|
|
4819
|
+
}
|
|
4820
|
+
this.contexts.set(interactionId, createInteractionContext(interactionId));
|
|
4821
|
+
this.deps.appendToHistory({
|
|
4822
|
+
role: "assistant",
|
|
4823
|
+
content: "",
|
|
4824
|
+
interaction: {
|
|
4825
|
+
interactionId,
|
|
4826
|
+
componentName,
|
|
4827
|
+
data,
|
|
4828
|
+
...presentation ? { presentation } : {}
|
|
4829
|
+
}
|
|
4830
|
+
});
|
|
4831
|
+
});
|
|
4832
|
+
}
|
|
4833
|
+
/**
|
|
4834
|
+
* See `ChatDriver.resolveInteraction`. Warns and returns when the id is unknown — a widget can
|
|
4835
|
+
* report twice, and the second report has nothing left to settle.
|
|
4836
|
+
*/
|
|
4837
|
+
resolve(interactionId, result) {
|
|
4838
|
+
const interaction = this.pending.get(interactionId);
|
|
4839
|
+
if (!interaction) {
|
|
4840
|
+
logger2.warn(`Interaction with ID ${interactionId} not found.`);
|
|
4841
|
+
return;
|
|
4842
|
+
}
|
|
4843
|
+
if (interaction.timeoutHandle) clearTimeout(interaction.timeoutHandle);
|
|
4844
|
+
this.deps.foldDiagnostics(
|
|
4845
|
+
interactionId,
|
|
4846
|
+
result?.diagnostics
|
|
4847
|
+
);
|
|
4848
|
+
const costUsd = typeof result?.costUsd === "number" && Number.isFinite(result.costUsd) && result.costUsd > 0 ? result.costUsd : void 0;
|
|
4849
|
+
const patched = this.deps.patchInteractionMessage(interactionId, (message) => ({
|
|
4850
|
+
...message,
|
|
4851
|
+
interaction: { ...message.interaction, resolved: result },
|
|
4852
|
+
...costUsd != null ? { externalCostUsd: (message.externalCostUsd ?? 0) + costUsd } : {}
|
|
4853
|
+
}));
|
|
4854
|
+
if (patched) this.deps.notifyHistoryUpdated();
|
|
4855
|
+
if (typeof result?.userMessage === "string" && result.userMessage.trim()) {
|
|
4856
|
+
this.deps.appendToHistory({ role: "synthetic-user", content: result.userMessage });
|
|
4857
|
+
}
|
|
4858
|
+
if (interaction.overrideId) {
|
|
4859
|
+
this.deps.emit(new CustomEvent("interaction-stop", { detail: { interactionId } }));
|
|
4860
|
+
}
|
|
4861
|
+
recordMetaEvent(this.deps.sessionKey, "interaction.resolved", { interactionId });
|
|
4862
|
+
this.deps.activityBus.publish("interaction-resolved", void 0);
|
|
4863
|
+
interaction.resolve(result);
|
|
4864
|
+
this.pending.delete(interactionId);
|
|
4865
|
+
this.contexts.get(interactionId)?.dispose();
|
|
4866
|
+
this.contexts.delete(interactionId);
|
|
4867
|
+
}
|
|
4868
|
+
/**
|
|
4869
|
+
* See `ChatDriver.recordExternalCost`.
|
|
4870
|
+
*
|
|
4871
|
+
* **Why this exists separately from `resolve`.** A widget's spend reaches the session total only
|
|
4872
|
+
* through `InteractionResult.costUsd`, which is read exactly once — `resolve` does its whole job
|
|
4873
|
+
* against a live pending entry and then deletes it. That is correct for the resolution itself,
|
|
4874
|
+
* but it assumes a widget stops costing money when it resolves, and several deliberately do not:
|
|
4875
|
+
* a widget that resolves EARLY so the assistant stops waiting (options shown, a preview
|
|
4876
|
+
* rendered) may stay interactive for minutes afterwards and do more paid work on the user's
|
|
4877
|
+
* behalf. Re-emitting `interaction-completed` does not help — with the interaction gone from the
|
|
4878
|
+
* map the call is a silent no-op — so that spend had nowhere to go and was simply missing from
|
|
4879
|
+
* the figure shown to the user.
|
|
4880
|
+
*
|
|
4881
|
+
* Touches only `externalCostUsd`: the recorded `interaction.resolved` payload is left exactly as
|
|
4882
|
+
* the resolution wrote it, because this is not a second resolution and must not read like one.
|
|
4883
|
+
*/
|
|
4884
|
+
recordExternalCost(interactionId, costUsd) {
|
|
4885
|
+
const drop = (reason) => {
|
|
4886
|
+
recordMetaEvent(this.deps.sessionKey, "interaction.external-cost-dropped", {
|
|
4887
|
+
interactionId,
|
|
4888
|
+
costUsd,
|
|
4889
|
+
reason
|
|
4890
|
+
});
|
|
4891
|
+
return false;
|
|
4892
|
+
};
|
|
4893
|
+
if (!interactionId) return drop("no interactionId");
|
|
4894
|
+
if (typeof costUsd !== "number" || !Number.isFinite(costUsd) || costUsd <= 0) {
|
|
4895
|
+
return drop("not a positive finite amount");
|
|
4896
|
+
}
|
|
4897
|
+
const patched = this.deps.patchInteractionMessage(interactionId, (message) => ({
|
|
4898
|
+
...message,
|
|
4899
|
+
externalCostUsd: (message.externalCostUsd ?? 0) + costUsd
|
|
4900
|
+
}));
|
|
4901
|
+
if (!patched) return drop("interaction message no longer in history");
|
|
4902
|
+
recordMetaEvent(this.deps.sessionKey, "interaction.external-cost", { interactionId, costUsd });
|
|
4903
|
+
this.deps.emit(
|
|
4904
|
+
new CustomEvent("external-cost-recorded", {
|
|
4905
|
+
detail: { interactionId, costUsd }
|
|
4906
|
+
})
|
|
4907
|
+
);
|
|
4908
|
+
return true;
|
|
4909
|
+
}
|
|
4910
|
+
/** See `ChatDriver.getInteractionContext`. */
|
|
4911
|
+
context(interactionId) {
|
|
4912
|
+
return this.contexts.get(interactionId)?.context;
|
|
4913
|
+
}
|
|
4914
|
+
/**
|
|
4915
|
+
* Resolve every in-flight interaction with a `'timeout'` result, closing its
|
|
4916
|
+
* widget read-only. Used when a sub-agent that owns the open widget times out:
|
|
4917
|
+
* the widget is parked in *this* driver's map (interactions resolve on the
|
|
4918
|
+
* host, not the child), so cancelling it here unblocks the sub-agent handler
|
|
4919
|
+
* awaiting it. The "one interaction at a time" invariant means there is at
|
|
4920
|
+
* most one.
|
|
4921
|
+
*/
|
|
4922
|
+
cancelAll(message) {
|
|
4923
|
+
for (const interactionId of [...this.pending.keys()]) {
|
|
4924
|
+
this.resolve(interactionId, { status: "timeout", message });
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
/**
|
|
4928
|
+
* Drop any live interaction contexts on driver teardown. Normally empty (a context is created
|
|
4929
|
+
* and destroyed with its pending interaction, and dispose is not reached mid-interaction);
|
|
4930
|
+
* disposed defensively so a stray context can never leak an open socket/timer.
|
|
4931
|
+
*/
|
|
4932
|
+
disposeContexts() {
|
|
4933
|
+
for (const handle of this.contexts.values()) handle.dispose();
|
|
4934
|
+
this.contexts.clear();
|
|
4935
|
+
}
|
|
4936
|
+
};
|
|
4937
|
+
|
|
4758
4938
|
// src/components/chat-driver/turn-failures.ts
|
|
4759
4939
|
var TURN_FAILURE_EXITS = {
|
|
4760
4940
|
contextGuard: {
|
|
@@ -4836,6 +5016,160 @@ function turnDone(failureReason, budget, providerRefused) {
|
|
|
4836
5016
|
};
|
|
4837
5017
|
}
|
|
4838
5018
|
|
|
5019
|
+
// src/components/chat-driver/turn-recorder.ts
|
|
5020
|
+
var TurnRecorder = class {
|
|
5021
|
+
constructor(sessionKey, maxSnapshots) {
|
|
5022
|
+
this.sessionKey = sessionKey;
|
|
5023
|
+
this.maxSnapshots = maxSnapshots;
|
|
5024
|
+
/**
|
|
5025
|
+
* Ring buffer of per-LLM-call snapshots, capped at `maxSnapshots`; older entries drop off as
|
|
5026
|
+
* new ones arrive.
|
|
5027
|
+
*/
|
|
5028
|
+
this.buffer = [];
|
|
5029
|
+
/** Monotonic counter that survives agent swaps — useful for cross-referencing with history. */
|
|
5030
|
+
this.turnIndex = 0;
|
|
5031
|
+
/**
|
|
5032
|
+
* `turn` + `message` diagnostic entries harvested from an out-of-band source. `event` entries
|
|
5033
|
+
* are NOT kept here — they are folded into the session's meta-event registry so they inherit
|
|
5034
|
+
* fresh host indices, and reach the log through `getMetaEvents`. Append-only, and never cleared:
|
|
5035
|
+
* it lives with the recorder, so a new chat (fresh driver) starts empty while a compaction (same
|
|
5036
|
+
* instance) keeps it. (GENC-1461.)
|
|
5037
|
+
*/
|
|
5038
|
+
this.externalEntries = [];
|
|
5039
|
+
/**
|
|
5040
|
+
* Monotonic count of external-diagnostics batches folded this driver-lifetime. Namespaces each
|
|
5041
|
+
* batch's re-keyed `turn` entries so distinct server generations in one chat session can't
|
|
5042
|
+
* collide on `turnIndex` in the forward-capture persister (which dedups turns by that key).
|
|
5043
|
+
*/
|
|
5044
|
+
this.externalBatches = 0;
|
|
5045
|
+
}
|
|
5046
|
+
/** Point at the incoming agent's `getDebugSnapshot`, or clear it when the agent supplies none. */
|
|
5047
|
+
setSnapshotter(snapshotter) {
|
|
5048
|
+
this.snapshotter = snapshotter;
|
|
5049
|
+
}
|
|
5050
|
+
/**
|
|
5051
|
+
* The per-turn snapshots captured so far — what the LLM saw on each turn: system prompt, tool
|
|
5052
|
+
* surface, and agent-supplied state (e.g. a machine snapshot).
|
|
5053
|
+
*/
|
|
5054
|
+
snapshots() {
|
|
5055
|
+
return this.buffer;
|
|
5056
|
+
}
|
|
5057
|
+
/**
|
|
5058
|
+
* The harvested `turn` + `message` diagnostic entries. Opaque, and chronologically sorted by the
|
|
5059
|
+
* host at assemble time.
|
|
5060
|
+
*/
|
|
5061
|
+
externalDiagnostics() {
|
|
5062
|
+
return this.externalEntries;
|
|
5063
|
+
}
|
|
5064
|
+
/**
|
|
5065
|
+
* Push one snapshot to the ring buffer. Called inside `runToolLoop` just
|
|
5066
|
+
* before each LLM call — that's the latest point where the prompt, tool
|
|
5067
|
+
* surface, and agent state line up with what the model is about to see.
|
|
5068
|
+
*
|
|
5069
|
+
* Returns the pushed object so the caller can back-fill what only the response
|
|
5070
|
+
* knows (`usage`). Mutating it after the fact is safe whether or not the ring
|
|
5071
|
+
* buffer has since evicted it — an evicted snapshot is simply no longer exported.
|
|
5072
|
+
*/
|
|
5073
|
+
record(turn) {
|
|
5074
|
+
let agentSnapshot;
|
|
5075
|
+
if (this.snapshotter) {
|
|
5076
|
+
try {
|
|
5077
|
+
agentSnapshot = this.snapshotter();
|
|
5078
|
+
} catch (e) {
|
|
5079
|
+
agentSnapshot = `<getDebugSnapshot threw: ${e instanceof Error ? e.message : String(e)}>`;
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
5082
|
+
const turnIndex = String(this.turnIndex);
|
|
5083
|
+
this.turnIndex += 1;
|
|
5084
|
+
const snapshot = {
|
|
5085
|
+
...turn,
|
|
5086
|
+
turnIndex,
|
|
5087
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5088
|
+
agentSnapshot
|
|
5089
|
+
};
|
|
5090
|
+
this.buffer.push(snapshot);
|
|
5091
|
+
this.trim();
|
|
5092
|
+
return snapshot;
|
|
5093
|
+
}
|
|
5094
|
+
/**
|
|
5095
|
+
* Merge a sub-agent's turn snapshots into this driver's buffer so they surface
|
|
5096
|
+
* as `kind:'turn'` entries in the exported debug log. The child runs as a
|
|
5097
|
+
* separate, discarded driver, so its snapshots would otherwise be lost. Each is
|
|
5098
|
+
* re-labelled under the parent turn that activated the sub-agent: the child's
|
|
5099
|
+
* own (numeric) turns become `"<parentTurn>-1"`, `"-2"`, … (1-based, in order);
|
|
5100
|
+
* any already-forwarded grand-child labels (strings) have their leading segment
|
|
5101
|
+
* remapped the same way, so nesting composes (`"5-2"` → `"5-2-1"`).
|
|
5102
|
+
*
|
|
5103
|
+
* Note: two sub-agents invoked in the *same* parent turn share the prefix, so
|
|
5104
|
+
* their labels can repeat — `agentName` on each snapshot disambiguates them.
|
|
5105
|
+
*/
|
|
5106
|
+
forwardSubAgent(childSnapshots) {
|
|
5107
|
+
if (childSnapshots.length === 0) return;
|
|
5108
|
+
const parentTurn = Math.max(0, this.turnIndex - 1);
|
|
5109
|
+
const ownTurnLabel = /* @__PURE__ */ new Map();
|
|
5110
|
+
let ownPos = 0;
|
|
5111
|
+
for (const snap of childSnapshots) {
|
|
5112
|
+
let turnIndex;
|
|
5113
|
+
if (!snap.turnIndex.includes("-")) {
|
|
5114
|
+
ownPos += 1;
|
|
5115
|
+
turnIndex = `${parentTurn}-${ownPos}`;
|
|
5116
|
+
ownTurnLabel.set(snap.turnIndex, turnIndex);
|
|
5117
|
+
} else {
|
|
5118
|
+
const [lead, ...rest] = snap.turnIndex.split("-");
|
|
5119
|
+
const leadLabel = ownTurnLabel.get(lead) ?? `${parentTurn}-${lead}`;
|
|
5120
|
+
turnIndex = [leadLabel, ...rest].join("-");
|
|
5121
|
+
}
|
|
5122
|
+
this.buffer.push({ ...snap, turnIndex });
|
|
5123
|
+
}
|
|
5124
|
+
this.trim();
|
|
5125
|
+
}
|
|
5126
|
+
/**
|
|
5127
|
+
* Fold a widget's out-of-band diagnostics into this session — typically a server-side
|
|
5128
|
+
* ChatDriver's collated log, arriving on `InteractionResult.diagnostics` — so the host's debug
|
|
5129
|
+
* log and persisted diagnostics stream include them. Independent of the interaction's own
|
|
5130
|
+
* message: these describe the widget's work, not that message. A non-array or empty harvest is
|
|
5131
|
+
* a no-op.
|
|
5132
|
+
*/
|
|
5133
|
+
foldDiagnostics(interactionId, harvested) {
|
|
5134
|
+
if (!Array.isArray(harvested) || !harvested.length) return;
|
|
5135
|
+
const batch = this.externalBatches += 1;
|
|
5136
|
+
const externalEvents = [];
|
|
5137
|
+
let turnCount = 0;
|
|
5138
|
+
let messageCount = 0;
|
|
5139
|
+
for (const raw of harvested) {
|
|
5140
|
+
if (!raw || typeof raw !== "object" || typeof raw.kind !== "string") {
|
|
5141
|
+
continue;
|
|
5142
|
+
}
|
|
5143
|
+
const entry = raw;
|
|
5144
|
+
if (entry.kind === "event") {
|
|
5145
|
+
const { kind: _kind, ...event } = entry;
|
|
5146
|
+
externalEvents.push(event);
|
|
5147
|
+
} else if (entry.kind === "turn") {
|
|
5148
|
+
this.externalEntries.push({
|
|
5149
|
+
...entry,
|
|
5150
|
+
turnIndex: `server-generation.${batch}:${turnCount}`
|
|
5151
|
+
});
|
|
5152
|
+
turnCount += 1;
|
|
5153
|
+
} else if (entry.kind === "message") {
|
|
5154
|
+
messageCount += 1;
|
|
5155
|
+
this.externalEntries.push(entry);
|
|
5156
|
+
}
|
|
5157
|
+
}
|
|
5158
|
+
if (externalEvents.length) mergeMetaEvents(this.sessionKey, externalEvents);
|
|
5159
|
+
recordMetaEvent(this.sessionKey, "external-diagnostics.folded", {
|
|
5160
|
+
interactionId,
|
|
5161
|
+
batch,
|
|
5162
|
+
counts: { turn: turnCount, event: externalEvents.length, message: messageCount }
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
5165
|
+
/** Hold the ring buffer at its cap, dropping oldest-first. */
|
|
5166
|
+
trim() {
|
|
5167
|
+
while (this.buffer.length > this.maxSnapshots) {
|
|
5168
|
+
this.buffer.shift();
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
};
|
|
5172
|
+
|
|
4839
5173
|
// src/components/chat-driver/chat-driver.ts
|
|
4840
5174
|
var toolResultImages = (result) => {
|
|
4841
5175
|
if (!result || typeof result !== "object") return void 0;
|
|
@@ -4873,17 +5207,6 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
4873
5207
|
this.busy = false;
|
|
4874
5208
|
/** Epoch ms when the current turn loop began — drives the `turn.end` duration. */
|
|
4875
5209
|
this.turnStartedAt = 0;
|
|
4876
|
-
this.pendingInteractions = /* @__PURE__ */ new Map();
|
|
4877
|
-
/**
|
|
4878
|
-
* Live per-interaction contexts (GENC-1390), keyed by interactionId. Created in
|
|
4879
|
-
* `requestInteraction` alongside the pending entry, disposed + removed in
|
|
4880
|
-
* `resolveInteraction`. Non-serialisable (sockets, timers, subscriber sets), so
|
|
4881
|
-
* they live here on the driver — not on the DOM, not in the persisted store — and
|
|
4882
|
-
* therefore survive the element remounts (layout reflow/drag, transcript
|
|
4883
|
-
* re-render) that this feature exists to make harmless. At most one is live at a
|
|
4884
|
-
* time (see the one-interaction-at-a-time guard in `requestInteraction`).
|
|
4885
|
-
*/
|
|
4886
|
-
this.interactionContexts = /* @__PURE__ */ new Map();
|
|
4887
5210
|
/**
|
|
4888
5211
|
* Tool-declared condensation policies, keyed by tool-call id. Populated by
|
|
4889
5212
|
* `condenseWhen` (first-wins per call); read by `applyCondensation` before each
|
|
@@ -4982,31 +5305,6 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
4982
5305
|
* doesn't leak forward.
|
|
4983
5306
|
*/
|
|
4984
5307
|
this.agentReleaseRequested = false;
|
|
4985
|
-
/**
|
|
4986
|
-
* Ring buffer of per-LLM-call snapshots. Cap is configurable via
|
|
4987
|
-
* `chatConfig.agent.maxTurnSnapshots`; older entries drop off as new ones
|
|
4988
|
-
* arrive. See {@link TurnSnapshot} for the captured shape.
|
|
4989
|
-
*/
|
|
4990
|
-
this.turnSnapshots = [];
|
|
4991
|
-
/**
|
|
4992
|
-
* `turn` + `message` diagnostic-log entries harvested from an out-of-band source (a resolved
|
|
4993
|
-
* interaction whose widget returned `InteractionResult.diagnostics` — e.g. a server-side
|
|
4994
|
-
* ChatDriver's debug log). `event` entries are NOT kept here — they're folded into the session's
|
|
4995
|
-
* meta-event registry via `mergeMetaEvents` (see `resolveInteraction`) so they inherit fresh host
|
|
4996
|
-
* indices, and reach the log through `getMetaEvents`. Append-only; the host folds these into its
|
|
4997
|
-
* debug log alongside this driver's own timeline. Not cleared here — it lives with the driver
|
|
4998
|
-
* instance, like `turnSnapshots`, so a new chat (fresh driver) starts empty while a compaction
|
|
4999
|
-
* (same instance) keeps it. (GENC-1461.)
|
|
5000
|
-
*/
|
|
5001
|
-
this.externalDiagnostics = [];
|
|
5002
|
-
/**
|
|
5003
|
-
* Monotonic count of external-diagnostics batches harvested this driver-lifetime. Namespaces each
|
|
5004
|
-
* batch's re-keyed `turn` entries so distinct server generations in one chat session can't collide
|
|
5005
|
-
* on `turnIndex` in the forward-capture persister (which dedups turns by that key). (GENC-1461.)
|
|
5006
|
-
*/
|
|
5007
|
-
this.externalDiagnosticsBatches = 0;
|
|
5008
|
-
/** Monotonic counter that survives agent swaps — useful for cross-referencing with history. */
|
|
5009
|
-
this.globalTurnIndex = 0;
|
|
5010
5308
|
/**
|
|
5011
5309
|
* Aborted by `dispose()` on driver teardown (e.g. an agent-config swap).
|
|
5012
5310
|
* Threaded into every provider call as `ChatRequestOptions.signal`, so a
|
|
@@ -5116,7 +5414,17 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5116
5414
|
this.systemPrompt = systemPrompt;
|
|
5117
5415
|
this.primerHistory = primerHistory;
|
|
5118
5416
|
this.maxFoldOperations = maxFoldOperations;
|
|
5119
|
-
this.
|
|
5417
|
+
this.turnRecorder = new TurnRecorder(sessionKey, maxTurnSnapshots);
|
|
5418
|
+
this.interactions = new InteractionController({
|
|
5419
|
+
sessionKey,
|
|
5420
|
+
activityBus,
|
|
5421
|
+
activeAgentName: () => this.activeAgentName,
|
|
5422
|
+
appendToHistory: (message) => this.appendToHistory(message),
|
|
5423
|
+
patchInteractionMessage: (interactionId, patch) => this.patchInteractionMessage(interactionId, patch),
|
|
5424
|
+
notifyHistoryUpdated: () => this.notifyHistoryUpdated(),
|
|
5425
|
+
emit: (event) => this.dispatchEvent(event),
|
|
5426
|
+
foldDiagnostics: (interactionId, harvested) => this.turnRecorder.foldDiagnostics(interactionId, harvested)
|
|
5427
|
+
});
|
|
5120
5428
|
if (isObservableAIProviderRegistry(this.providerRegistry)) {
|
|
5121
5429
|
this.unsubscribeRegistry = this.providerRegistry.subscribe(() => {
|
|
5122
5430
|
this.resolvedProviderCache.clear();
|
|
@@ -5134,7 +5442,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5134
5442
|
* on to completion or the transport timeout. Called by the host on driver
|
|
5135
5443
|
* swap and by `OrchestratingDriver.dispose()`. Idempotent.
|
|
5136
5444
|
*
|
|
5137
|
-
* Deliberately does NOT settle
|
|
5445
|
+
* Deliberately does NOT settle pending interactions here: dispose is never
|
|
5138
5446
|
* reached while one is pending. The only host-level dispose is the agent-swap
|
|
5139
5447
|
* rebuild, which is guarded by `isBusy()` — a pending interaction keeps the
|
|
5140
5448
|
* tool loop suspended (`busy === true`), so the swap is deferred until the
|
|
@@ -5145,8 +5453,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5145
5453
|
*/
|
|
5146
5454
|
async dispose() {
|
|
5147
5455
|
this.lifecycleController.abort(new DOMException("AI assistant driver disposed", "AbortError"));
|
|
5148
|
-
|
|
5149
|
-
this.interactionContexts.clear();
|
|
5456
|
+
this.interactions.disposeContexts();
|
|
5150
5457
|
this.unsubscribeRegistry?.();
|
|
5151
5458
|
this.unsubscribeRegistry = void 0;
|
|
5152
5459
|
await this.agentSession.dispose();
|
|
@@ -5403,7 +5710,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5403
5710
|
this.activeAgentName = config.name;
|
|
5404
5711
|
this.displayName = config.displayName;
|
|
5405
5712
|
this.activeAgentLabel = typeof config.displayName === "string" ? config.displayName : config.name;
|
|
5406
|
-
this.
|
|
5713
|
+
this.turnRecorder.setSnapshotter(config.getDebugSnapshot);
|
|
5407
5714
|
this.subAgentsMap = new Map((config.subAgents ?? []).map((s) => [s.name, s]));
|
|
5408
5715
|
this.activeProviderInput = config.provider;
|
|
5409
5716
|
this.activeTemperatureInput = config.temperature;
|
|
@@ -5600,93 +5907,21 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5600
5907
|
* log exporter to show what the LLM saw on each turn — system prompt, tool
|
|
5601
5908
|
* surface, and agent-supplied state (e.g. a machine snapshot).
|
|
5602
5909
|
*
|
|
5603
|
-
* Ring-buffered at `
|
|
5910
|
+
* Ring-buffered at `maxTurnSnapshots`; older entries are dropped.
|
|
5604
5911
|
*/
|
|
5605
5912
|
getTurnSnapshots() {
|
|
5606
|
-
return this.
|
|
5913
|
+
return this.turnRecorder.snapshots();
|
|
5607
5914
|
}
|
|
5608
5915
|
/**
|
|
5609
5916
|
* The `turn` + `message` diagnostic-log entries harvested from resolved interactions'
|
|
5610
5917
|
* `InteractionResult.diagnostics` (e.g. a server-side ChatDriver's collated debug log). The host
|
|
5611
5918
|
* concatenates these into its own debug log so `downloadDebugLog`/the persisted diagnostics stream
|
|
5612
5919
|
* include external-engine logs; `event` entries are excluded here because they're merged into the
|
|
5613
|
-
* session's meta-event registry instead
|
|
5614
|
-
*
|
|
5920
|
+
* session's meta-event registry instead and reach the log via `getMetaEvents`. Opaque,
|
|
5921
|
+
* chronologically sorted by the host at assemble time. (GENC-1461.)
|
|
5615
5922
|
*/
|
|
5616
5923
|
getExternalDiagnostics() {
|
|
5617
|
-
return this.externalDiagnostics;
|
|
5618
|
-
}
|
|
5619
|
-
/**
|
|
5620
|
-
* Merge a sub-agent's turn snapshots into this driver's buffer so they surface
|
|
5621
|
-
* as `kind:'turn'` entries in the exported debug log. The child runs as a
|
|
5622
|
-
* separate, discarded driver, so its snapshots would otherwise be lost. Each is
|
|
5623
|
-
* re-labelled under the parent turn that activated the sub-agent: the child's
|
|
5624
|
-
* own (numeric) turns become `"<parentTurn>-1"`, `"-2"`, … (1-based, in order);
|
|
5625
|
-
* any already-forwarded grand-child labels (strings) have their leading segment
|
|
5626
|
-
* remapped the same way, so nesting composes (`"5-2"` → `"5-2-1"`).
|
|
5627
|
-
*
|
|
5628
|
-
* Note: two sub-agents invoked in the *same* parent turn share the prefix, so
|
|
5629
|
-
* their labels can repeat — `agentName` on each snapshot disambiguates them.
|
|
5630
|
-
*/
|
|
5631
|
-
forwardSubAgentSnapshots(childSnapshots) {
|
|
5632
|
-
if (childSnapshots.length === 0) return;
|
|
5633
|
-
const parentTurn = Math.max(0, this.globalTurnIndex - 1);
|
|
5634
|
-
const ownTurnLabel = /* @__PURE__ */ new Map();
|
|
5635
|
-
let ownPos = 0;
|
|
5636
|
-
for (const snap of childSnapshots) {
|
|
5637
|
-
let turnIndex;
|
|
5638
|
-
if (!snap.turnIndex.includes("-")) {
|
|
5639
|
-
ownPos += 1;
|
|
5640
|
-
turnIndex = `${parentTurn}-${ownPos}`;
|
|
5641
|
-
ownTurnLabel.set(snap.turnIndex, turnIndex);
|
|
5642
|
-
} else {
|
|
5643
|
-
const [lead, ...rest] = snap.turnIndex.split("-");
|
|
5644
|
-
const leadLabel = ownTurnLabel.get(lead) ?? `${parentTurn}-${lead}`;
|
|
5645
|
-
turnIndex = [leadLabel, ...rest].join("-");
|
|
5646
|
-
}
|
|
5647
|
-
this.turnSnapshots.push({ ...snap, turnIndex });
|
|
5648
|
-
}
|
|
5649
|
-
while (this.turnSnapshots.length > this.maxTurnSnapshots) {
|
|
5650
|
-
this.turnSnapshots.shift();
|
|
5651
|
-
}
|
|
5652
|
-
}
|
|
5653
|
-
/**
|
|
5654
|
-
* Push one snapshot to the ring buffer. Called inside `runToolLoop` just
|
|
5655
|
-
* before each LLM call — that's the latest point where the prompt, tool
|
|
5656
|
-
* surface, and agent state line up with what the model is about to see.
|
|
5657
|
-
*
|
|
5658
|
-
* Returns the pushed object so the caller can back-fill what only the response
|
|
5659
|
-
* knows (`usage`). Mutating it after the fact is safe whether or not the ring
|
|
5660
|
-
* buffer has since evicted it — an evicted snapshot is simply no longer exported.
|
|
5661
|
-
*/
|
|
5662
|
-
recordTurnSnapshot(resolvedSystemPrompt, temperature, toolChoice, tailContext) {
|
|
5663
|
-
let agentSnapshot;
|
|
5664
|
-
if (this.debugSnapshotter) {
|
|
5665
|
-
try {
|
|
5666
|
-
agentSnapshot = this.debugSnapshotter();
|
|
5667
|
-
} catch (e) {
|
|
5668
|
-
agentSnapshot = `<getDebugSnapshot threw: ${e instanceof Error ? e.message : String(e)}>`;
|
|
5669
|
-
}
|
|
5670
|
-
}
|
|
5671
|
-
const turnIndex = String(this.globalTurnIndex);
|
|
5672
|
-
this.globalTurnIndex += 1;
|
|
5673
|
-
const snapshot = {
|
|
5674
|
-
turnIndex,
|
|
5675
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5676
|
-
agentName: this.activeAgentName,
|
|
5677
|
-
agentLabel: this.activeAgentLabel,
|
|
5678
|
-
systemPrompt: resolvedSystemPrompt,
|
|
5679
|
-
tailContext,
|
|
5680
|
-
toolNames: this.toolDefinitions.map((t) => t.name),
|
|
5681
|
-
temperature,
|
|
5682
|
-
toolChoice,
|
|
5683
|
-
agentSnapshot
|
|
5684
|
-
};
|
|
5685
|
-
this.turnSnapshots.push(snapshot);
|
|
5686
|
-
if (this.turnSnapshots.length > this.maxTurnSnapshots) {
|
|
5687
|
-
this.turnSnapshots.shift();
|
|
5688
|
-
}
|
|
5689
|
-
return snapshot;
|
|
5924
|
+
return this.turnRecorder.externalDiagnostics();
|
|
5690
5925
|
}
|
|
5691
5926
|
/**
|
|
5692
5927
|
* Set the mid-loop context guard. Its margin is deliberately far smaller than
|
|
@@ -5731,6 +5966,10 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5731
5966
|
);
|
|
5732
5967
|
return { estimated, threshold };
|
|
5733
5968
|
}
|
|
5969
|
+
/**
|
|
5970
|
+
* Optional transform applied to conversation history immediately before each LLM request.
|
|
5971
|
+
* Cleared when `undefined`. Does not alter stored history.
|
|
5972
|
+
*/
|
|
5734
5973
|
setProviderHistoryTransform(transform) {
|
|
5735
5974
|
this.providerHistoryTransform = transform;
|
|
5736
5975
|
}
|
|
@@ -5884,7 +6123,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5884
6123
|
* naturally: a grandchild → child → root.
|
|
5885
6124
|
*/
|
|
5886
6125
|
setHostInteractionRequester(fn) {
|
|
5887
|
-
this.
|
|
6126
|
+
this.interactions.setHostRequester(fn);
|
|
5888
6127
|
}
|
|
5889
6128
|
/**
|
|
5890
6129
|
* Request a custom UI interaction. Emits a new message with the interaction.
|
|
@@ -5906,76 +6145,15 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5906
6145
|
* bubble and/or shows the "Assistant" label.
|
|
5907
6146
|
*/
|
|
5908
6147
|
async requestInteraction(componentName, data, options) {
|
|
5909
|
-
|
|
5910
|
-
return this.hostInteractionRequester(componentName, data, options);
|
|
5911
|
-
}
|
|
5912
|
-
if (this.pendingInteractions.size > 0) {
|
|
5913
|
-
throw new Error(
|
|
5914
|
-
"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."
|
|
5915
|
-
);
|
|
5916
|
-
}
|
|
5917
|
-
const interactionId = crypto.randomUUID();
|
|
5918
|
-
const chatInputDuringExecution = options?.chatInputDuringExecution;
|
|
5919
|
-
const timeoutMs = options?.timeoutMs;
|
|
5920
|
-
const presentation = options?.presentation;
|
|
5921
|
-
return new Promise((resolve, reject) => {
|
|
5922
|
-
this.pendingInteractions.set(interactionId, {
|
|
5923
|
-
resolve,
|
|
5924
|
-
reject,
|
|
5925
|
-
overrideId: chatInputDuringExecution ? interactionId : void 0,
|
|
5926
|
-
// Resolve the interaction with a timeout result if the user does not
|
|
5927
|
-
// respond in time. `resolveInteraction` runs the same teardown a normal
|
|
5928
|
-
// resolution does (marks the widget read-only, reverts any input
|
|
5929
|
-
// override) and clears any still-pending handle.
|
|
5930
|
-
timeoutHandle: timeoutMs != null ? setTimeout(() => {
|
|
5931
|
-
this.resolveInteraction(interactionId, {
|
|
5932
|
-
status: "timeout",
|
|
5933
|
-
message: `Interaction timed out after ${timeoutMs}ms.`
|
|
5934
|
-
});
|
|
5935
|
-
}, timeoutMs) : void 0
|
|
5936
|
-
});
|
|
5937
|
-
recordMetaEvent(this.sessionKey, "interaction.requested", {
|
|
5938
|
-
interactionId,
|
|
5939
|
-
component: componentName,
|
|
5940
|
-
agent: this.activeAgentName
|
|
5941
|
-
});
|
|
5942
|
-
this.activityBus.publish("interaction-requested", void 0);
|
|
5943
|
-
if (chatInputDuringExecution) {
|
|
5944
|
-
this.dispatchEvent(
|
|
5945
|
-
new CustomEvent("interaction-start", {
|
|
5946
|
-
detail: { interactionId, chatInputDuringExecution }
|
|
5947
|
-
})
|
|
5948
|
-
);
|
|
5949
|
-
}
|
|
5950
|
-
this.interactionContexts.set(interactionId, createInteractionContext(interactionId));
|
|
5951
|
-
this.appendToHistory({
|
|
5952
|
-
role: "assistant",
|
|
5953
|
-
content: "",
|
|
5954
|
-
interaction: {
|
|
5955
|
-
interactionId,
|
|
5956
|
-
componentName,
|
|
5957
|
-
data,
|
|
5958
|
-
...presentation ? { presentation } : {}
|
|
5959
|
-
}
|
|
5960
|
-
});
|
|
5961
|
-
});
|
|
6148
|
+
return this.interactions.request(componentName, data, options);
|
|
5962
6149
|
}
|
|
5963
6150
|
/**
|
|
5964
|
-
* Add external (non-LLM) spend to an interaction's message AFTER it has resolved
|
|
6151
|
+
* Add external (non-LLM) spend to an interaction's message AFTER it has resolved — for a widget
|
|
6152
|
+
* that resolves early so the assistant stops waiting, then keeps doing paid work on the user's
|
|
6153
|
+
* behalf. `InteractionController.recordExternalCost` explains why the resolution path cannot
|
|
6154
|
+
* carry this.
|
|
5965
6155
|
*
|
|
5966
|
-
*
|
|
5967
|
-
* total only through `InteractionResult.costUsd`, which is read exactly once — `resolveInteraction`
|
|
5968
|
-
* does its whole job inside `if (interaction)` and then deletes the entry from
|
|
5969
|
-
* `pendingInteractions`. That is correct for the resolution itself, but it assumes a widget stops
|
|
5970
|
-
* costing money when it resolves, and several deliberately do not: a widget that resolves EARLY so
|
|
5971
|
-
* the assistant stops waiting (options shown, a preview rendered) may stay interactive for minutes
|
|
5972
|
-
* afterwards and do more paid work on the user's behalf. Re-emitting `interaction-completed` does
|
|
5973
|
-
* not help — with the interaction gone from the map the call is a silent no-op — so that spend had
|
|
5974
|
-
* nowhere to go and was simply missing from the figure shown to the user.
|
|
5975
|
-
*
|
|
5976
|
-
* Additive by design: the caller reports a DELTA, not a running total, so repeated calls sum. It
|
|
5977
|
-
* touches only `externalCostUsd`; the recorded `interaction.resolved` payload is left exactly as
|
|
5978
|
-
* the resolution wrote it, because this is not a second resolution and must not read like one.
|
|
6156
|
+
* Additive by design: the caller reports a DELTA, not a running total, so repeated calls sum.
|
|
5979
6157
|
*
|
|
5980
6158
|
* Deliberately does NOT require the interaction to be pending, does not resurrect it, and has no
|
|
5981
6159
|
* loading/turn side effects — this is bookkeeping about a turn that already happened.
|
|
@@ -5985,106 +6163,19 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
5985
6163
|
* @returns `true` when the spend was recorded; `false` when it was not — either the value was
|
|
5986
6164
|
* not a positive finite amount, or the message is no longer in history (trimmed or condensed
|
|
5987
6165
|
* away). A real outcome a caller may want to log rather than a failure to throw on; every
|
|
5988
|
-
* `false` also records an `interaction.external-cost-dropped` meta event
|
|
5989
|
-
*
|
|
6166
|
+
* `false` also records an `interaction.external-cost-dropped` meta event, so money can never
|
|
6167
|
+
* disappear without a trace.
|
|
5990
6168
|
* @beta
|
|
5991
6169
|
*/
|
|
5992
6170
|
recordExternalCost(interactionId, costUsd) {
|
|
5993
|
-
|
|
5994
|
-
recordMetaEvent(this.sessionKey, "interaction.external-cost-dropped", {
|
|
5995
|
-
interactionId,
|
|
5996
|
-
costUsd,
|
|
5997
|
-
reason
|
|
5998
|
-
});
|
|
5999
|
-
return false;
|
|
6000
|
-
};
|
|
6001
|
-
if (!interactionId) return drop("no interactionId");
|
|
6002
|
-
if (typeof costUsd !== "number" || !Number.isFinite(costUsd) || costUsd <= 0) {
|
|
6003
|
-
return drop("not a positive finite amount");
|
|
6004
|
-
}
|
|
6005
|
-
const idx = this.history.findIndex((m) => m.interaction?.interactionId === interactionId);
|
|
6006
|
-
if (idx === -1) return drop("interaction message no longer in history");
|
|
6007
|
-
this.history[idx] = {
|
|
6008
|
-
...this.history[idx],
|
|
6009
|
-
externalCostUsd: (this.history[idx].externalCostUsd ?? 0) + costUsd
|
|
6010
|
-
};
|
|
6011
|
-
recordMetaEvent(this.sessionKey, "interaction.external-cost", { interactionId, costUsd });
|
|
6012
|
-
this.dispatchEvent(
|
|
6013
|
-
new CustomEvent("external-cost-recorded", {
|
|
6014
|
-
detail: { interactionId, costUsd }
|
|
6015
|
-
})
|
|
6016
|
-
);
|
|
6017
|
-
return true;
|
|
6171
|
+
return this.interactions.recordExternalCost(interactionId, costUsd);
|
|
6018
6172
|
}
|
|
6019
6173
|
/**
|
|
6020
6174
|
* Resolve a pending interaction. The wrapper component calls this on completion.
|
|
6021
6175
|
* Marks the interaction message as resolved so it renders read-only on re-render.
|
|
6022
6176
|
*/
|
|
6023
6177
|
resolveInteraction(interactionId, result) {
|
|
6024
|
-
|
|
6025
|
-
if (interaction) {
|
|
6026
|
-
if (interaction.timeoutHandle) clearTimeout(interaction.timeoutHandle);
|
|
6027
|
-
const harvestedDiagnostics = result?.diagnostics;
|
|
6028
|
-
if (Array.isArray(harvestedDiagnostics) && harvestedDiagnostics.length) {
|
|
6029
|
-
const batch = this.externalDiagnosticsBatches += 1;
|
|
6030
|
-
const externalEvents = [];
|
|
6031
|
-
let turnCount = 0;
|
|
6032
|
-
let messageCount = 0;
|
|
6033
|
-
for (const raw of harvestedDiagnostics) {
|
|
6034
|
-
if (!raw || typeof raw !== "object" || typeof raw.kind !== "string") {
|
|
6035
|
-
continue;
|
|
6036
|
-
}
|
|
6037
|
-
const entry = raw;
|
|
6038
|
-
if (entry.kind === "event") {
|
|
6039
|
-
const { kind: _kind, ...event } = entry;
|
|
6040
|
-
externalEvents.push(event);
|
|
6041
|
-
} else if (entry.kind === "turn") {
|
|
6042
|
-
this.externalDiagnostics.push({
|
|
6043
|
-
...entry,
|
|
6044
|
-
turnIndex: `server-generation.${batch}:${turnCount}`
|
|
6045
|
-
});
|
|
6046
|
-
turnCount += 1;
|
|
6047
|
-
} else if (entry.kind === "message") {
|
|
6048
|
-
messageCount += 1;
|
|
6049
|
-
this.externalDiagnostics.push(entry);
|
|
6050
|
-
}
|
|
6051
|
-
}
|
|
6052
|
-
if (externalEvents.length) mergeMetaEvents(this.sessionKey, externalEvents);
|
|
6053
|
-
recordMetaEvent(this.sessionKey, "external-diagnostics.folded", {
|
|
6054
|
-
interactionId,
|
|
6055
|
-
batch,
|
|
6056
|
-
counts: { turn: turnCount, event: externalEvents.length, message: messageCount }
|
|
6057
|
-
});
|
|
6058
|
-
}
|
|
6059
|
-
const idx = this.history.findIndex((m) => m.interaction?.interactionId === interactionId);
|
|
6060
|
-
if (idx !== -1) {
|
|
6061
|
-
const costUsd = typeof result?.costUsd === "number" && Number.isFinite(result.costUsd) && result.costUsd > 0 ? result.costUsd : void 0;
|
|
6062
|
-
this.history[idx] = {
|
|
6063
|
-
...this.history[idx],
|
|
6064
|
-
interaction: { ...this.history[idx].interaction, resolved: result },
|
|
6065
|
-
...costUsd != null ? { externalCostUsd: (this.history[idx].externalCostUsd ?? 0) + costUsd } : {}
|
|
6066
|
-
};
|
|
6067
|
-
this.dispatchEvent(
|
|
6068
|
-
new CustomEvent("history-updated", {
|
|
6069
|
-
detail: this.history
|
|
6070
|
-
})
|
|
6071
|
-
);
|
|
6072
|
-
}
|
|
6073
|
-
if (typeof result?.userMessage === "string" && result.userMessage.trim()) {
|
|
6074
|
-
this.appendToHistory({ role: "synthetic-user", content: result.userMessage });
|
|
6075
|
-
}
|
|
6076
|
-
if (interaction.overrideId) {
|
|
6077
|
-
this.dispatchEvent(new CustomEvent("interaction-stop", { detail: { interactionId } }));
|
|
6078
|
-
}
|
|
6079
|
-
recordMetaEvent(this.sessionKey, "interaction.resolved", { interactionId });
|
|
6080
|
-
this.activityBus.publish("interaction-resolved", void 0);
|
|
6081
|
-
interaction.resolve(result);
|
|
6082
|
-
this.pendingInteractions.delete(interactionId);
|
|
6083
|
-
this.interactionContexts.get(interactionId)?.dispose();
|
|
6084
|
-
this.interactionContexts.delete(interactionId);
|
|
6085
|
-
} else {
|
|
6086
|
-
logger2.warn(`Interaction with ID ${interactionId} not found.`);
|
|
6087
|
-
}
|
|
6178
|
+
this.interactions.resolve(interactionId, result);
|
|
6088
6179
|
}
|
|
6089
6180
|
/**
|
|
6090
6181
|
* The live {@link InteractionContext} for a pending interaction (GENC-1390), or
|
|
@@ -6092,23 +6183,17 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6092
6183
|
* host) to build the widget's pending `interaction` descriptor.
|
|
6093
6184
|
*/
|
|
6094
6185
|
getInteractionContext(interactionId) {
|
|
6095
|
-
return this.
|
|
6186
|
+
return this.interactions.context(interactionId);
|
|
6096
6187
|
}
|
|
6097
6188
|
/**
|
|
6098
|
-
*
|
|
6099
|
-
*
|
|
6100
|
-
* the widget is parked in *this* driver's map (interactions resolve on the
|
|
6101
|
-
* host, not the child), so cancelling it here unblocks the sub-agent handler
|
|
6102
|
-
* awaiting it. The "one interaction at a time" invariant means there is at
|
|
6103
|
-
* most one.
|
|
6189
|
+
* Replace the history message owning `interactionId`. `false` when it is no longer in history.
|
|
6190
|
+
* Does not announce the change — the interaction paths differ on whether they should.
|
|
6104
6191
|
*/
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
});
|
|
6111
|
-
}
|
|
6192
|
+
patchInteractionMessage(interactionId, patch) {
|
|
6193
|
+
const idx = this.history.findIndex((m) => m.interaction?.interactionId === interactionId);
|
|
6194
|
+
if (idx === -1) return false;
|
|
6195
|
+
this.history[idx] = patch(this.history[idx]);
|
|
6196
|
+
return true;
|
|
6112
6197
|
}
|
|
6113
6198
|
/**
|
|
6114
6199
|
* Seeds the driver with a prior conversation history so a new component instance
|
|
@@ -6116,11 +6201,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6116
6201
|
*/
|
|
6117
6202
|
loadHistory(messages) {
|
|
6118
6203
|
this.history = [...messages];
|
|
6119
|
-
this.
|
|
6120
|
-
new CustomEvent("history-updated", {
|
|
6121
|
-
detail: this.history
|
|
6122
|
-
})
|
|
6123
|
-
);
|
|
6204
|
+
this.notifyHistoryUpdated();
|
|
6124
6205
|
}
|
|
6125
6206
|
async sendMessage(userInput, attachments) {
|
|
6126
6207
|
if (this.busy || !userInput.trim() && !attachments?.length) return { reason: "done" };
|
|
@@ -6340,7 +6421,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6340
6421
|
if (winner === "timeout") {
|
|
6341
6422
|
timedOut = true;
|
|
6342
6423
|
child.dispose();
|
|
6343
|
-
this.
|
|
6424
|
+
this.interactions.cancelAll(`Sub-agent "${name}" timed out after ${timeoutMs}ms.`);
|
|
6344
6425
|
await runPromise.catch(() => void 0);
|
|
6345
6426
|
}
|
|
6346
6427
|
} finally {
|
|
@@ -6353,7 +6434,7 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6353
6434
|
child.removeEventListener("provider-changed", forwardProviderChanged);
|
|
6354
6435
|
child.dispose();
|
|
6355
6436
|
this.dispatchEvent(new CustomEvent("sub-agent-stop", { detail: lifecycleDetail }));
|
|
6356
|
-
this.
|
|
6437
|
+
this.turnRecorder.forwardSubAgent(child.getTurnSnapshots());
|
|
6357
6438
|
const parentName = this.activeAgentName ?? "?";
|
|
6358
6439
|
const harvested = getMetaEvents(childSessionKey).filter((e) => HARVESTED_SUBAGENT_EVENTS.has(e.type)).map((e) => {
|
|
6359
6440
|
const existing = e.detail?.subAgent;
|
|
@@ -6441,7 +6522,10 @@ var ChatDriver = class _ChatDriver extends EventTarget {
|
|
|
6441
6522
|
// ---------------------------------------------------------------------------
|
|
6442
6523
|
// Tool loop
|
|
6443
6524
|
// ---------------------------------------------------------------------------
|
|
6444
|
-
//
|
|
6525
|
+
// Over both ceilings deliberately: the per-iteration phases share a dozen accumulators, so
|
|
6526
|
+
// splitting threads that state through signatures rather than removing it. `turn-setup.ts`
|
|
6527
|
+
// (docs/code-health.md §11.3) is the intended carve-up.
|
|
6528
|
+
// oxlint-disable-next-line complexity, max-lines-per-function
|
|
6445
6529
|
async runToolLoop(userInput, attachments, transientPrimer) {
|
|
6446
6530
|
if (!this.systemPrompt) {
|
|
6447
6531
|
logger2.warn(
|
|
@@ -6597,12 +6681,15 @@ ${EMPTY_RESPONSE_NUDGE}` : "";
|
|
|
6597
6681
|
${tailBody}
|
|
6598
6682
|
</system-reminder>` : void 0;
|
|
6599
6683
|
const effectiveToolChoice = resolvedToolChoice ?? (this.isSubAgent ? "required" : void 0);
|
|
6600
|
-
const turnSnapshot = this.
|
|
6684
|
+
const turnSnapshot = this.turnRecorder.record({
|
|
6685
|
+
agentName: this.activeAgentName,
|
|
6686
|
+
agentLabel: this.activeAgentLabel,
|
|
6601
6687
|
systemPrompt,
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
|
|
6605
|
-
|
|
6688
|
+
tailContext,
|
|
6689
|
+
toolNames: this.toolDefinitions.map((t) => t.name),
|
|
6690
|
+
temperature: resolvedTemperature,
|
|
6691
|
+
toolChoice: effectiveToolChoice
|
|
6692
|
+
});
|
|
6606
6693
|
const userInputForCall = currentInput;
|
|
6607
6694
|
const attachmentsForCall = currentAttachments;
|
|
6608
6695
|
currentInput = "";
|
|
@@ -7104,11 +7191,7 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
|
|
|
7104
7191
|
};
|
|
7105
7192
|
});
|
|
7106
7193
|
this.history[tcMsgIdx] = { ...tcMsg, toolCalls: annotatedCalls };
|
|
7107
|
-
this.
|
|
7108
|
-
new CustomEvent("history-updated", {
|
|
7109
|
-
detail: this.history
|
|
7110
|
-
})
|
|
7111
|
-
);
|
|
7194
|
+
this.notifyHistoryUpdated();
|
|
7112
7195
|
}
|
|
7113
7196
|
if (hitUnknownToolLimit) {
|
|
7114
7197
|
logger2.error(
|
|
@@ -7191,6 +7274,10 @@ RECOVERY: this tool failed once \u2014 you may retry it, or take a different val
|
|
|
7191
7274
|
} : {}
|
|
7192
7275
|
};
|
|
7193
7276
|
this.history = [...this.history, tagged];
|
|
7277
|
+
this.notifyHistoryUpdated();
|
|
7278
|
+
}
|
|
7279
|
+
/** Announce the current history to the host. */
|
|
7280
|
+
notifyHistoryUpdated() {
|
|
7194
7281
|
this.dispatchEvent(
|
|
7195
7282
|
new CustomEvent("history-updated", {
|
|
7196
7283
|
detail: this.history
|