@aparte/core 0.25.0 → 0.26.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/client/agent-runner.d.ts.map +1 -1
- package/dist/client/aparte-client.d.ts +35 -76
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/client/wire-history.d.ts +78 -0
- package/dist/client/wire-history.d.ts.map +1 -1
- package/dist/components/bubble/aparte-chat-bubble.d.ts +88 -2
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/bubble/bubble-sync.d.ts +5 -1
- package/dist/components/bubble/bubble-sync.d.ts.map +1 -1
- package/dist/components/bubble/checkpoint-separator.d.ts +40 -0
- package/dist/components/bubble/checkpoint-separator.d.ts.map +1 -0
- package/dist/components/context/aparte-context.d.ts +157 -25
- package/dist/components/context/aparte-context.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +7 -0
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/aparte-config.d.ts +17 -0
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/locale.d.ts +62 -4
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/context/checkpoint-state.d.ts +70 -0
- package/dist/context/checkpoint-state.d.ts.map +1 -0
- package/dist/context/checkpoint.d.ts +28 -0
- package/dist/context/checkpoint.d.ts.map +1 -0
- package/dist/context/measure.d.ts +148 -0
- package/dist/context/measure.d.ts.map +1 -0
- package/dist/context/restore.d.ts +24 -0
- package/dist/context/restore.d.ts.map +1 -0
- package/dist/conversations/conversation-controller.d.ts +13 -4
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/conversation-manager.d.ts +6 -0
- package/dist/conversations/conversation-manager.d.ts.map +1 -1
- package/dist/custom-elements.json +13734 -12139
- package/dist/{defer-hydration-BwMqeRHq.js → defer-hydration-8vI8xNNx.js} +683 -250
- package/dist/defer-hydration-8vI8xNNx.js.map +1 -0
- package/dist/generated/element-attributes.d.ts +8 -2
- package/dist/generated/element-attributes.d.ts.map +1 -1
- package/dist/host/aparte-chat-host.d.ts +19 -2
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +819 -81
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +4 -1
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +33 -29
- package/dist/index.node.js.map +1 -1
- package/dist/transport/agent-handler.d.ts.map +1 -1
- package/dist/transport/backend-handler.d.ts.map +1 -1
- package/dist/transport/types.d.ts +24 -4
- package/dist/transport/types.d.ts.map +1 -1
- package/dist/types/chat.d.ts +16 -1
- package/dist/types/chat.d.ts.map +1 -1
- package/dist/types/compactor.d.ts +104 -0
- package/dist/types/compactor.d.ts.map +1 -0
- package/dist/types/errors.d.ts +2 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/events.d.ts +50 -3
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/model-provider.d.ts +3 -0
- package/dist/types/model-provider.d.ts.map +1 -1
- package/dist/types/models.d.ts +43 -3
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +1 -1
- package/package.json +2 -2
- package/dist/defer-hydration-BwMqeRHq.js.map +0 -1
|
@@ -58,9 +58,14 @@ class AparteError extends Error {
|
|
|
58
58
|
if (error instanceof AparteError) return error;
|
|
59
59
|
const message = error instanceof Error ? error.message : String(error);
|
|
60
60
|
const status = error?.status || error?.statusCode || defaultStatus;
|
|
61
|
-
const
|
|
61
|
+
const carried = error?.code;
|
|
62
|
+
const code = defaultCode !== "UNKNOWN_ERROR" ? defaultCode : AparteError.carriedCode(carried) ?? AparteError.codeForStatus(status) ?? AparteError.codeForFailure(error) ?? defaultCode;
|
|
62
63
|
return new AparteError(message, code, void 0, error, status);
|
|
63
64
|
}
|
|
65
|
+
/** The value as an {@link AparteErrorCode}, when it is one — a Node `ECONNRESET` or a vendor's own string is not. */
|
|
66
|
+
static carriedCode(value) {
|
|
67
|
+
return typeof value === "string" && Object.values(AparteErrorCode).includes(value) ? value : void 0;
|
|
68
|
+
}
|
|
64
69
|
/**
|
|
65
70
|
* What a thrown, statusless error says about itself. `fetch` rejects a network
|
|
66
71
|
* failure with a TypeError whose message names the fetch ("Failed to fetch",
|
|
@@ -104,7 +109,26 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
104
109
|
scrollToBottom: "Scroll to bottom",
|
|
105
110
|
suggestionsLabel: "Suggested prompts",
|
|
106
111
|
contextLabel: "Context window",
|
|
112
|
+
contextSystem: "System prompt",
|
|
113
|
+
contextTools: "Tools",
|
|
114
|
+
contextSummary: "Summary",
|
|
115
|
+
contextMessages: "Messages",
|
|
116
|
+
contextOther: "Other",
|
|
117
|
+
contextFree: "Free",
|
|
118
|
+
contextTotal: "Total",
|
|
119
|
+
contextReclaim: "Compacting now would free ~{n} tokens",
|
|
120
|
+
contextCompactNow: "Compact now",
|
|
107
121
|
compactionSummaryTitle: "Conversation summary",
|
|
122
|
+
checkpointCount: "{n} messages summarised for the model",
|
|
123
|
+
checkpointCountOne: "1 message summarised for the model",
|
|
124
|
+
checkpointLegacy: "Earlier messages were summarised",
|
|
125
|
+
checkpointGroup: "Context summary",
|
|
126
|
+
checkpointShow: "Show summary",
|
|
127
|
+
checkpointHide: "Hide summary",
|
|
128
|
+
checkpointRestore: "Restore",
|
|
129
|
+
checkpointRestoreConfirm: "Send the original messages to the model again?",
|
|
130
|
+
checkpointPending: "Summarising the context…",
|
|
131
|
+
checkpointFailed: "Couldn't summarise the context",
|
|
108
132
|
toolRunning: "Running",
|
|
109
133
|
toolCompleted: "Done",
|
|
110
134
|
toolRejected: "Rejected",
|
|
@@ -143,6 +167,7 @@ const APARTE_DEFAULT_LOCALE = {
|
|
|
143
167
|
generating: "Generating…",
|
|
144
168
|
rebuildingPreview: "Rebuilding preview…",
|
|
145
169
|
typing: "Typing",
|
|
170
|
+
responseCutOff: "Cut off at the model's length limit.",
|
|
146
171
|
error: "Error",
|
|
147
172
|
running: "Running...",
|
|
148
173
|
run: "Run",
|
|
@@ -199,15 +224,19 @@ function readAuth(auth) {
|
|
|
199
224
|
if (auth && typeof auth === "object") return { key: auth["apiKey"], endpoint: auth["endpoint"] };
|
|
200
225
|
return {};
|
|
201
226
|
}
|
|
202
|
-
|
|
203
|
-
const body = await response.json().catch(() => ({}));
|
|
227
|
+
function messageOf(body, status, label) {
|
|
204
228
|
const raw = body?.error;
|
|
205
229
|
const msg = typeof raw === "object" ? raw?.message : raw;
|
|
206
|
-
return typeof msg === "string" && msg ? msg : `${label} ${
|
|
230
|
+
return typeof msg === "string" && msg ? msg : `${label} ${status}`;
|
|
231
|
+
}
|
|
232
|
+
function isAparteErrorCode(value) {
|
|
233
|
+
return typeof value === "string" && Object.values(AparteErrorCode).includes(value);
|
|
207
234
|
}
|
|
208
|
-
async function vendorError(response, label = "HTTP") {
|
|
209
|
-
const
|
|
210
|
-
const
|
|
235
|
+
async function vendorError(response, label = "HTTP", adapter) {
|
|
236
|
+
const body = await response.json().catch(() => void 0);
|
|
237
|
+
const message = messageOf(body, response.status, label);
|
|
238
|
+
const forwarded = body?.error?.code;
|
|
239
|
+
const code = adapter?.classifyError?.(response.status, body) ?? (isAparteErrorCode(forwarded) ? forwarded : void 0) ?? AparteError.codeForStatus(response.status) ?? AparteErrorCode.UNKNOWN_ERROR;
|
|
211
240
|
return new AparteError(message, code, void 0, void 0, response.status);
|
|
212
241
|
}
|
|
213
242
|
async function parseNonStreamText(provider, response, providerId) {
|
|
@@ -250,7 +279,7 @@ class AparteDirectTransport {
|
|
|
250
279
|
signal: ctx.signal
|
|
251
280
|
});
|
|
252
281
|
if (!response.ok) {
|
|
253
|
-
throw await vendorError(response);
|
|
282
|
+
throw await vendorError(response, "HTTP", provider);
|
|
254
283
|
}
|
|
255
284
|
if (request.stream === false) {
|
|
256
285
|
return parseNonStreamText(provider, response, ctx.providerId);
|
|
@@ -878,12 +907,18 @@ function createAparteChatHandler(options) {
|
|
|
878
907
|
const raw = await vendor.text().catch(() => "");
|
|
879
908
|
let code;
|
|
880
909
|
let type;
|
|
910
|
+
let body;
|
|
881
911
|
try {
|
|
882
|
-
|
|
912
|
+
body = JSON.parse(raw);
|
|
913
|
+
const parsed = body;
|
|
883
914
|
code = parsed?.error?.code;
|
|
884
915
|
type = parsed?.error?.type;
|
|
885
916
|
} catch {
|
|
886
917
|
}
|
|
918
|
+
try {
|
|
919
|
+
code = adapter.classifyError?.(vendor.status, body) ?? code;
|
|
920
|
+
} catch {
|
|
921
|
+
}
|
|
887
922
|
return new Response(
|
|
888
923
|
JSON.stringify({
|
|
889
924
|
error: {
|
|
@@ -897,7 +932,12 @@ function createAparteChatHandler(options) {
|
|
|
897
932
|
}
|
|
898
933
|
if (request.stream === false) {
|
|
899
934
|
const json = await vendor.json().catch(() => ({}));
|
|
900
|
-
|
|
935
|
+
let text;
|
|
936
|
+
try {
|
|
937
|
+
text = typeof adapter.parseText === "function" ? adapter.parseText(json) : "";
|
|
938
|
+
} catch {
|
|
939
|
+
return jsonError$1(502, "The vendor answered with an error.");
|
|
940
|
+
}
|
|
901
941
|
return new Response(JSON.stringify({ text }), {
|
|
902
942
|
headers: { "Content-Type": "application/json" }
|
|
903
943
|
});
|
|
@@ -915,7 +955,7 @@ function createAparteChatHandler(options) {
|
|
|
915
955
|
controller.close();
|
|
916
956
|
return;
|
|
917
957
|
}
|
|
918
|
-
controller.enqueue(encoder2.encode(JSON.stringify(value) + "\n"));
|
|
958
|
+
controller.enqueue(encoder2.encode(JSON.stringify(relayable(value)) + "\n"));
|
|
919
959
|
} catch (err) {
|
|
920
960
|
controller.error(err);
|
|
921
961
|
}
|
|
@@ -927,6 +967,13 @@ function createAparteChatHandler(options) {
|
|
|
927
967
|
return new Response(ndjson, { headers: { "Content-Type": "application/x-ndjson" } });
|
|
928
968
|
};
|
|
929
969
|
}
|
|
970
|
+
function relayable(event) {
|
|
971
|
+
const e = event;
|
|
972
|
+
if (e?.type !== "error") return event;
|
|
973
|
+
console.error("[aparte] the vendor streamed an error", event);
|
|
974
|
+
const code = typeof e.code === "string" && Object.values(AparteErrorCode).includes(e.code) ? e.code : void 0;
|
|
975
|
+
return { type: "error", message: "The vendor reported an error.", ...code ? { code } : {} };
|
|
976
|
+
}
|
|
930
977
|
function jsonError$1(status, message) {
|
|
931
978
|
return new Response(JSON.stringify({ error: { message } }), {
|
|
932
979
|
status,
|
|
@@ -1248,7 +1295,10 @@ function createStreamAdapter(opts) {
|
|
|
1248
1295
|
break;
|
|
1249
1296
|
}
|
|
1250
1297
|
case "run-done":
|
|
1251
|
-
target.updateMessage?.(messageId, {
|
|
1298
|
+
target.updateMessage?.(messageId, {
|
|
1299
|
+
status: "completed",
|
|
1300
|
+
...e.finishReason ? { finishReason: e.finishReason } : {}
|
|
1301
|
+
});
|
|
1252
1302
|
if (e.usage) {
|
|
1253
1303
|
try {
|
|
1254
1304
|
target.setUsage?.(messageId, e.usage);
|
|
@@ -1519,10 +1569,11 @@ function createAparteAgentHandler(options) {
|
|
|
1519
1569
|
console.error("[aparte] agent run failed", error);
|
|
1520
1570
|
await pendingWrite?.catch(() => {
|
|
1521
1571
|
});
|
|
1572
|
+
const named = AparteError.from(error).code;
|
|
1522
1573
|
write({
|
|
1523
1574
|
type: "run-error",
|
|
1524
1575
|
message: "The agent run failed.",
|
|
1525
|
-
code:
|
|
1576
|
+
code: named === AparteErrorCode.UNKNOWN_ERROR ? "AGENT_RUN_FAILED" : named
|
|
1526
1577
|
});
|
|
1527
1578
|
close();
|
|
1528
1579
|
});
|
|
@@ -2023,6 +2074,8 @@ class AparteConfig {
|
|
|
2023
2074
|
_modelPreferenceProvider;
|
|
2024
2075
|
// Per-call approval policy (a mode: plan / ask / auto). `null` = the tools' own flags.
|
|
2025
2076
|
_approvalPolicy = null;
|
|
2077
|
+
// What makes the context smaller — `@aparte/plugin-compaction` registers itself here. `null` = nothing does.
|
|
2078
|
+
_compactor = null;
|
|
2026
2079
|
// Conversation persistence (optional, agnostic)
|
|
2027
2080
|
_conversationManager;
|
|
2028
2081
|
/*
|
|
@@ -2513,7 +2566,11 @@ class AparteConfig {
|
|
|
2513
2566
|
try {
|
|
2514
2567
|
const auth = await this.getKey(providerId);
|
|
2515
2568
|
const models = await provider.fetchModels(auth);
|
|
2569
|
+
const held = this._fetchedModels.get(providerId);
|
|
2570
|
+
if (models.length === 0 && held?.length) return held;
|
|
2571
|
+
const windowBefore = this.getCurrentModel()?.contextWindow;
|
|
2516
2572
|
this._fetchedModels.set(providerId, models);
|
|
2573
|
+
if (this.getCurrentModel()?.contextWindow !== windowBefore) this._notify();
|
|
2517
2574
|
return models;
|
|
2518
2575
|
} catch (error) {
|
|
2519
2576
|
console.warn(`[AparteConfig] Failed to refresh models for ${providerId}`, error);
|
|
@@ -2637,6 +2694,26 @@ class AparteConfig {
|
|
|
2637
2694
|
getApprovalPolicy() {
|
|
2638
2695
|
return this._approvalPolicy;
|
|
2639
2696
|
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Register what makes the context smaller: the gauge's Compact now, a separator's
|
|
2699
|
+
* Retry and the client's pre-flight and overflow retry call it. `null` removes it.
|
|
2700
|
+
*
|
|
2701
|
+
* `@aparte/plugin-compaction`'s `setupCompaction()` registers itself here; core never
|
|
2702
|
+
* compacts on its own, so a page without a compactor has a gauge that measures and a
|
|
2703
|
+
* separator whose Retry falls back to the `aparte-compact` event. A chat reads the one
|
|
2704
|
+
* on ITS config first (`resolveConfig(chat).getCompactor()`), then the client's.
|
|
2705
|
+
*
|
|
2706
|
+
* Notifies like every other registration: a mounted gauge has to hear that "Compact
|
|
2707
|
+
* now" became possible after it was drawn, or went away.
|
|
2708
|
+
*/
|
|
2709
|
+
setCompactor(compactor) {
|
|
2710
|
+
this._compactor = compactor;
|
|
2711
|
+
this._notify();
|
|
2712
|
+
}
|
|
2713
|
+
/** The registered compactor, or `null`. */
|
|
2714
|
+
getCompactor() {
|
|
2715
|
+
return this._compactor;
|
|
2716
|
+
}
|
|
2640
2717
|
/**
|
|
2641
2718
|
* What the policy says about one call, with the tool's own `needsApproval` as the
|
|
2642
2719
|
* answer when it has no opinion. The one place the two are combined, so the client's
|
|
@@ -3377,6 +3454,7 @@ class AparteConfig {
|
|
|
3377
3454
|
this._elicitationFieldRenderer = void 0;
|
|
3378
3455
|
this._modelPreferenceProvider = void 0;
|
|
3379
3456
|
this._approvalPolicy = null;
|
|
3457
|
+
this._compactor = null;
|
|
3380
3458
|
this._bubbleActionsConfig = { ...APARTE_DEFAULT_BUBBLE_ACTIONS };
|
|
3381
3459
|
this._hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
|
|
3382
3460
|
this._notify();
|
|
@@ -4520,13 +4598,25 @@ function populateBubbleFromMessage(bubble, message, siblingInfo) {
|
|
|
4520
4598
|
if (message.usage !== void 0) {
|
|
4521
4599
|
bubble.setUsage?.(message.usage);
|
|
4522
4600
|
}
|
|
4601
|
+
bubble.setFinishReason?.(message.finishReason);
|
|
4602
|
+
bubble.setContextSummary?.(message.contextSummary, { legacy: message.compaction === true });
|
|
4523
4603
|
}
|
|
4524
4604
|
const APARTE_CONVERSATION_SCHEMA_VERSION = 2;
|
|
4525
4605
|
const APARTE_DERIVED_SEGMENTS = Symbol.for("aparte.derivedSegments");
|
|
4526
4606
|
function applyRetention(messages, tree, maxMessages) {
|
|
4527
4607
|
if (maxMessages <= 0 || messages.length <= maxMessages) return { messages, tree };
|
|
4528
|
-
const
|
|
4529
|
-
const
|
|
4608
|
+
const cutIndex = messages.length - maxMessages;
|
|
4609
|
+
const trimmedFlat = messages.slice(cutIndex);
|
|
4610
|
+
const first = trimmedFlat[0];
|
|
4611
|
+
const cutoffId = first?.id;
|
|
4612
|
+
let inherited;
|
|
4613
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
4614
|
+
const own = messages[i].contextSummary;
|
|
4615
|
+
if (!own) continue;
|
|
4616
|
+
if (i < cutIndex) inherited = { ...own, count: own.count + (cutIndex - i) };
|
|
4617
|
+
break;
|
|
4618
|
+
}
|
|
4619
|
+
if (first && inherited) trimmedFlat[0] = { ...first, contextSummary: inherited };
|
|
4530
4620
|
if (!cutoffId || !tree) return { messages: trimmedFlat, tree };
|
|
4531
4621
|
const treeIds = new Set(tree.messages.map((m) => m.message.id));
|
|
4532
4622
|
if (!treeIds.has(cutoffId)) return { messages: trimmedFlat, tree };
|
|
@@ -4548,7 +4638,7 @@ function applyRetention(messages, tree, maxMessages) {
|
|
|
4548
4638
|
}
|
|
4549
4639
|
const trimmedTree = {
|
|
4550
4640
|
headId: tree.headId,
|
|
4551
|
-
messages: tree.messages.filter((m) => keep.has(m.message.id)).map((m) => m.message.id === cutoffId ? { message: m.message, parentId: null } : m)
|
|
4641
|
+
messages: tree.messages.filter((m) => keep.has(m.message.id)).map((m) => m.message.id === cutoffId ? { message: inherited ? { ...m.message, contextSummary: inherited } : m.message, parentId: null } : m)
|
|
4552
4642
|
};
|
|
4553
4643
|
return { messages: trimmedFlat, tree: trimmedTree };
|
|
4554
4644
|
}
|
|
@@ -4579,6 +4669,7 @@ function treeContentOf(tree) {
|
|
|
4579
4669
|
const byId = [...tree.messages].sort((a, b) => a.message.id < b.message.id ? -1 : a.message.id > b.message.id ? 1 : 0);
|
|
4580
4670
|
return stableStringify(byId.map((entry) => ({ parentId: entry.parentId, message: asContent(entry.message) })));
|
|
4581
4671
|
}
|
|
4672
|
+
const isTitleSource = (m) => m.role === "user" && !m.compaction;
|
|
4582
4673
|
class AparteConversationManager {
|
|
4583
4674
|
_adapter;
|
|
4584
4675
|
_conversations = [];
|
|
@@ -4787,7 +4878,7 @@ class AparteConversationManager {
|
|
|
4787
4878
|
async _append(convId, msg) {
|
|
4788
4879
|
const conv = await this._full(convId);
|
|
4789
4880
|
if (!conv) return;
|
|
4790
|
-
const isFirstUserMsg = msg.role === "user" && conv.messages.every((m) => m.role !== "user");
|
|
4881
|
+
const isFirstUserMsg = msg.role === "user" && !msg.compaction && conv.messages.every((m) => m.role !== "user" || m.compaction);
|
|
4791
4882
|
const title = isFirstUserMsg ? await this._title(msg) : null;
|
|
4792
4883
|
const fresh = this._find(convId);
|
|
4793
4884
|
if (!fresh) return;
|
|
@@ -4815,7 +4906,7 @@ class AparteConversationManager {
|
|
|
4815
4906
|
console.warn(`[AparteConversationManager] updateMessages('${convId}') before its messages were loaded: ignored.`);
|
|
4816
4907
|
return;
|
|
4817
4908
|
}
|
|
4818
|
-
const firstUser = messages.find(
|
|
4909
|
+
const firstUser = messages.find(isTitleSource);
|
|
4819
4910
|
if (this._retention) {
|
|
4820
4911
|
({ messages, tree } = applyRetention(messages, tree, this._retention.maxMessages));
|
|
4821
4912
|
}
|
|
@@ -4836,7 +4927,7 @@ class AparteConversationManager {
|
|
|
4836
4927
|
}
|
|
4837
4928
|
let title;
|
|
4838
4929
|
if (conv.autoTitle) {
|
|
4839
|
-
const before = conv.messages.find(
|
|
4930
|
+
const before = conv.messages.find(isTitleSource);
|
|
4840
4931
|
if (firstUser && (firstUser.content ?? "") !== (before?.content ?? "")) {
|
|
4841
4932
|
title = await this._title(firstUser);
|
|
4842
4933
|
}
|
|
@@ -5209,7 +5300,7 @@ class AparteConversationController {
|
|
|
5209
5300
|
this._onCompactDone = (e) => {
|
|
5210
5301
|
const detail = e.detail;
|
|
5211
5302
|
if (detail?.targetId && detail.targetId !== this._binding.hostId) return;
|
|
5212
|
-
queueMicrotask(() => this._persistOnceSummarised(detail
|
|
5303
|
+
queueMicrotask(() => this._persistOnceSummarised(detail ?? {}, 0));
|
|
5213
5304
|
};
|
|
5214
5305
|
if (typeof window !== "undefined") {
|
|
5215
5306
|
window.addEventListener("aparte-conversation-select", this._onSelectConversation);
|
|
@@ -5414,20 +5505,40 @@ class AparteConversationController {
|
|
|
5414
5505
|
* that was partially written before a switch or component destruction.
|
|
5415
5506
|
*/
|
|
5416
5507
|
/**
|
|
5417
|
-
* Save once the binding holds
|
|
5418
|
-
* a framework store reaches the binding only when that framework renders, so a
|
|
5419
|
-
* the microtask wrote the conversation as it stood before the
|
|
5420
|
-
* run
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5508
|
+
* Save once the binding holds what a compaction (or a Restore) announced. A transcript
|
|
5509
|
+
* kept in a framework store reaches the binding only when that framework renders, so a
|
|
5510
|
+
* save on the microtask wrote the conversation as it stood before the change. Bounded:
|
|
5511
|
+
* a run that changed nothing (skipped), or a binding that never shows the change, is
|
|
5512
|
+
* saved as it is.
|
|
5513
|
+
*
|
|
5514
|
+
* What "holds" means follows what the run said it did:
|
|
5515
|
+
* - a checkpoint — `messageId` names the message carrying it, and it holds once that
|
|
5516
|
+
* message carries a `contextSummary` with its `createdAt` (a summary without the
|
|
5517
|
+
* stamp is a half-written one);
|
|
5518
|
+
* - a Restore — it holds once no message of the path carries a checkpoint any more;
|
|
5519
|
+
* - an older host's summary MESSAGE (`compaction: true`, ending with the summary).
|
|
5520
|
+
* A run that names none of these has nothing to wait for.
|
|
5521
|
+
*/
|
|
5522
|
+
_persistOnceSummarised(detail, waits) {
|
|
5523
|
+
const messages = this._binding.getMessages();
|
|
5524
|
+
let holds = true;
|
|
5525
|
+
if (detail.restored) {
|
|
5526
|
+
holds = !messages.some((m) => m.contextSummary !== void 0);
|
|
5527
|
+
} else if (detail.messageId || detail.summary) {
|
|
5528
|
+
const { messageId, summary } = detail;
|
|
5529
|
+
const checkpointed = !!messageId && messages.some(
|
|
5530
|
+
(m) => m.id === messageId && typeof m.contextSummary?.createdAt === "number"
|
|
5531
|
+
);
|
|
5532
|
+
const summaryMessage = !!summary && messages.some(
|
|
5533
|
+
(m) => m.compaction === true && typeof m.content === "string" && m.content.endsWith(summary)
|
|
5534
|
+
);
|
|
5535
|
+
holds = checkpointed || summaryMessage;
|
|
5536
|
+
}
|
|
5426
5537
|
if (holds || !this._binding.afterRender || waits >= MAX_COMPACTION_SAVE_WAITS) {
|
|
5427
5538
|
this._persistActive();
|
|
5428
5539
|
return;
|
|
5429
5540
|
}
|
|
5430
|
-
this._binding.afterRender(() => this._persistOnceSummarised(
|
|
5541
|
+
this._binding.afterRender(() => this._persistOnceSummarised(detail, waits + 1));
|
|
5431
5542
|
}
|
|
5432
5543
|
_persistActive() {
|
|
5433
5544
|
if (!this._activeId) return;
|
|
@@ -5505,16 +5616,126 @@ function gateGlide(el2) {
|
|
|
5505
5616
|
el2.addEventListener("aparte-send", lift);
|
|
5506
5617
|
return off;
|
|
5507
5618
|
}
|
|
5619
|
+
function chatIdOf(el2) {
|
|
5620
|
+
let node = el2.parentElement;
|
|
5621
|
+
while (node) {
|
|
5622
|
+
const isHost = node.tagName.toLowerCase() === "aparte-chat" || node.hasAttribute("data-aparte-chat");
|
|
5623
|
+
if (isHost && node.id) return node.id;
|
|
5624
|
+
node = node.parentElement;
|
|
5625
|
+
}
|
|
5626
|
+
return void 0;
|
|
5627
|
+
}
|
|
5628
|
+
class CheckpointStateWatcher {
|
|
5629
|
+
constructor(_options) {
|
|
5630
|
+
this._options = _options;
|
|
5631
|
+
}
|
|
5632
|
+
_messageId = null;
|
|
5633
|
+
_state = null;
|
|
5634
|
+
_reason = "";
|
|
5635
|
+
/** The bubble the attributes were last written to, so they can be taken off it even after it left the lookup. */
|
|
5636
|
+
_marked = null;
|
|
5637
|
+
/** Listen on `window` for the three events; returns the function that stops. */
|
|
5638
|
+
start() {
|
|
5639
|
+
const mine = (targetId) => {
|
|
5640
|
+
if (this._options.active && !this._options.active()) return false;
|
|
5641
|
+
return !targetId || targetId === this._options.ownId();
|
|
5642
|
+
};
|
|
5643
|
+
const mineMessage = (messageId) => !!messageId && this._options.owns(messageId);
|
|
5644
|
+
const onStart = (e) => {
|
|
5645
|
+
const detail = e.detail;
|
|
5646
|
+
if (!mine(detail?.targetId) || !mineMessage(detail?.messageId)) return;
|
|
5647
|
+
this.clear();
|
|
5648
|
+
this._set(detail.messageId, "pending", "");
|
|
5649
|
+
};
|
|
5650
|
+
const onDone = (e) => {
|
|
5651
|
+
const detail = e.detail;
|
|
5652
|
+
if (!mine(detail?.targetId) || detail?.skipped) return;
|
|
5653
|
+
if (detail?.messageId && detail.messageId === this._messageId) this.clear();
|
|
5654
|
+
};
|
|
5655
|
+
const onError = (e) => {
|
|
5656
|
+
const detail = e.detail;
|
|
5657
|
+
if (!mine(detail?.targetId) || !mineMessage(detail?.messageId)) return;
|
|
5658
|
+
if (detail.aborted) {
|
|
5659
|
+
if (detail.messageId === this._messageId) this.clear();
|
|
5660
|
+
return;
|
|
5661
|
+
}
|
|
5662
|
+
if (detail.messageId !== this._messageId) this.clear();
|
|
5663
|
+
this._set(detail.messageId, "failed", detail.error ?? "");
|
|
5664
|
+
};
|
|
5665
|
+
window.addEventListener("aparte-compact-start", onStart);
|
|
5666
|
+
window.addEventListener("aparte-compact-done", onDone);
|
|
5667
|
+
window.addEventListener("aparte-compact-error", onError);
|
|
5668
|
+
return () => {
|
|
5669
|
+
window.removeEventListener("aparte-compact-start", onStart);
|
|
5670
|
+
window.removeEventListener("aparte-compact-done", onDone);
|
|
5671
|
+
window.removeEventListener("aparte-compact-error", onError);
|
|
5672
|
+
};
|
|
5673
|
+
}
|
|
5674
|
+
/** Take the state off the bubble and forget it. */
|
|
5675
|
+
clear() {
|
|
5676
|
+
const bubble = this._marked ?? (this._messageId ? this._options.bubbleOf(this._messageId) : null);
|
|
5677
|
+
this._messageId = null;
|
|
5678
|
+
this._state = null;
|
|
5679
|
+
this._reason = "";
|
|
5680
|
+
this._marked = null;
|
|
5681
|
+
bubble?.removeAttribute("data-checkpoint");
|
|
5682
|
+
bubble?.removeAttribute("data-checkpoint-reason");
|
|
5683
|
+
}
|
|
5684
|
+
/** Write the remembered state onto the bubble, which may have been created since. */
|
|
5685
|
+
reapply() {
|
|
5686
|
+
if (!this._messageId || !this._state) return;
|
|
5687
|
+
this._write(this._messageId, this._state, this._reason);
|
|
5688
|
+
}
|
|
5689
|
+
_set(messageId, state, reason) {
|
|
5690
|
+
this._messageId = messageId;
|
|
5691
|
+
this._state = state;
|
|
5692
|
+
this._reason = reason;
|
|
5693
|
+
this._write(messageId, state, reason);
|
|
5694
|
+
}
|
|
5695
|
+
_write(messageId, state, reason) {
|
|
5696
|
+
const bubble = this._options.bubbleOf(messageId);
|
|
5697
|
+
if (!bubble) return;
|
|
5698
|
+
if (this._marked && this._marked !== bubble) {
|
|
5699
|
+
this._marked.removeAttribute("data-checkpoint");
|
|
5700
|
+
this._marked.removeAttribute("data-checkpoint-reason");
|
|
5701
|
+
}
|
|
5702
|
+
this._marked = bubble;
|
|
5703
|
+
if (reason) bubble.setAttribute("data-checkpoint-reason", reason);
|
|
5704
|
+
else bubble.removeAttribute("data-checkpoint-reason");
|
|
5705
|
+
if (bubble.getAttribute("data-checkpoint") !== state) bubble.setAttribute("data-checkpoint", state);
|
|
5706
|
+
}
|
|
5707
|
+
}
|
|
5508
5708
|
const MAX_SIBLING_RETRIES = 6;
|
|
5509
5709
|
const TREE_OWNED_FIELDS = /* @__PURE__ */ new Set(["id", "branches", "activeBranchIndex", "isStreaming"]);
|
|
5510
5710
|
const ALWAYS_PRESENT_FIELDS = /* @__PURE__ */ new Set(["content", "segments", "status"]);
|
|
5711
|
+
const RENDERED_FIELDS = ["content", "segments", "attachments", "usage", "finishReason", "contextSummary", "compaction"];
|
|
5511
5712
|
class AparteChatHost {
|
|
5512
5713
|
binding;
|
|
5513
5714
|
options;
|
|
5514
5715
|
/** Id of the assistant message currently streaming (null when idle). */
|
|
5515
5716
|
_streamingId = null;
|
|
5516
|
-
/**
|
|
5517
|
-
|
|
5717
|
+
/**
|
|
5718
|
+
* The message object each bubble was last drawn from, by id (skip-rerender cache).
|
|
5719
|
+
*
|
|
5720
|
+
* An id alone could not say whether the list had changed under a bubble: a framework
|
|
5721
|
+
* that owns the transcript (a controlled `messages` prop, a v-model, a store behind a
|
|
5722
|
+
* compaction target) never goes through {@link updateMessage}, it hands over a list
|
|
5723
|
+
* with a NEW object for the message it changed, and a checkpoint written that way to a
|
|
5724
|
+
* message that was not the last never reached its bubble. Immutable updates always
|
|
5725
|
+
* produce a new object, so the reference is the signal; the same object again means
|
|
5726
|
+
* nothing changed and the bubble is left alone.
|
|
5727
|
+
*/
|
|
5728
|
+
_renderedIds = /* @__PURE__ */ new Map();
|
|
5729
|
+
/**
|
|
5730
|
+
* Says on the bubble what a compaction is doing to the message it will sit on. The
|
|
5731
|
+
* viewport answers for this in a chat with no framework; here the framework owns the
|
|
5732
|
+
* list and the host is the one that knows which bubble is whose.
|
|
5733
|
+
*/
|
|
5734
|
+
_checkpointState = new CheckpointStateWatcher({
|
|
5735
|
+
ownId: () => this.binding.hostId,
|
|
5736
|
+
bubbleOf: (id) => this._bubbleById(id),
|
|
5737
|
+
owns: (id) => this.binding.getMessages().some((m) => m.id === id)
|
|
5738
|
+
});
|
|
5518
5739
|
/** Sibling info from the last path-change, applied after the next render. */
|
|
5519
5740
|
_pendingSiblings;
|
|
5520
5741
|
/** How many render passes the sibling write has waited for — see `_applyPendingSiblings`. */
|
|
@@ -5600,6 +5821,7 @@ class AparteChatHost {
|
|
|
5600
5821
|
}
|
|
5601
5822
|
this._installHostMethods(host2);
|
|
5602
5823
|
this._installLifecycleListeners(host2);
|
|
5824
|
+
this._teardown.push(this._checkpointState.start());
|
|
5603
5825
|
this._teardown.push(gateGlide(host2));
|
|
5604
5826
|
this._installViewportListeners();
|
|
5605
5827
|
this._createController(host2);
|
|
@@ -5713,6 +5935,7 @@ class AparteChatHost {
|
|
|
5713
5935
|
this._setStreamingId(null);
|
|
5714
5936
|
this.binding.onTypingChange?.(false);
|
|
5715
5937
|
this._renderedIds.clear();
|
|
5938
|
+
this._checkpointState.clear();
|
|
5716
5939
|
this.binding.resetComposer?.();
|
|
5717
5940
|
}
|
|
5718
5941
|
// ── host method surface (also installed on the host element) ───────────
|
|
@@ -5762,6 +5985,13 @@ class AparteChatHost {
|
|
|
5762
5985
|
if (updates.status) {
|
|
5763
5986
|
this._bubbleById(messageId)?.updateMessage?.({ status: updates.status });
|
|
5764
5987
|
}
|
|
5988
|
+
if (index !== msgs.length - 1 && RENDERED_FIELDS.some((key) => key in updates)) {
|
|
5989
|
+
this._renderedIds.delete(messageId);
|
|
5990
|
+
const bubble = this._bubbleById(messageId);
|
|
5991
|
+
if (bubble && ("contextSummary" in updates || "compaction" in updates)) {
|
|
5992
|
+
bubble.setContextSummary?.(merged.contextSummary, { legacy: merged.compaction === true });
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5765
5995
|
if (isTerminalStatus(merged.status) && merged.segments) {
|
|
5766
5996
|
for (const id of openSegmentIds(merged.segments)) {
|
|
5767
5997
|
this.updateSegment(id, { isStreaming: false });
|
|
@@ -6060,18 +6290,20 @@ class AparteChatHost {
|
|
|
6060
6290
|
/**
|
|
6061
6291
|
* Reconcile rendered bubbles with the current message list. The framework
|
|
6062
6292
|
* calls this from its reactive message-change hook after rendering; the
|
|
6063
|
-
* last message is always re-synced (streaming), others
|
|
6293
|
+
* last message is always re-synced (streaming), the others whenever the list
|
|
6294
|
+
* holds a different object for them than the one they were drawn from.
|
|
6064
6295
|
*/
|
|
6065
6296
|
syncBubbles() {
|
|
6066
6297
|
const msgs = this.binding.getMessages();
|
|
6067
6298
|
msgs.forEach((message, index) => {
|
|
6068
6299
|
const isLast = index === msgs.length - 1;
|
|
6069
|
-
if (this._renderedIds.
|
|
6300
|
+
if (this._renderedIds.get(message.id) === message && !isLast) return;
|
|
6070
6301
|
const bubble = this._bubbleById(message.id);
|
|
6071
6302
|
if (!bubble) return;
|
|
6072
6303
|
populateBubbleFromMessage(bubble, message);
|
|
6073
|
-
this._renderedIds.
|
|
6304
|
+
this._renderedIds.set(message.id, message);
|
|
6074
6305
|
});
|
|
6306
|
+
this._checkpointState.reapply();
|
|
6075
6307
|
}
|
|
6076
6308
|
/**
|
|
6077
6309
|
* Rebuild the viewport's internal repo from the message list before branch
|
|
@@ -6190,6 +6422,7 @@ class AparteChatHost {
|
|
|
6190
6422
|
host2["removeSegment"] = (id) => this.removeSegment(id);
|
|
6191
6423
|
host2["appendToSegment"] = (id, c) => this.appendToSegment(id, c);
|
|
6192
6424
|
host2["getMessages"] = () => this.getMessages();
|
|
6425
|
+
host2["getMessage"] = (id) => this.binding.getMessages().find((m) => m.id === id);
|
|
6193
6426
|
host2["addBranch"] = (id) => this.addBranch(id);
|
|
6194
6427
|
host2["addSiblingOf"] = (id, m) => this.addSiblingOf(id, m);
|
|
6195
6428
|
host2["truncateFrom"] = (id) => this.truncateFrom(id);
|
|
@@ -6264,6 +6497,37 @@ class AparteChatHost {
|
|
|
6264
6497
|
});
|
|
6265
6498
|
}
|
|
6266
6499
|
}
|
|
6500
|
+
function historyFromCheckpoint(path) {
|
|
6501
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
6502
|
+
const summary = path[i].contextSummary?.summary;
|
|
6503
|
+
if (summary !== void 0) return { summary, messages: path.slice(i) };
|
|
6504
|
+
}
|
|
6505
|
+
return { messages: path };
|
|
6506
|
+
}
|
|
6507
|
+
const COMPACTION_PREAMBLE = "The earlier part of this conversation was compacted. This is a summary of what came before; continue from it.";
|
|
6508
|
+
const underPreamble = (text) => `${COMPACTION_PREAMBLE}
|
|
6509
|
+
|
|
6510
|
+
${text}`;
|
|
6511
|
+
function mergeAdjacentUserMessages(messages) {
|
|
6512
|
+
const asParts = (c) => typeof c === "string" ? [{ type: "text", text: c }] : c;
|
|
6513
|
+
const out = [];
|
|
6514
|
+
for (const m of messages) {
|
|
6515
|
+
const prev = out[out.length - 1];
|
|
6516
|
+
if (m.role !== "user" || prev?.role !== "user") {
|
|
6517
|
+
out.push(m);
|
|
6518
|
+
} else if (typeof prev.content === "string" && typeof m.content === "string") {
|
|
6519
|
+
out[out.length - 1] = { ...prev, content: `${prev.content}
|
|
6520
|
+
|
|
6521
|
+
${m.content}` };
|
|
6522
|
+
} else {
|
|
6523
|
+
out[out.length - 1] = {
|
|
6524
|
+
...prev,
|
|
6525
|
+
content: [...asParts(prev.content), { type: "text", text: "\n\n" }, ...asParts(m.content)]
|
|
6526
|
+
};
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
return out;
|
|
6530
|
+
}
|
|
6267
6531
|
const STOPPED_TOOL_TEXT = "The tool call was stopped before it ran.";
|
|
6268
6532
|
const EMPTY_TOOL_TEXT = "The tool returned no output.";
|
|
6269
6533
|
function answerOf(segment) {
|
|
@@ -6314,6 +6578,139 @@ function wireTurns(message, textOf, options = {}) {
|
|
|
6314
6578
|
flush();
|
|
6315
6579
|
return out;
|
|
6316
6580
|
}
|
|
6581
|
+
const isInFlight = (m) => m.status === "streaming" || m.status === "pending";
|
|
6582
|
+
function segmentsToWireText(segments) {
|
|
6583
|
+
if (!segments?.length) return "";
|
|
6584
|
+
const fence = "```";
|
|
6585
|
+
const parts = [];
|
|
6586
|
+
for (const segment of segments) {
|
|
6587
|
+
const content = segment.content;
|
|
6588
|
+
if (segment.type === "text") {
|
|
6589
|
+
if (content) parts.push(content);
|
|
6590
|
+
} else if (segment.type === "code") {
|
|
6591
|
+
const lang = segment.language ?? "";
|
|
6592
|
+
parts.push(`${fence}${lang}
|
|
6593
|
+
${content ?? ""}
|
|
6594
|
+
${fence}`);
|
|
6595
|
+
} else if (segment.type === "thinking" || segment.type === "tool_call" || segment.type === "error") {
|
|
6596
|
+
continue;
|
|
6597
|
+
} else if (typeof content === "string" && content) {
|
|
6598
|
+
parts.push(content);
|
|
6599
|
+
} else {
|
|
6600
|
+
const fallback = segment.fallback;
|
|
6601
|
+
if (fallback) parts.push(fallback);
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
6604
|
+
return parts.join("\n").trim();
|
|
6605
|
+
}
|
|
6606
|
+
function extractWireText(message) {
|
|
6607
|
+
const rendered = segmentsToWireText(message.segments);
|
|
6608
|
+
if (rendered) return rendered;
|
|
6609
|
+
if (typeof message.content === "string" && message.content) return message.content;
|
|
6610
|
+
return "";
|
|
6611
|
+
}
|
|
6612
|
+
function messageWireText(message) {
|
|
6613
|
+
const text = extractWireText(message);
|
|
6614
|
+
return message.compaction ? underPreamble(text) : text;
|
|
6615
|
+
}
|
|
6616
|
+
function wireMessages(messages) {
|
|
6617
|
+
return messages.filter((m) => {
|
|
6618
|
+
if (m.role !== "user" && m.role !== "assistant") return false;
|
|
6619
|
+
return m.status !== "error" && !isInFlight(m);
|
|
6620
|
+
}).flatMap((m) => {
|
|
6621
|
+
if (m.role === "assistant" && m.segments?.some((s) => s.type === "tool_call")) {
|
|
6622
|
+
return wireTurns(m, segmentsToWireText);
|
|
6623
|
+
}
|
|
6624
|
+
const content = messageWireText(m);
|
|
6625
|
+
return content ? [{ role: m.role, content }] : [];
|
|
6626
|
+
});
|
|
6627
|
+
}
|
|
6628
|
+
function estimateTokens(text) {
|
|
6629
|
+
if (!text) return 0;
|
|
6630
|
+
return Math.ceil(text.length / 3.8);
|
|
6631
|
+
}
|
|
6632
|
+
const estimateJson = (value) => {
|
|
6633
|
+
if (!value) return 0;
|
|
6634
|
+
try {
|
|
6635
|
+
return estimateTokens(JSON.stringify(value));
|
|
6636
|
+
} catch {
|
|
6637
|
+
return 0;
|
|
6638
|
+
}
|
|
6639
|
+
};
|
|
6640
|
+
function estimateChatTokens(messages) {
|
|
6641
|
+
let total = 0;
|
|
6642
|
+
for (const message of messages) {
|
|
6643
|
+
total += estimateTokens(contentToText(message.content));
|
|
6644
|
+
if (message.toolCalls?.length) total += estimateJson(message.toolCalls);
|
|
6645
|
+
}
|
|
6646
|
+
return total;
|
|
6647
|
+
}
|
|
6648
|
+
function carriesNonTextParts(messages) {
|
|
6649
|
+
return messages.some((m) => typeof m.content !== "string" && m.content.some((p) => p.type !== "text"));
|
|
6650
|
+
}
|
|
6651
|
+
function estimateRequestTokens(request) {
|
|
6652
|
+
const declared = (request.tools ?? []).map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }));
|
|
6653
|
+
return estimateChatTokens(request.messages) + (declared.length ? estimateJson(declared) : 0);
|
|
6654
|
+
}
|
|
6655
|
+
function estimateWireTokens(messages) {
|
|
6656
|
+
return estimateChatTokens(wireMessages(messages));
|
|
6657
|
+
}
|
|
6658
|
+
const usageTokens = (usage) => usage.totalTokens ?? usage.inputTokens + usage.outputTokens;
|
|
6659
|
+
function toolsForCurrentModel(config) {
|
|
6660
|
+
const capabilities = config.getCurrentModel()?.capabilities;
|
|
6661
|
+
const declinesTools = capabilities ? !capabilities.includes("function_calling") : false;
|
|
6662
|
+
return declinesTools ? [] : config.getTools();
|
|
6663
|
+
}
|
|
6664
|
+
function windowOf(config, windowOverride) {
|
|
6665
|
+
if (typeof windowOverride === "number" && Number.isFinite(windowOverride) && windowOverride > 0) return windowOverride;
|
|
6666
|
+
const declared = config.getCurrentModel()?.contextWindow;
|
|
6667
|
+
return declared && declared > 0 ? declared : void 0;
|
|
6668
|
+
}
|
|
6669
|
+
const hasSegment = (message, type) => message.segments?.some((s) => s.type === type) === true;
|
|
6670
|
+
function calibration(reply) {
|
|
6671
|
+
const sent = reply.usage?.inputTokens;
|
|
6672
|
+
const recorded = reply.contextEstimate;
|
|
6673
|
+
if (hasSegment(reply, "tool_call")) return 1;
|
|
6674
|
+
return sent && recorded && sent > 0 && recorded > 0 ? Math.min(3, Math.max(0.5, sent / recorded)) : 1;
|
|
6675
|
+
}
|
|
6676
|
+
function nextRequestTokens(reply, k) {
|
|
6677
|
+
const usage = reply.usage;
|
|
6678
|
+
if (!hasSegment(reply, "thinking") || !(usage.inputTokens > 0)) return usageTokens(usage);
|
|
6679
|
+
const segments = reply.segments ?? [];
|
|
6680
|
+
const lastCall = segments.map((s) => s.type).lastIndexOf("tool_call");
|
|
6681
|
+
const said = Math.round(k * estimateTokens(segmentsToWireText(segments.slice(lastCall + 1))));
|
|
6682
|
+
return usage.inputTokens + (usage.outputTokens > 0 ? Math.min(said, usage.outputTokens) : said);
|
|
6683
|
+
}
|
|
6684
|
+
function measureContext(input) {
|
|
6685
|
+
const { messages, config } = input;
|
|
6686
|
+
const view = historyFromCheckpoint(messages);
|
|
6687
|
+
const system = estimateTokens(config.resolveSystemPrompt()) + estimateTokens(config.resolveToolSystemPrompts());
|
|
6688
|
+
const declared = toolsForCurrentModel(config).map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema }));
|
|
6689
|
+
const tools = declared.length ? estimateJson(declared) : 0;
|
|
6690
|
+
const summary = view.summary === void 0 ? 0 : estimateTokens(underPreamble(view.summary));
|
|
6691
|
+
const parts = { system, tools, summary, messages: estimateWireTokens(view.messages) };
|
|
6692
|
+
const total = system + tools + summary + parts.messages;
|
|
6693
|
+
let replyIndex = -1;
|
|
6694
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
6695
|
+
if (messages[i].role === "assistant" && messages[i].usage) {
|
|
6696
|
+
replyIndex = i;
|
|
6697
|
+
break;
|
|
6698
|
+
}
|
|
6699
|
+
}
|
|
6700
|
+
const reply = replyIndex >= 0 ? messages[replyIndex] : void 0;
|
|
6701
|
+
const window2 = windowOf(config, input.windowOverride);
|
|
6702
|
+
const result = (used, estimated) => ({
|
|
6703
|
+
...window2 !== void 0 ? { window: window2 } : {},
|
|
6704
|
+
used,
|
|
6705
|
+
estimated,
|
|
6706
|
+
parts,
|
|
6707
|
+
other: Math.max(0, used - total)
|
|
6708
|
+
});
|
|
6709
|
+
const unchanged = reply !== void 0 && !input.viewChanged && replyIndex === messages.length - 1 && !messages.some((m) => m.contextSummary !== void 0 && m.contextSummary.createdAt > reply.timestamp);
|
|
6710
|
+
const k = reply ? calibration(reply) : 1;
|
|
6711
|
+
if (reply && unchanged) return result(nextRequestTokens(reply, k), false);
|
|
6712
|
+
return result(Math.round(k * total), true);
|
|
6713
|
+
}
|
|
6317
6714
|
function eventOrigin(e) {
|
|
6318
6715
|
return e.composedPath?.()[0] ?? e.target;
|
|
6319
6716
|
}
|
|
@@ -6850,7 +7247,6 @@ function buildApprovalPanel(message, options, onChange, details) {
|
|
|
6850
7247
|
function requestUserInput(request) {
|
|
6851
7248
|
return resolveConfig(request.target ?? null).requestUserInput(request);
|
|
6852
7249
|
}
|
|
6853
|
-
const COMPACTION_PREAMBLE = "The earlier part of this conversation was compacted. This is a summary of what came before; continue from it.";
|
|
6854
7250
|
const NOT_RUN_AFTER_A_REFUSAL = "Not run: an earlier call of the same step was refused.";
|
|
6855
7251
|
const approvalKey = (call) => JSON.stringify([call.id, call.name, call.input]);
|
|
6856
7252
|
const warnedUninlinedFiles = /* @__PURE__ */ new Set();
|
|
@@ -6879,6 +7275,28 @@ class AparteClient {
|
|
|
6879
7275
|
_boundEditHandler = null;
|
|
6880
7276
|
_releaseKeyResolver = null;
|
|
6881
7277
|
_isAborted = false;
|
|
7278
|
+
/** Provider and model pairs whose window was already read again after a turn (see `_readWindowAfterTurn`). */
|
|
7279
|
+
_windowReadFor = /* @__PURE__ */ new Set();
|
|
7280
|
+
/**
|
|
7281
|
+
* The current model's window, read again once after a turn when it had none.
|
|
7282
|
+
*
|
|
7283
|
+
* A local server that loads a model on its first request — LM Studio's just-in-time
|
|
7284
|
+
* loading, Ollama — has no loaded window to report when the model list is fetched at
|
|
7285
|
+
* start-up, so the model comes without one, and the gauge and the compaction have
|
|
7286
|
+
* nothing to measure against. After a reply the model is loaded, and the provider's
|
|
7287
|
+
* own probe finds it. Once per provider and model: a server that never says costs one
|
|
7288
|
+
* request, not one per turn. The config announces the window when it appears.
|
|
7289
|
+
*/
|
|
7290
|
+
async _readWindowAfterTurn() {
|
|
7291
|
+
const config = this._config;
|
|
7292
|
+
const model = config.getCurrentModel();
|
|
7293
|
+
const providerId = config.getModelConfig().defaultProvider;
|
|
7294
|
+
if (!model || model.contextWindow || !providerId) return;
|
|
7295
|
+
const key = `${providerId}\0${model.id}`;
|
|
7296
|
+
if (this._windowReadFor.has(key)) return;
|
|
7297
|
+
this._windowReadFor.add(key);
|
|
7298
|
+
await config.refreshProviderModels(providerId);
|
|
7299
|
+
}
|
|
6882
7300
|
/**
|
|
6883
7301
|
* Does this window event belong to this client?
|
|
6884
7302
|
*
|
|
@@ -7160,19 +7578,23 @@ class AparteClient {
|
|
|
7160
7578
|
async _streamTurn(targetElement, messageId, provider, messages, modelId, authConfig, opts) {
|
|
7161
7579
|
const chatId = this._targetIdOf(targetElement);
|
|
7162
7580
|
const registeredTools = this._toolsForCurrentModel();
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
7581
|
+
const buildRequest = async (requestMessages2) => {
|
|
7582
|
+
let built = {
|
|
7583
|
+
messages: requestMessages2,
|
|
7584
|
+
modelId,
|
|
7585
|
+
stream: true,
|
|
7586
|
+
tools: registeredTools.length ? registeredTools : void 0,
|
|
7587
|
+
...opts?.temperature !== void 0 ? { temperature: opts.temperature } : {}
|
|
7588
|
+
};
|
|
7589
|
+
if (this.options.requestInterceptor) {
|
|
7590
|
+
built = await this.options.requestInterceptor(built);
|
|
7591
|
+
}
|
|
7592
|
+
if (built.toolChoice === "none") {
|
|
7593
|
+
built = { ...built, tools: void 0 };
|
|
7594
|
+
}
|
|
7595
|
+
return built;
|
|
7169
7596
|
};
|
|
7170
|
-
|
|
7171
|
-
baseRequest = await this.options.requestInterceptor(baseRequest);
|
|
7172
|
-
}
|
|
7173
|
-
if (baseRequest.toolChoice === "none") {
|
|
7174
|
-
baseRequest = { ...baseRequest, tools: void 0 };
|
|
7175
|
-
}
|
|
7597
|
+
let baseRequest = await buildRequest(messages);
|
|
7176
7598
|
if (this._isAborted) {
|
|
7177
7599
|
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
|
|
7178
7600
|
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
@@ -7193,7 +7615,23 @@ class AparteClient {
|
|
|
7193
7615
|
if (this._streamController !== streamController) this._streamController?.abort();
|
|
7194
7616
|
this._streamController = streamController;
|
|
7195
7617
|
this._turnsInFlight.add(messageId);
|
|
7196
|
-
|
|
7618
|
+
const compactor = opts?.rebuild ? resolveConfig(targetElement).getCompactor() ?? this._config.getCompactor() : null;
|
|
7619
|
+
const turnIds = () => {
|
|
7620
|
+
const path = targetElement.getMessages?.() ?? [];
|
|
7621
|
+
const at = path.findIndex((m) => m.id === messageId);
|
|
7622
|
+
const asked = at > 0 ? path[at - 1] : void 0;
|
|
7623
|
+
return asked?.role === "user" ? [asked.id, messageId] : [messageId];
|
|
7624
|
+
};
|
|
7625
|
+
const requestMessages = () => opts?.resume ? this._resumeMessages(targetElement, messageId, baseRequest.messages) : baseRequest.messages;
|
|
7626
|
+
const requestEstimate = () => estimateRequestTokens({ messages: requestMessages(), tools: baseRequest.tools });
|
|
7627
|
+
const recordedEstimate = () => carriesNonTextParts(requestMessages()) ? void 0 : requestEstimate();
|
|
7628
|
+
const endStopped = () => {
|
|
7629
|
+
this._closeOpenCalls(targetElement, messageId);
|
|
7630
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
|
|
7631
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
7632
|
+
};
|
|
7633
|
+
const isStopped = () => this._isAborted || streamController.signal.aborted;
|
|
7634
|
+
const runOnce = async () => {
|
|
7197
7635
|
let result = opts?.resume ? { usage: void 0, suspended: opts.resume } : await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig, streamController);
|
|
7198
7636
|
const maxResumes = this.options.maxTurns ?? 10;
|
|
7199
7637
|
let resumes = 0;
|
|
@@ -7236,21 +7674,80 @@ class AparteClient {
|
|
|
7236
7674
|
);
|
|
7237
7675
|
}
|
|
7238
7676
|
const usage = result.usage;
|
|
7677
|
+
const finishReason = result.finishReason;
|
|
7239
7678
|
if (this._isAborted || streamController.signal.aborted) {
|
|
7240
7679
|
this._closeOpenCalls(targetElement, messageId);
|
|
7241
7680
|
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
7242
7681
|
} else {
|
|
7243
|
-
dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage }, chatId);
|
|
7682
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage, ...finishReason ? { finishReason } : {} }, chatId);
|
|
7683
|
+
void this._readWindowAfterTurn();
|
|
7244
7684
|
}
|
|
7245
|
-
}
|
|
7246
|
-
|
|
7247
|
-
if (
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7685
|
+
};
|
|
7686
|
+
const resendAfterOverflow = async (error) => {
|
|
7687
|
+
if (error.code !== AparteErrorCode.USAGE_CONTEXT_EXCEEDED || !compactor || !opts?.rebuild) return "stands";
|
|
7688
|
+
const segments = targetElement.getMessage?.(messageId)?.segments ?? targetElement.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
|
|
7689
|
+
if (segments.some((s) => s.type === "text" || s.type === "tool_call")) return "stands";
|
|
7690
|
+
try {
|
|
7691
|
+
const outcome = await compactor.compact({
|
|
7692
|
+
target: targetElement,
|
|
7693
|
+
...chatId ? { targetId: chatId } : {},
|
|
7694
|
+
force: true,
|
|
7695
|
+
reason: "overflow",
|
|
7696
|
+
exclude: turnIds(),
|
|
7697
|
+
signal: streamController.signal
|
|
7698
|
+
});
|
|
7699
|
+
if (isStopped()) return "stopped";
|
|
7700
|
+
if (!outcome.ok || outcome.skipped) return "stands";
|
|
7701
|
+
baseRequest = await buildRequest(await opts.rebuild());
|
|
7702
|
+
} catch {
|
|
7703
|
+
return "stands";
|
|
7704
|
+
}
|
|
7705
|
+
this._updateMessage(targetElement, messageId, { status: "pending" });
|
|
7706
|
+
return "resend";
|
|
7707
|
+
};
|
|
7708
|
+
try {
|
|
7709
|
+
for (let attempt = 0; ; attempt++) {
|
|
7710
|
+
try {
|
|
7711
|
+
if (attempt === 0 && compactor?.beforeSend && opts?.rebuild) {
|
|
7712
|
+
let compacted = false;
|
|
7713
|
+
try {
|
|
7714
|
+
compacted = await compactor.beforeSend({
|
|
7715
|
+
target: targetElement,
|
|
7716
|
+
...chatId ? { targetId: chatId } : {},
|
|
7717
|
+
exclude: turnIds(),
|
|
7718
|
+
requestTokens: requestEstimate(),
|
|
7719
|
+
signal: streamController.signal
|
|
7720
|
+
});
|
|
7721
|
+
} catch (failure) {
|
|
7722
|
+
console.warn("[AparteClient] The compactor threw before a send; sending the request as built.", failure);
|
|
7723
|
+
}
|
|
7724
|
+
if (isStopped()) {
|
|
7725
|
+
endStopped();
|
|
7726
|
+
return;
|
|
7727
|
+
}
|
|
7728
|
+
if (compacted) baseRequest = await buildRequest(await opts.rebuild());
|
|
7729
|
+
}
|
|
7730
|
+
this._updateMessage(targetElement, messageId, { contextEstimate: recordedEstimate() });
|
|
7731
|
+
await runOnce();
|
|
7732
|
+
return;
|
|
7733
|
+
} catch (error) {
|
|
7734
|
+
this._closeOpenCalls(targetElement, messageId);
|
|
7735
|
+
if (isStopped()) {
|
|
7736
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId }, chatId);
|
|
7737
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
7738
|
+
return;
|
|
7739
|
+
}
|
|
7740
|
+
const aparteError = AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR);
|
|
7741
|
+
const next = attempt === 0 ? await resendAfterOverflow(aparteError) : "stands";
|
|
7742
|
+
if (next === "resend") continue;
|
|
7743
|
+
if (next === "stopped") {
|
|
7744
|
+
endStopped();
|
|
7745
|
+
return;
|
|
7746
|
+
}
|
|
7747
|
+
this._handleLifecycleError(targetElement, messageId, aparteError);
|
|
7748
|
+
return;
|
|
7749
|
+
}
|
|
7251
7750
|
}
|
|
7252
|
-
const aparteError = AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR);
|
|
7253
|
-
this._handleLifecycleError(targetElement, messageId, aparteError);
|
|
7254
7751
|
} finally {
|
|
7255
7752
|
this._turnsInFlight.delete(messageId);
|
|
7256
7753
|
}
|
|
@@ -7265,29 +7762,12 @@ class AparteClient {
|
|
|
7265
7762
|
* using the same conversation history minus the retried reply.
|
|
7266
7763
|
*/
|
|
7267
7764
|
/**
|
|
7268
|
-
* The registered tools, unless the current model says it cannot call one
|
|
7269
|
-
*
|
|
7270
|
-
*
|
|
7271
|
-
*
|
|
7272
|
-
* The question is "did this model say it cannot", not "did it say it can": the
|
|
7273
|
-
* app sets a model id by hand (`setModelConfig`) or a selector sets it from a
|
|
7274
|
-
* fetched list, and in both cases the model's declared capabilities are commonly
|
|
7275
|
-
* unknown. `undefined` capabilities read as "can", or `tools: []` would go on
|
|
7276
|
-
* the wire while `getTools()` held the tool the app had explicitly registered,
|
|
7277
|
-
* and the model would answer that it had no such tool. Registering a tool is an explicit act by the app; silently dropping it
|
|
7278
|
-
* because a `/models` listing is terse is the library second-guessing the
|
|
7279
|
-
* developer. A model that declares its capabilities and omits function calling
|
|
7280
|
-
* is still honoured — that is a statement, and it is respected.
|
|
7281
|
-
*
|
|
7282
|
-
* Failure modes, both ways round: over-sending means a model that cannot use
|
|
7283
|
-
* tools does not call one, or its own endpoint rejects the array with an error
|
|
7284
|
-
* the developer can read. Under-sending would be silent, total, and look like a
|
|
7285
|
-
* lying model.
|
|
7765
|
+
* The registered tools, unless the current model says it cannot call one — the gate
|
|
7766
|
+
* lives in `toolsForCurrentModel` (with its reasoning), where the context measure reads
|
|
7767
|
+
* the same rule, so send / retry / edit and the gauge cannot drift.
|
|
7286
7768
|
*/
|
|
7287
7769
|
_toolsForCurrentModel() {
|
|
7288
|
-
|
|
7289
|
-
const declinesTools = capabilities ? !capabilities.includes("function_calling") : false;
|
|
7290
|
-
return declinesTools ? [] : this._config.getTools();
|
|
7770
|
+
return toolsForCurrentModel(this._config);
|
|
7291
7771
|
}
|
|
7292
7772
|
async _handleRetry(event) {
|
|
7293
7773
|
const { messageId, targetId } = event.detail ?? {};
|
|
@@ -7322,10 +7802,16 @@ class AparteClient {
|
|
|
7322
7802
|
targetElement,
|
|
7323
7803
|
newMessageId,
|
|
7324
7804
|
provider,
|
|
7325
|
-
[...systemMessages, ...chatMessages],
|
|
7805
|
+
[...systemMessages, ...mergeAdjacentUserMessages(chatMessages)],
|
|
7326
7806
|
config.defaultModel || "",
|
|
7327
7807
|
authConfig,
|
|
7328
|
-
{
|
|
7808
|
+
{
|
|
7809
|
+
temperature: 0.4,
|
|
7810
|
+
rebuild: async () => [
|
|
7811
|
+
...this._systemMessages(),
|
|
7812
|
+
...mergeAdjacentUserMessages(this._messagesToChatMessages(this._current(targetElement, historyMessages)))
|
|
7813
|
+
]
|
|
7814
|
+
}
|
|
7329
7815
|
);
|
|
7330
7816
|
}
|
|
7331
7817
|
/**
|
|
@@ -7388,11 +7874,27 @@ class AparteClient {
|
|
|
7388
7874
|
targetElement,
|
|
7389
7875
|
newMessageId,
|
|
7390
7876
|
provider,
|
|
7391
|
-
[...systemMessages, ...chatMessages],
|
|
7877
|
+
[...systemMessages, ...mergeAdjacentUserMessages(chatMessages)],
|
|
7392
7878
|
config.defaultModel || "",
|
|
7393
|
-
authConfig
|
|
7879
|
+
authConfig,
|
|
7880
|
+
{
|
|
7881
|
+
rebuild: async () => [
|
|
7882
|
+
...this._systemMessages(),
|
|
7883
|
+
...mergeAdjacentUserMessages(this._messagesToChatMessages(this._current(targetElement, historyMessages)))
|
|
7884
|
+
]
|
|
7885
|
+
}
|
|
7394
7886
|
);
|
|
7395
7887
|
}
|
|
7888
|
+
/**
|
|
7889
|
+
* These messages as the transcript holds them NOW. A retry or an edit slices the path
|
|
7890
|
+
* before it streams; a compaction that runs during the turn writes its checkpoint onto
|
|
7891
|
+
* one of those messages in the transcript, and the slice still holds the objects as they
|
|
7892
|
+
* were. Looked up by id, tree-wide, because a retry has moved the path since.
|
|
7893
|
+
*/
|
|
7894
|
+
_current(target, messages) {
|
|
7895
|
+
const live2 = new Map((target.getMessages?.() ?? []).map((m) => [m.id, m]));
|
|
7896
|
+
return messages.map((m) => target.getMessage?.(m.id) ?? live2.get(m.id) ?? m);
|
|
7897
|
+
}
|
|
7396
7898
|
/**
|
|
7397
7899
|
* The chain the gesture really came up, nearest node first.
|
|
7398
7900
|
*
|
|
@@ -7526,29 +8028,17 @@ class AparteClient {
|
|
|
7526
8028
|
* imitating.
|
|
7527
8029
|
*/
|
|
7528
8030
|
_messagesToChatMessages(messages) {
|
|
7529
|
-
|
|
8031
|
+
const { summary, messages: kept } = historyFromCheckpoint(messages);
|
|
8032
|
+
return [...this._summaryMessage(summary), ...wireMessages(kept)];
|
|
7530
8033
|
}
|
|
7531
8034
|
/**
|
|
7532
|
-
*
|
|
7533
|
-
*
|
|
7534
|
-
*
|
|
7535
|
-
*
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
* empty here and the drop below would swallow the whole transcript.
|
|
7540
|
-
*/
|
|
7541
|
-
_wireHistory(messages) {
|
|
7542
|
-
return messages.filter((m) => {
|
|
7543
|
-
if (m.role !== "user" && m.role !== "assistant") return false;
|
|
7544
|
-
return m.status !== "error" && !AparteClient._inFlight(m);
|
|
7545
|
-
}).flatMap((m) => {
|
|
7546
|
-
if (m.role === "assistant" && m.segments?.some((s) => s.type === "tool_call")) {
|
|
7547
|
-
return wireTurns(m, (segments) => this._segmentsToText(segments));
|
|
7548
|
-
}
|
|
7549
|
-
const content = this._wireText(m);
|
|
7550
|
-
return content ? [{ role: m.role, content }] : [];
|
|
7551
|
-
});
|
|
8035
|
+
* What the model reads in place of everything before a checkpoint: the summary, as a
|
|
8036
|
+
* `user` message under the same preamble a legacy compaction message gets, so it is
|
|
8037
|
+
* context and not something the person said. It may sit right before a user turn;
|
|
8038
|
+
* `mergeAdjacentUserMessages` joins the two where the history is assembled.
|
|
8039
|
+
*/
|
|
8040
|
+
_summaryMessage(summary) {
|
|
8041
|
+
return summary === void 0 ? [] : [{ role: "user", content: underPreamble(summary) }];
|
|
7552
8042
|
}
|
|
7553
8043
|
/** The "no model selected" warning is said once per client, not once per dropped send. */
|
|
7554
8044
|
_warnedNoModel = false;
|
|
@@ -7638,12 +8128,14 @@ class AparteClient {
|
|
|
7638
8128
|
});
|
|
7639
8129
|
let authConfig;
|
|
7640
8130
|
let messages;
|
|
8131
|
+
let fileParts;
|
|
7641
8132
|
try {
|
|
7642
8133
|
authConfig = providerId ? await this._resolveAuth(providerId) : void 0;
|
|
7643
8134
|
const rawFiles = Array.isArray(event.detail?.files) ? event.detail.files : [];
|
|
7644
8135
|
const filesToInject = this._selectFilesToInject(rawFiles);
|
|
7645
8136
|
const contentParts = filesToInject.length > 0 ? await this._filesToContentParts(filesToInject) : [];
|
|
7646
|
-
|
|
8137
|
+
fileParts = contentParts.length > 0 ? contentParts : void 0;
|
|
8138
|
+
messages = this._buildMessages(content, targetElement, fileParts);
|
|
7647
8139
|
} catch (error) {
|
|
7648
8140
|
this._handleLifecycleError(targetElement, messageId, AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR));
|
|
7649
8141
|
return;
|
|
@@ -7654,7 +8146,8 @@ class AparteClient {
|
|
|
7654
8146
|
provider,
|
|
7655
8147
|
messages,
|
|
7656
8148
|
modelId || config.defaultModel || "",
|
|
7657
|
-
authConfig
|
|
8149
|
+
authConfig,
|
|
8150
|
+
{ rebuild: async () => this._buildMessages(content, targetElement, fileParts) }
|
|
7658
8151
|
);
|
|
7659
8152
|
}
|
|
7660
8153
|
/**
|
|
@@ -7671,101 +8164,18 @@ class AparteClient {
|
|
|
7671
8164
|
}
|
|
7672
8165
|
const userMsg = parts && parts.length > 0 ? [{ type: "text", text: userContent }, ...parts] : userContent;
|
|
7673
8166
|
messages.push({ role: "user", content: userMsg });
|
|
7674
|
-
return messages;
|
|
8167
|
+
return mergeAdjacentUserMessages(messages);
|
|
7675
8168
|
}
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
* the question the history gates can ask is "is this still in flight?", never
|
|
7679
|
-
* "is this completed?".
|
|
7680
|
-
*
|
|
7681
|
-
* The gates below ask the first, because a host that SEEDS a transcript
|
|
7682
|
-
* — restoring a saved conversation, hydrating a server-rendered one — has no
|
|
7683
|
-
* reason to invent a status for turns that are already over. Asking "is this
|
|
7684
|
-
* completed?" would leave the cutoff at 0 and slice away the whole transcript: the
|
|
7685
|
-
* model would get the new question with no context, and nothing in the UI would say
|
|
7686
|
-
* so, because the viewport still shows every message.
|
|
7687
|
-
*/
|
|
7688
|
-
static _inFlight(m) {
|
|
7689
|
-
return m.status === "streaming" || m.status === "pending";
|
|
7690
|
-
}
|
|
7691
|
-
_toHistoryMessages(messages) {
|
|
8169
|
+
_toHistoryMessages(path) {
|
|
8170
|
+
const { summary, messages } = historyFromCheckpoint(path);
|
|
7692
8171
|
let cutoff = 0;
|
|
7693
8172
|
for (let i = 0; i < messages.length; i++) {
|
|
7694
8173
|
const m = messages[i];
|
|
7695
|
-
if (m.role === "assistant" && m.status !== "error" && !
|
|
8174
|
+
if (m.role === "assistant" && m.status !== "error" && !isInFlight(m)) {
|
|
7696
8175
|
cutoff = i + 1;
|
|
7697
8176
|
}
|
|
7698
8177
|
}
|
|
7699
|
-
return this.
|
|
7700
|
-
}
|
|
7701
|
-
/**
|
|
7702
|
-
* A message's text as the MODEL should read it — the one mapper both history
|
|
7703
|
-
* paths use, so the compaction preamble cannot be applied on retry and edit and
|
|
7704
|
-
* forgotten on the ordinary send, which is what the first review of this feature
|
|
7705
|
-
* caught: the summary went out on every normal turn as something the user said.
|
|
7706
|
-
*/
|
|
7707
|
-
_wireText(message) {
|
|
7708
|
-
const text = this._extractText(message);
|
|
7709
|
-
return message.compaction ? `${COMPACTION_PREAMBLE}
|
|
7710
|
-
|
|
7711
|
-
${text}` : text;
|
|
7712
|
-
}
|
|
7713
|
-
/**
|
|
7714
|
-
* What the model is told it said last turn — reconstructed from what was
|
|
7715
|
-
* RENDERED, not from the raw-text field.
|
|
7716
|
-
*
|
|
7717
|
-
* The segments are what the reader saw, so they come first. `content` is the
|
|
7718
|
-
* fallback, and that is not vestigial: a non-streaming transport writes the whole
|
|
7719
|
-
* reply there and creates no segments at all.
|
|
7720
|
-
*/
|
|
7721
|
-
_extractText(message) {
|
|
7722
|
-
const rendered = this._segmentsToText(message.segments);
|
|
7723
|
-
if (rendered) return rendered;
|
|
7724
|
-
if (typeof message.content === "string" && message.content) return message.content;
|
|
7725
|
-
return "";
|
|
7726
|
-
}
|
|
7727
|
-
/**
|
|
7728
|
-
* Segments → the text a model can read back.
|
|
7729
|
-
*
|
|
7730
|
-
* Fences and the language tag are kept. Without them the model re-reads its own
|
|
7731
|
-
* code as prose — which is how it starts explaining a snippet it believes it
|
|
7732
|
-
* wrote in English.
|
|
7733
|
-
*
|
|
7734
|
-
* A type core does not know — a registered block grammar's (an artifact, a
|
|
7735
|
-
* citation, a file), a consumer's custom segment — follows one rule: its
|
|
7736
|
-
* `content` if it has one, else its `fallback`, else nothing. Dropping such a
|
|
7737
|
-
* segment would make a document the model had just produced invisible on the very
|
|
7738
|
-
* next turn, so it could not be asked to change the thing it had built. An artifact
|
|
7739
|
-
* is a plugin's type like any other and follows this rule.
|
|
7740
|
-
*
|
|
7741
|
-
* `thinking` and `tool_call` stay out on purpose — the first is the model's own
|
|
7742
|
-
* scratchpad, and most APIs neither want it back nor bill for it kindly; the
|
|
7743
|
-
* second is not text: `_wireHistory` sends a reply's calls as the assistant's
|
|
7744
|
-
* `toolCalls` and the `tool` messages answering them (`wire-history.ts`).
|
|
7745
|
-
*/
|
|
7746
|
-
_segmentsToText(segments) {
|
|
7747
|
-
if (!segments?.length) return "";
|
|
7748
|
-
const fence = "```";
|
|
7749
|
-
const parts = [];
|
|
7750
|
-
for (const segment of segments) {
|
|
7751
|
-
const content = segment.content;
|
|
7752
|
-
if (segment.type === "text") {
|
|
7753
|
-
if (content) parts.push(content);
|
|
7754
|
-
} else if (segment.type === "code") {
|
|
7755
|
-
const lang = segment.language ?? "";
|
|
7756
|
-
parts.push(`${fence}${lang}
|
|
7757
|
-
${content ?? ""}
|
|
7758
|
-
${fence}`);
|
|
7759
|
-
} else if (segment.type === "thinking" || segment.type === "tool_call" || segment.type === "error") {
|
|
7760
|
-
continue;
|
|
7761
|
-
} else if (typeof content === "string" && content) {
|
|
7762
|
-
parts.push(content);
|
|
7763
|
-
} else {
|
|
7764
|
-
const fallback = segment.fallback;
|
|
7765
|
-
if (fallback) parts.push(fallback);
|
|
7766
|
-
}
|
|
7767
|
-
}
|
|
7768
|
-
return parts.join("\n").trim();
|
|
8178
|
+
return [...this._summaryMessage(summary), ...wireMessages(messages.slice(0, cutoff))];
|
|
7769
8179
|
}
|
|
7770
8180
|
/**
|
|
7771
8181
|
* Which of the send's pending files get inlined into the request: the
|
|
@@ -7872,8 +8282,10 @@ ${content}
|
|
|
7872
8282
|
});
|
|
7873
8283
|
const prefixSegments = baseRequest._meta?.["prefixSegments"];
|
|
7874
8284
|
let suspended;
|
|
8285
|
+
let finishReason;
|
|
7875
8286
|
const emitter = (event) => {
|
|
7876
8287
|
adapter(event);
|
|
8288
|
+
if (event.type === "run-done") finishReason = event.finishReason;
|
|
7877
8289
|
if (event.type === "run-suspended") {
|
|
7878
8290
|
const ticket = event.ticket;
|
|
7879
8291
|
suspended = { pending: event.pending, ...typeof ticket === "string" ? { ticket } : {} };
|
|
@@ -7926,7 +8338,11 @@ ${content}
|
|
|
7926
8338
|
// BARE uuid for the synthetic tool (the adapter renders `tool-<id>`).
|
|
7927
8339
|
idGen: (prefix) => prefix === "synthetic-tool" ? uuid() : `${prefix}-${uuid()}`
|
|
7928
8340
|
});
|
|
7929
|
-
return {
|
|
8341
|
+
return {
|
|
8342
|
+
usage: usage ?? void 0,
|
|
8343
|
+
...finishReason ? { finishReason } : {},
|
|
8344
|
+
...suspended ? { suspended } : {}
|
|
8345
|
+
};
|
|
7930
8346
|
}
|
|
7931
8347
|
/**
|
|
7932
8348
|
* The approval channel a run asks through — and the one a paused server run's calls
|
|
@@ -8077,7 +8493,7 @@ ${content}
|
|
|
8077
8493
|
*/
|
|
8078
8494
|
_resumeMessages(targetElement, messageId, sent) {
|
|
8079
8495
|
const reply = targetElement.getMessages?.().find((m) => m.id === messageId);
|
|
8080
|
-
return [...sent, ...reply ? wireTurns(reply,
|
|
8496
|
+
return [...sent, ...reply ? wireTurns(reply, segmentsToWireText, { openDeferred: true }) : []];
|
|
8081
8497
|
}
|
|
8082
8498
|
/**
|
|
8083
8499
|
* What the send of a reloaded turn put before its reply, rebuilt the way
|
|
@@ -8092,7 +8508,11 @@ ${content}
|
|
|
8092
8508
|
const askedAt = before.map((m) => m.role).lastIndexOf("user");
|
|
8093
8509
|
const historyOption = this.options.history ?? "viewport";
|
|
8094
8510
|
const history = historyOption === "viewport" ? this._toHistoryMessages(before) : typeof historyOption === "function" ? historyOption(before) : [];
|
|
8095
|
-
return
|
|
8511
|
+
return mergeAdjacentUserMessages([
|
|
8512
|
+
...this._systemMessages(),
|
|
8513
|
+
...history,
|
|
8514
|
+
...askedAt >= 0 ? wireMessages([before[askedAt]]) : []
|
|
8515
|
+
]);
|
|
8096
8516
|
}
|
|
8097
8517
|
/**
|
|
8098
8518
|
* A new message closes a wait nobody answered. The paused calls of the last reply are
|
|
@@ -8153,6 +8573,11 @@ ${content}
|
|
|
8153
8573
|
const base = this._resumeBase(messages, messages.indexOf(last));
|
|
8154
8574
|
await this._streamTurn(targetElement, last.id, provider, base, recorded?.modelId ?? config.defaultModel ?? "", authConfig, {
|
|
8155
8575
|
resume: { pending: pending2, ticket },
|
|
8576
|
+
rebuild: async () => {
|
|
8577
|
+
const now = targetElement.getMessages?.() ?? [];
|
|
8578
|
+
const at = now.findIndex((m) => m.id === last.id);
|
|
8579
|
+
return at >= 0 ? this._resumeBase(now, at) : base;
|
|
8580
|
+
},
|
|
8156
8581
|
...recorded?.temperature !== void 0 ? { temperature: recorded.temperature } : {}
|
|
8157
8582
|
});
|
|
8158
8583
|
} finally {
|
|
@@ -8232,7 +8657,8 @@ function createAgentStreamRunner(options) {
|
|
|
8232
8657
|
let ended = false;
|
|
8233
8658
|
for await (const line of readNdjson(response.body, opts.signal)) {
|
|
8234
8659
|
if (line.type === "run-error") {
|
|
8235
|
-
|
|
8660
|
+
const known2 = Object.values(AparteErrorCode).includes(line.code ?? "") ? line.code : void 0;
|
|
8661
|
+
throw new AparteError(line.message || "The agent run failed.", known2 ?? AparteErrorCode.PROVIDER_ERROR, line.code ? { code: line.code } : void 0);
|
|
8236
8662
|
}
|
|
8237
8663
|
if (line.type === "run-done" || line.type === "run-suspended") {
|
|
8238
8664
|
usage = line.usage;
|
|
@@ -8727,7 +9153,7 @@ function deferHydrationOf(ctor) {
|
|
|
8727
9153
|
};
|
|
8728
9154
|
}
|
|
8729
9155
|
export {
|
|
8730
|
-
|
|
9156
|
+
isSegmentSettled as $,
|
|
8731
9157
|
APARTE_CONFIG_CHANGE as A,
|
|
8732
9158
|
contextConfig as B,
|
|
8733
9159
|
copyText as C,
|
|
@@ -8744,57 +9170,64 @@ export {
|
|
|
8744
9170
|
detachConfig as N,
|
|
8745
9171
|
escapeAttr as O,
|
|
8746
9172
|
escapeHtml as P,
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
9173
|
+
estimateTokens as Q,
|
|
9174
|
+
estimateWireTokens as R,
|
|
9175
|
+
filesToAttachments as S,
|
|
9176
|
+
getAllRenderers as T,
|
|
9177
|
+
getSegmentRenderer as U,
|
|
9178
|
+
historyFromCheckpoint as V,
|
|
9179
|
+
installDefaultRenderersOnce as W,
|
|
9180
|
+
installDialogTriggersOnce as X,
|
|
9181
|
+
isAwaitingReply as Y,
|
|
9182
|
+
isFormatAdapter as Z,
|
|
9183
|
+
isSafeUrl as _,
|
|
8758
9184
|
APARTE_CONVERSATION_SCHEMA_VERSION as a,
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
9185
|
+
measureContext as a0,
|
|
9186
|
+
overrideSegmentRenderer as a1,
|
|
9187
|
+
parseAparteEventStream as a2,
|
|
9188
|
+
parseMarkdownToSegments as a3,
|
|
9189
|
+
populateBubbleFromMessage as a4,
|
|
9190
|
+
readableToAsyncIterable as a5,
|
|
9191
|
+
registerDefaultRenderers as a6,
|
|
9192
|
+
registerSegmentRenderer as a7,
|
|
9193
|
+
releaseDeferredHydration as a8,
|
|
9194
|
+
requestUserInput as a9,
|
|
9195
|
+
renumberSegments as aA,
|
|
9196
|
+
openSegmentIds as aB,
|
|
9197
|
+
isTerminalStatus as aC,
|
|
9198
|
+
adoptMessageSegments as aD,
|
|
9199
|
+
APARTE_DERIVED_SEGMENTS as aE,
|
|
9200
|
+
rememberConversationStatus as aF,
|
|
9201
|
+
tellConversationStatus as aG,
|
|
9202
|
+
conversationStatusOf as aH,
|
|
9203
|
+
usageTokens as aI,
|
|
9204
|
+
rootOf as aJ,
|
|
9205
|
+
resolveConfig as aa,
|
|
9206
|
+
revokeAttachmentUrls as ab,
|
|
9207
|
+
runWithConfig as ac,
|
|
9208
|
+
segmentDuration as ad,
|
|
9209
|
+
segmentTiming as ae,
|
|
9210
|
+
subscribeConfigChange as af,
|
|
9211
|
+
toToolCallView as ag,
|
|
9212
|
+
toolRefusal as ah,
|
|
9213
|
+
unregisterSegmentRenderer as ai,
|
|
9214
|
+
uuid as aj,
|
|
9215
|
+
eventOrigin as ak,
|
|
9216
|
+
activeElementIn as al,
|
|
9217
|
+
gateGlide as am,
|
|
9218
|
+
chatBoundaryOf as an,
|
|
9219
|
+
toolCallRenderer as ao,
|
|
9220
|
+
toolRendererThatDrew as ap,
|
|
9221
|
+
isMountedRenderer as aq,
|
|
9222
|
+
renderingVersionOf as ar,
|
|
9223
|
+
onRenderersChange as as,
|
|
9224
|
+
mergeSegmentUpdate as at,
|
|
9225
|
+
writeStreamedMarkdown as au,
|
|
9226
|
+
CheckpointStateWatcher as av,
|
|
9227
|
+
chatIdOf as aw,
|
|
9228
|
+
stampSegmentActivity as ax,
|
|
9229
|
+
stampSegmentOnInsert as ay,
|
|
9230
|
+
stampSegmentOnUpdate as az,
|
|
8798
9231
|
APARTE_DEFAULT_BUBBLE_ACTIONS as b,
|
|
8799
9232
|
APARTE_DEFAULT_HOST_HANDLERS as c,
|
|
8800
9233
|
APARTE_DEFAULT_ICON_FALLBACKS as d,
|
|
@@ -8821,4 +9254,4 @@ export {
|
|
|
8821
9254
|
collectRendererStyles as y,
|
|
8822
9255
|
contentToText as z
|
|
8823
9256
|
};
|
|
8824
|
-
//# sourceMappingURL=defer-hydration-
|
|
9257
|
+
//# sourceMappingURL=defer-hydration-8vI8xNNx.js.map
|