@aparte/engine 0.15.1 → 0.16.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/README.md +4 -4
- package/dist/agent/stream-events.d.ts +42 -13
- package/dist/agent/stream-events.d.ts.map +1 -1
- package/dist/agent/stream-run.d.ts +9 -9
- package/dist/agent/stream-run.d.ts.map +1 -1
- package/dist/index.d.ts +4 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -270
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/agent/parsers/artifact-kind.d.ts +0 -19
- package/dist/agent/parsers/artifact-kind.d.ts.map +0 -1
- package/dist/conversation/compactor.d.ts +0 -175
- package/dist/conversation/compactor.d.ts.map +0 -1
- package/dist/conversation/selector.d.ts +0 -78
- package/dist/conversation/selector.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ new AparteClient().start();
|
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Its core export is **`runStreamAgent`**: a DOM-free structured-stream loop that turns a
|
|
20
|
-
transport's token stream into high-level run events (text, thinking, tool calls
|
|
20
|
+
transport's token stream into high-level run events (text, thinking, tool calls),
|
|
21
21
|
drives the tool-calling loop (with optional human-in-the-loop approval), and reports usage.
|
|
22
22
|
|
|
23
23
|
`@aparte/core` runs this loop by default — it used to embed a copy of it inline, and the
|
|
@@ -26,9 +26,9 @@ with the loop's call sequences pinned as snapshots. `AparteClientOptions.streamR
|
|
|
26
26
|
the seam to wrap it (`(opts) => runStreamAgent({ ...opts, onHistoryAppend })` for a host
|
|
27
27
|
that owns its transcript) or to replace it with a loop of your own emitting the same events.
|
|
28
28
|
|
|
29
|
-
`@aparte/core` is not needed here — `runStreamAgent
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
`@aparte/core` is not needed here — `runStreamAgent` and the parsers import nothing from it.
|
|
30
|
+
Conversation compaction (the context budget, the selector, the summariser) is
|
|
31
|
+
`@aparte/plugin-compaction`, not the engine: the loop reports usage and lets the caller decide.
|
|
32
32
|
|
|
33
33
|
## Owning the history (prefix-cache hosts)
|
|
34
34
|
|
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
* core types (`AparteStreamEvent`, `AparteUsage`, `AparteChatMessage`, `AparteToolCall`)
|
|
17
17
|
* so the adapter passes the real objects through with zero runtime conversion.
|
|
18
18
|
*
|
|
19
|
-
* SCOPE: text · thinking · tool_use (+ HITL approval) · done · error ·
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* artifact
|
|
19
|
+
* SCOPE: text · thinking · tool_use (+ HITL approval) · done · error · synthetic
|
|
20
|
+
* toolChoice bypass. Tagged blocks in the text are adapter-side (core's parser reads
|
|
21
|
+
* the grammars registered on the config); the built-in `create_artifact` left with
|
|
22
|
+
* the artifact (D7), and the raw / XML artifact modes and the multi-phase pipeline
|
|
23
|
+
* were removed before it (audit 2026-08-28, D2).
|
|
23
24
|
*/
|
|
24
25
|
/**
|
|
25
26
|
* Token usage. Structurally a superset-compatible mirror of `AparteUsage`: the
|
|
@@ -170,7 +171,7 @@ export interface StreamChatRequest {
|
|
|
170
171
|
* un-assignable — from the opposite direction to the `messages` mismatch.
|
|
171
172
|
*/
|
|
172
173
|
modelId: string;
|
|
173
|
-
/** Per-turn hints carried through for the
|
|
174
|
+
/** Per-turn hints carried through for the host (`prefixSegments`, and whatever the consumer puts there); the loop reads none. */
|
|
174
175
|
_meta?: Record<string, unknown>;
|
|
175
176
|
/** `'none'` makes the loop drop the tool inventory for that turn. */
|
|
176
177
|
toolChoice?: 'auto' | 'none' | {
|
|
@@ -182,12 +183,32 @@ export interface StreamChatRequest {
|
|
|
182
183
|
}
|
|
183
184
|
/** A tool handler (mirrors the resolved `AparteToolHandler`). */
|
|
184
185
|
export type StreamToolHandler = (call: StreamToolCall, signal: AbortSignal) => Promise<{
|
|
186
|
+
/** What the model reads. */
|
|
185
187
|
content: string;
|
|
188
|
+
/** The same result as a value, forwarded on `tool-resolved` as `structuredResult`; the model never sees it. */
|
|
189
|
+
structuredContent?: unknown;
|
|
186
190
|
}>;
|
|
187
191
|
/** Per-tool loop configuration (mirrors the `AparteTool` subset the loop reads). */
|
|
188
192
|
export interface StreamToolConfig {
|
|
189
193
|
maxTurns?: number;
|
|
190
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Whether a call of this tool pauses for a decision. A boolean is the tool's own
|
|
196
|
+
* declaration; a predicate decides PER CALL, from the arguments — the shape an
|
|
197
|
+
* approval policy needs ("a write to this path asks, a read does not"), and one a
|
|
198
|
+
* boolean could only approximate by gating every call and auto-approving most,
|
|
199
|
+
* which paints `awaiting-approval` on rows nobody was ever going to be asked about.
|
|
200
|
+
*
|
|
201
|
+
* The predicate has three answers, because a policy has three: `false` runs the
|
|
202
|
+
* call, `'ask'` (or `true`) puts it to someone — the loop announces the pause with
|
|
203
|
+
* `tool-awaiting-approval` — and `'deny'` means the decision is already made: the
|
|
204
|
+
* resolver is still consulted (it is the one channel that carries the refusal
|
|
205
|
+
* sentence back), but nothing is announced, since nobody is being asked.
|
|
206
|
+
*/
|
|
207
|
+
needsApproval?: boolean | ((call: {
|
|
208
|
+
id: string;
|
|
209
|
+
name: string;
|
|
210
|
+
input: Record<string, unknown>;
|
|
211
|
+
}) => boolean | 'ask' | 'deny');
|
|
191
212
|
}
|
|
192
213
|
/**
|
|
193
214
|
* Resolves a human-in-the-loop approval (mirrors core's
|
|
@@ -200,7 +221,14 @@ export type StreamApprovalResolver = (call: {
|
|
|
200
221
|
}, signal: AbortSignal) => Promise<{
|
|
201
222
|
approved: boolean;
|
|
202
223
|
payload?: unknown;
|
|
224
|
+
/** What the user said to do instead, on a refusal — quoted to the model as theirs. */
|
|
203
225
|
instruction?: string;
|
|
226
|
+
/**
|
|
227
|
+
* The refusal, verbatim, when nobody said anything — a policy that refused on its
|
|
228
|
+
* own ("plan mode: read-only tools only"). The sentence the loop would otherwise
|
|
229
|
+
* write attributes the refusal to the user, which would be a lie here.
|
|
230
|
+
*/
|
|
231
|
+
reason?: string;
|
|
204
232
|
}>;
|
|
205
233
|
/**
|
|
206
234
|
* High-level, DOM-free events isomorphic to the inline loop's `targetElement.*`
|
|
@@ -219,6 +247,7 @@ export type StreamApprovalResolver = (call: {
|
|
|
219
247
|
* - `tool-awaiting-approval` → updateSegment('awaiting-approval') + dispatch `aparte-tool-approval-request`
|
|
220
248
|
* - `tool-approved` → updateSegment('pending')
|
|
221
249
|
* - `tool-rejected` → updateSegment('rejected', result)
|
|
250
|
+
* - `tool-failed` → updateSegment('failed', error) — the handler threw; the run then ends on that error
|
|
222
251
|
* - `tool-resolved` → updateSegment('resolved', result)
|
|
223
252
|
* - `tool-aborted` → updateSegment('aborted') (no-handler path, timeout/abort path, or per-tool maxTurns path)
|
|
224
253
|
* - `turn-limit-exceeded` scope:'global' → addSegment(error 'MAX_TURNS_EXCEEDED');
|
|
@@ -238,13 +267,6 @@ export type StreamRunEvent = {
|
|
|
238
267
|
} | {
|
|
239
268
|
type: 'thinking-delta';
|
|
240
269
|
delta: string;
|
|
241
|
-
} | {
|
|
242
|
-
type: 'artifact-ready';
|
|
243
|
-
id: string;
|
|
244
|
-
mimeType: string;
|
|
245
|
-
kind: string;
|
|
246
|
-
title: string;
|
|
247
|
-
content: string;
|
|
248
270
|
} | {
|
|
249
271
|
type: 'tool-start';
|
|
250
272
|
toolCallId: string;
|
|
@@ -262,10 +284,17 @@ export type StreamRunEvent = {
|
|
|
262
284
|
type: 'tool-rejected';
|
|
263
285
|
toolCallId: string;
|
|
264
286
|
reason: string;
|
|
287
|
+
}
|
|
288
|
+
/** The handler threw (not an abort): the row hears of the crash before the run ends on it. */
|
|
289
|
+
| {
|
|
290
|
+
type: 'tool-failed';
|
|
291
|
+
toolCallId: string;
|
|
292
|
+
error: string;
|
|
265
293
|
} | {
|
|
266
294
|
type: 'tool-resolved';
|
|
267
295
|
toolCallId: string;
|
|
268
296
|
result: string;
|
|
297
|
+
structuredResult?: unknown;
|
|
269
298
|
} | {
|
|
270
299
|
type: 'tool-aborted';
|
|
271
300
|
toolCallId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-events.d.ts","sourceRoot":"","sources":["../../src/agent/stream-events.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"stream-events.d.ts","sourceRoot":"","sources":["../../src/agent/stream-events.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Formerly the parity target of core's inline loop; now the one loop, its call
|
|
13
13
|
* sequences pinned by core's `stream-parity` snapshots.
|
|
14
|
-
* Scope: text · thinking · tool_use (+ HITL) · done · error ·
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* multi-phase pipeline were removed (audit 2026-08-28, D2) — orchestration is the
|
|
14
|
+
* Scope: text · thinking · tool_use (+ HITL) · done · error · synthetic toolChoice
|
|
15
|
+
* bypass. Tagged blocks in the text (`<artifact>`, `<cite>`…) are adapter-side: core's
|
|
16
|
+
* parser reads the grammars registered on the config. The built-in `create_artifact`
|
|
17
|
+
* left with the artifact (D7) — it is a registered tool now. The raw / XML artifact
|
|
18
|
+
* modes and the multi-phase pipeline were removed (audit 2026-08-28, D2) — orchestration is the
|
|
19
19
|
* product's, and a mode nothing in this repository emitted was a contract
|
|
20
20
|
* maintained for nobody.
|
|
21
21
|
*/
|
|
@@ -68,10 +68,10 @@ export interface StreamRunOptions {
|
|
|
68
68
|
*/
|
|
69
69
|
onHistoryAppend?: (message: StreamAgentMessage) => void;
|
|
70
70
|
/**
|
|
71
|
-
* Generates
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* Generates the ids the loop mints itself — today only the synthetic call of a
|
|
72
|
+
* forced `toolChoice` (`prefix` is `'synthetic-tool'`). The default is a
|
|
73
|
+
* deterministic per-run counter; core's client injects a crypto-based one. Tool
|
|
74
|
+
* ids otherwise flow from the stream.
|
|
75
75
|
*/
|
|
76
76
|
idGen?: (prefix: string) => string;
|
|
77
77
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream-run.d.ts","sourceRoot":"","sources":["../../src/agent/stream-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAElB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,WAAW,EACd,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"stream-run.d.ts","sourceRoot":"","sources":["../../src/agent/stream-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAElB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,WAAW,EACd,MAAM,oBAAoB,CAAC;AAO5B,MAAM,WAAW,gBAAgB;IAC7B,8EAA8E;IAC9E,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,WAAW,EAAE,iBAAiB,CAAC;IAC/B;;;;;OAKG;IACH,aAAa,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,aAAa,CAAC,eAAe,CAAC,GAAG,MAAM,CAAC,CAAC;IAChG,uFAAuF;IACvF,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,iBAAiB,GAAG,SAAS,CAAC;IAC5D,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,gBAAgB,GAAG,SAAS,CAAC;IAClE,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,sBAAsB,CAAC;IAC1C,+DAA+D;IAC/D,OAAO,EAAE,gBAAgB,CAAC;IAC1B,0EAA0E;IAC1E,MAAM,EAAE,WAAW,CAAC;IACpB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;;;;;;;;;;;;OAmBG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACxD;;;;;OAKG;IACH,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AAeD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA6a7F"}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,14 +7,11 @@
|
|
|
7
7
|
* renders its events through `createStreamAdapter`. Its call sequences are pinned by
|
|
8
8
|
* core's `stream-parity` snapshots, recorded while the inline loop still ran beside it.
|
|
9
9
|
*
|
|
10
|
-
* Deliberately just the loop core drives
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Deliberately just the loop core drives. Opt-in *tools* (ask-user / RAG / skills / code)
|
|
11
|
+
* belong in `plugins/*`, and so does compaction: the context budget and the selector that
|
|
12
|
+
* lived here until 0.16.0 are `@aparte/plugin-compaction`'s now — the loop reports usage
|
|
13
|
+
* and lets the caller decide, so nothing in it ever read them.
|
|
13
14
|
*/
|
|
14
15
|
export * from './agent/stream-events.js';
|
|
15
16
|
export * from './agent/stream-run.js';
|
|
16
|
-
export { deriveArtifactKind } from './agent/parsers/artifact-kind.js';
|
|
17
|
-
export * from './conversation/compactor.js';
|
|
18
|
-
export { createCompactionSelector } from './conversation/selector.js';
|
|
19
|
-
export type { CompactionSelectorOptions, CompactableMessage, CompactionSelection, CompactionSelector } from './conversation/selector.js';
|
|
20
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,25 +1,3 @@
|
|
|
1
|
-
function deriveArtifactKind(mimeType, fallback = "unknown") {
|
|
2
|
-
const m = (mimeType || "").toLowerCase().trim();
|
|
3
|
-
const ant = m.match(/^application\/vnd\.ant\.([a-z0-9-]+)/);
|
|
4
|
-
if (ant) return ant[1];
|
|
5
|
-
if (m === "text/html" || m === "application/xhtml+xml") return "html";
|
|
6
|
-
if (m === "application/javascript" || m === "text/javascript") return "js";
|
|
7
|
-
if (m === "text/css") return "css";
|
|
8
|
-
if (m === "image/svg+xml") return "svg";
|
|
9
|
-
if (m === "application/json") return "json";
|
|
10
|
-
if (m === "text/markdown") return "markdown";
|
|
11
|
-
if (m === "text/csv") return "csv";
|
|
12
|
-
if (m === "text/plain") return "text";
|
|
13
|
-
if (m.includes("react")) return "react";
|
|
14
|
-
if (m.includes("html")) return "html";
|
|
15
|
-
if (m.includes("javascript")) return "js";
|
|
16
|
-
if (m.includes("css")) return "css";
|
|
17
|
-
if (m.includes("svg")) return "svg";
|
|
18
|
-
if (m.includes("json")) return "json";
|
|
19
|
-
if (m.includes("csv")) return "csv";
|
|
20
|
-
if (m.includes("markdown")) return "markdown";
|
|
21
|
-
return fallback;
|
|
22
|
-
}
|
|
23
1
|
const DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
24
2
|
const DEFAULT_MAX_TURNS = 10;
|
|
25
3
|
const warnedUnknownEvents = /* @__PURE__ */ new Set();
|
|
@@ -55,10 +33,7 @@ async function runStreamAgent(opts) {
|
|
|
55
33
|
let lastUsage;
|
|
56
34
|
emitter({ type: "run-start" });
|
|
57
35
|
while (continueLoop) {
|
|
58
|
-
if (signal.aborted)
|
|
59
|
-
emitter({ type: "run-aborted" });
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
36
|
+
if (signal.aborted) break;
|
|
62
37
|
turns++;
|
|
63
38
|
if (turns > maxTurns) {
|
|
64
39
|
emitter({ type: "turn-limit-exceeded", scope: "global", limit: maxTurns });
|
|
@@ -81,7 +56,11 @@ async function runStreamAgent(opts) {
|
|
|
81
56
|
continueLoop = false;
|
|
82
57
|
continue;
|
|
83
58
|
}
|
|
84
|
-
|
|
59
|
+
if (outcome.status === "failed") {
|
|
60
|
+
emitter({ type: "tool-failed", toolCallId: syntheticId, error: errorText(outcome.error) });
|
|
61
|
+
throw outcome.error;
|
|
62
|
+
}
|
|
63
|
+
emitter({ type: "tool-resolved", toolCallId: syntheticId, result: outcome.content, structuredResult: outcome.structuredContent });
|
|
85
64
|
append({ role: "tool_call", content: "", toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });
|
|
86
65
|
append({ role: "tool_result", content: outcome.content, toolCallId: syntheticId });
|
|
87
66
|
baseRequest = { ...baseRequest, toolChoice: "none", tools: void 0 };
|
|
@@ -92,7 +71,6 @@ async function runStreamAgent(opts) {
|
|
|
92
71
|
response = await transportCall(request);
|
|
93
72
|
} catch (err) {
|
|
94
73
|
if (signal.aborted || err?.name === "AbortError") {
|
|
95
|
-
emitter({ type: "run-aborted" });
|
|
96
74
|
continueLoop = false;
|
|
97
75
|
break;
|
|
98
76
|
}
|
|
@@ -106,8 +84,10 @@ async function runStreamAgent(opts) {
|
|
|
106
84
|
emitter({ type: "turn-start" });
|
|
107
85
|
let precedingText = "";
|
|
108
86
|
const toolCallsThisTurn = [];
|
|
87
|
+
let sawToolUse = false;
|
|
109
88
|
let envelope = null;
|
|
110
|
-
const
|
|
89
|
+
const declareCall = (call) => {
|
|
90
|
+
toolCallsThisTurn.push(call);
|
|
111
91
|
if (envelope) return;
|
|
112
92
|
envelope = { role: "tool_call", content: "", toolCalls: toolCallsThisTurn, precedingText: precedingText.trim() || void 0 };
|
|
113
93
|
append(envelope);
|
|
@@ -116,7 +96,6 @@ async function runStreamAgent(opts) {
|
|
|
116
96
|
const bailOnAbort = async () => {
|
|
117
97
|
if (!signal.aborted) return false;
|
|
118
98
|
await iterator.return?.(void 0);
|
|
119
|
-
emitter({ type: "run-aborted" });
|
|
120
99
|
continueLoop = false;
|
|
121
100
|
return true;
|
|
122
101
|
};
|
|
@@ -147,20 +126,11 @@ async function runStreamAgent(opts) {
|
|
|
147
126
|
warnUnknownStreamEvent(event);
|
|
148
127
|
continue;
|
|
149
128
|
}
|
|
150
|
-
|
|
151
|
-
if (event.name === "create_artifact") {
|
|
152
|
-
const input = event.input ?? {};
|
|
153
|
-
const mimeType = input.mimeType ?? "text/plain";
|
|
154
|
-
const kind = deriveArtifactKind(mimeType, "text");
|
|
155
|
-
emitter({ type: "artifact-ready", id: `artifact-${event.id}`, mimeType, kind, title: input.title ?? kind, content: input.content ?? "" });
|
|
156
|
-
ensureEnvelope();
|
|
157
|
-
append({ role: "tool_result", content: "Artifact created successfully.", toolCallId: event.id });
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
129
|
+
sawToolUse = true;
|
|
160
130
|
emitter({ type: "tool-start", toolCallId: event.id, name: event.name, input: event.input });
|
|
161
131
|
const cfg = toolConfigLookup?.(event.name);
|
|
162
132
|
const effectiveMaxTurns = cfg?.maxTurns ?? maxTurns;
|
|
163
|
-
if (turns
|
|
133
|
+
if (turns > effectiveMaxTurns) {
|
|
164
134
|
emitter({ type: "turn-limit-exceeded", scope: "tool", limit: effectiveMaxTurns, toolCallId: event.id });
|
|
165
135
|
continueLoop = false;
|
|
166
136
|
break;
|
|
@@ -172,8 +142,11 @@ async function runStreamAgent(opts) {
|
|
|
172
142
|
break;
|
|
173
143
|
}
|
|
174
144
|
let effectiveInput = event.input;
|
|
175
|
-
|
|
176
|
-
|
|
145
|
+
const gated = typeof cfg?.needsApproval === "function" ? cfg.needsApproval({ id: event.id, name: event.name, input: event.input }) : cfg?.needsApproval;
|
|
146
|
+
if (gated) {
|
|
147
|
+
if (gated !== "deny") {
|
|
148
|
+
emitter({ type: "tool-awaiting-approval", toolCallId: event.id, name: event.name, input: event.input });
|
|
149
|
+
}
|
|
177
150
|
if (!approvalResolver) {
|
|
178
151
|
emitter({ type: "tool-aborted", toolCallId: event.id });
|
|
179
152
|
continueLoop = false;
|
|
@@ -196,9 +169,9 @@ async function runStreamAgent(opts) {
|
|
|
196
169
|
break;
|
|
197
170
|
}
|
|
198
171
|
if (!decision.approved) {
|
|
199
|
-
const rejection = decision.instruction ? `The user rejected this tool call and said: ${decision.instruction}` : "Tool execution was rejected by the user.";
|
|
172
|
+
const rejection = decision.reason?.trim() || (decision.instruction ? `The user rejected this tool call and said: ${decision.instruction}` : "Tool execution was rejected by the user.");
|
|
200
173
|
emitter({ type: "tool-rejected", toolCallId: event.id, reason: rejection });
|
|
201
|
-
|
|
174
|
+
declareCall({ id: event.id, name: event.name, input: event.input });
|
|
202
175
|
append({ role: "tool_result", content: rejection, toolCallId: event.id });
|
|
203
176
|
break;
|
|
204
177
|
}
|
|
@@ -217,9 +190,12 @@ async function runStreamAgent(opts) {
|
|
|
217
190
|
emitter({ type: "tool-aborted", toolCallId: event.id });
|
|
218
191
|
continueLoop = false;
|
|
219
192
|
break;
|
|
193
|
+
} else if (outcome.status === "failed") {
|
|
194
|
+
emitter({ type: "tool-failed", toolCallId: event.id, error: errorText(outcome.error) });
|
|
195
|
+
throw outcome.error;
|
|
220
196
|
} else {
|
|
221
|
-
emitter({ type: "tool-resolved", toolCallId: event.id, result: outcome.content });
|
|
222
|
-
|
|
197
|
+
emitter({ type: "tool-resolved", toolCallId: event.id, result: outcome.content, structuredResult: outcome.structuredContent });
|
|
198
|
+
declareCall({ id: event.id, name: event.name, input: event.input });
|
|
223
199
|
append({ role: "tool_result", content: outcome.content, toolCallId: event.id });
|
|
224
200
|
}
|
|
225
201
|
}
|
|
@@ -228,8 +204,9 @@ async function runStreamAgent(opts) {
|
|
|
228
204
|
await iterator.return?.(void 0).catch(() => {
|
|
229
205
|
});
|
|
230
206
|
}
|
|
231
|
-
if (
|
|
207
|
+
if (!sawToolUse) continueLoop = false;
|
|
232
208
|
}
|
|
209
|
+
if (signal.aborted) emitter({ type: "run-aborted" });
|
|
233
210
|
emitter({ type: "run-done", usage: lastUsage });
|
|
234
211
|
return lastUsage;
|
|
235
212
|
}
|
|
@@ -247,239 +224,23 @@ async function invokeToolHandler(handler, call, signal, toolTimeoutMs) {
|
|
|
247
224
|
});
|
|
248
225
|
try {
|
|
249
226
|
const result = await Promise.race([
|
|
250
|
-
handler(call, controller.signal).then((r) => ({ status: "resolved", content: r.content })),
|
|
227
|
+
handler(call, controller.signal).then((r) => ({ status: "resolved", content: r.content, structuredContent: r.structuredContent })),
|
|
251
228
|
timedOut
|
|
252
229
|
]);
|
|
253
230
|
return result;
|
|
254
231
|
} catch (err) {
|
|
255
232
|
if (err?.name === "AbortError") return { status: "aborted" };
|
|
256
|
-
|
|
233
|
+
return { status: "failed", error: err };
|
|
257
234
|
} finally {
|
|
258
235
|
clearTimeout(timeout);
|
|
259
236
|
signal.removeEventListener("abort", onParentAbort);
|
|
260
237
|
}
|
|
261
238
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
contextWindow: 8192,
|
|
266
|
-
reservedThinking: 0,
|
|
267
|
-
reservedGeneration: 2e3,
|
|
268
|
-
autocompactBufferPct: 0.1,
|
|
269
|
-
// ~3300 tok
|
|
270
|
-
safetyMargin: 500,
|
|
271
|
-
minHistoryBudget: 1e3,
|
|
272
|
-
summaryRatio: 0.1,
|
|
273
|
-
summaryMaxTokens: 400,
|
|
274
|
-
ragHistRatio: 0.25,
|
|
275
|
-
ragHistMaxTokens: 1e3,
|
|
276
|
-
triggerSummaryThresholdPct: 0.75,
|
|
277
|
-
summarizeEveryNTurns: 5,
|
|
278
|
-
summaryLabel: "Conversation summary:",
|
|
279
|
-
ragIntroLabel: "Relevant excerpts from earlier in the conversation:"
|
|
280
|
-
};
|
|
281
|
-
function estimateTokens(text) {
|
|
282
|
-
if (!text) return 0;
|
|
283
|
-
return Math.ceil(text.length / 3.8);
|
|
284
|
-
}
|
|
285
|
-
function estimateTokensJson(obj) {
|
|
286
|
-
if (!obj) return 0;
|
|
287
|
-
try {
|
|
288
|
-
return estimateTokens(JSON.stringify(obj));
|
|
289
|
-
} catch {
|
|
290
|
-
return 0;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
function computeHistoryBudget(input) {
|
|
294
|
-
const cfg = { ...DEFAULT_COMPACTION_CONFIG, ...input.config ?? {} };
|
|
295
|
-
const systemTokens = estimateTokens(input.systemPrompt);
|
|
296
|
-
const toolsTokens = estimateTokensJson(input.toolsArray);
|
|
297
|
-
const autocompactBuffer = Math.floor(cfg.contextWindow * cfg.autocompactBufferPct);
|
|
298
|
-
const fixed = systemTokens + toolsTokens + cfg.reservedThinking + cfg.reservedGeneration + autocompactBuffer + cfg.safetyMargin;
|
|
299
|
-
const historyBudget = Math.max(cfg.minHistoryBudget, cfg.contextWindow - fixed);
|
|
300
|
-
return {
|
|
301
|
-
historyBudget,
|
|
302
|
-
breakdown: {
|
|
303
|
-
contextWindow: cfg.contextWindow,
|
|
304
|
-
systemPrompt: systemTokens,
|
|
305
|
-
tools: toolsTokens,
|
|
306
|
-
reservedThinking: cfg.reservedThinking,
|
|
307
|
-
reservedGeneration: cfg.reservedGeneration,
|
|
308
|
-
autocompactBuffer,
|
|
309
|
-
safetyMargin: cfg.safetyMargin,
|
|
310
|
-
historyAvailable: historyBudget
|
|
311
|
-
},
|
|
312
|
-
config: cfg
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
function splitHistoryBudget(historyBudget, cfg = DEFAULT_COMPACTION_CONFIG) {
|
|
316
|
-
const summary = Math.min(cfg.summaryMaxTokens, Math.floor(historyBudget * cfg.summaryRatio));
|
|
317
|
-
const ragHist = Math.min(cfg.ragHistMaxTokens, Math.floor(historyBudget * cfg.ragHistRatio));
|
|
318
|
-
const window = historyBudget - summary - ragHist;
|
|
319
|
-
return { summary, ragHist, window };
|
|
320
|
-
}
|
|
321
|
-
function assembleCompacted(params) {
|
|
322
|
-
const {
|
|
323
|
-
messages,
|
|
324
|
-
summary = "",
|
|
325
|
-
retrievedTurns = [],
|
|
326
|
-
windowBudget,
|
|
327
|
-
summaryBudget,
|
|
328
|
-
ragBudget,
|
|
329
|
-
systemContent,
|
|
330
|
-
summaryLabel = DEFAULT_COMPACTION_CONFIG.summaryLabel,
|
|
331
|
-
ragIntroLabel = DEFAULT_COMPACTION_CONFIG.ragIntroLabel
|
|
332
|
-
} = params;
|
|
333
|
-
const out = [];
|
|
334
|
-
const used = { system: 0, summary: 0, ragHist: 0, window: 0 };
|
|
335
|
-
const dropped = { ragHits: 0, oldTurns: 0, summary: false };
|
|
336
|
-
if (systemContent) {
|
|
337
|
-
out.push({ role: "system", content: systemContent });
|
|
338
|
-
used.system = estimateTokens(systemContent);
|
|
339
|
-
}
|
|
340
|
-
if (summary) {
|
|
341
|
-
const summaryWrapped = `${summaryLabel}
|
|
342
|
-
${summary}`;
|
|
343
|
-
const summaryToks = estimateTokens(summaryWrapped);
|
|
344
|
-
if (summaryToks <= summaryBudget) {
|
|
345
|
-
out.push({ role: "system", content: summaryWrapped });
|
|
346
|
-
used.summary = summaryToks;
|
|
347
|
-
} else {
|
|
348
|
-
const ratio = summaryBudget / summaryToks * 0.95;
|
|
349
|
-
const truncated = summary.slice(0, Math.max(0, Math.floor(summary.length * ratio)));
|
|
350
|
-
const wrapped = `${summaryLabel}
|
|
351
|
-
${truncated}…`;
|
|
352
|
-
out.push({ role: "system", content: wrapped });
|
|
353
|
-
used.summary = estimateTokens(wrapped);
|
|
354
|
-
dropped.summary = true;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
if (retrievedTurns.length > 0) {
|
|
358
|
-
const sorted = [...retrievedTurns].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
359
|
-
const lines = [];
|
|
360
|
-
const intro = `${ragIntroLabel}
|
|
361
|
-
`;
|
|
362
|
-
let toks = estimateTokens(intro);
|
|
363
|
-
for (const t of sorted) {
|
|
364
|
-
const line = `[${t.role}] ${t.content}`;
|
|
365
|
-
const lt = estimateTokens(line);
|
|
366
|
-
if (toks + lt > ragBudget) {
|
|
367
|
-
dropped.ragHits++;
|
|
368
|
-
continue;
|
|
369
|
-
}
|
|
370
|
-
lines.push(line);
|
|
371
|
-
toks += lt;
|
|
372
|
-
}
|
|
373
|
-
if (lines.length > 0) {
|
|
374
|
-
const content = intro + lines.join("\n");
|
|
375
|
-
out.push({ role: "system", content });
|
|
376
|
-
used.ragHist = estimateTokens(content);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
const nonSystem = messages.filter((m) => m.role !== "system");
|
|
380
|
-
if (nonSystem.length === 0) {
|
|
381
|
-
return { compactedMessages: out, used, dropped };
|
|
382
|
-
}
|
|
383
|
-
const reversedWindow = [];
|
|
384
|
-
let windowToks = 0;
|
|
385
|
-
const minKeep = Math.min(2, nonSystem.length);
|
|
386
|
-
for (let i = nonSystem.length - 1; i >= 0; i--) {
|
|
387
|
-
const m = nonSystem[i];
|
|
388
|
-
if (!m) continue;
|
|
389
|
-
const t = estimateTokens(m.content);
|
|
390
|
-
const idxFromEnd = nonSystem.length - 1 - i;
|
|
391
|
-
if (idxFromEnd < minKeep) {
|
|
392
|
-
reversedWindow.unshift(m);
|
|
393
|
-
windowToks += t;
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
if (windowToks + t > windowBudget) {
|
|
397
|
-
dropped.oldTurns += i + 1;
|
|
398
|
-
break;
|
|
399
|
-
}
|
|
400
|
-
reversedWindow.unshift(m);
|
|
401
|
-
windowToks += t;
|
|
402
|
-
}
|
|
403
|
-
while (reversedWindow.length && reversedWindow[0]?.role === "tool") {
|
|
404
|
-
const orphan = reversedWindow.shift();
|
|
405
|
-
if (orphan) {
|
|
406
|
-
windowToks -= estimateTokens(orphan.content);
|
|
407
|
-
dropped.oldTurns += 1;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
out.push(...reversedWindow);
|
|
411
|
-
used.window = windowToks;
|
|
412
|
-
return { compactedMessages: out, used, dropped };
|
|
413
|
-
}
|
|
414
|
-
function compactConversation(input) {
|
|
415
|
-
const { messages, systemPrompt, toolsArray, summary, retrievedTurns, config } = input;
|
|
416
|
-
const budget = computeHistoryBudget({ systemPrompt, toolsArray, config });
|
|
417
|
-
const split = splitHistoryBudget(budget.historyBudget, budget.config);
|
|
418
|
-
const { compactedMessages, used, dropped } = assembleCompacted({
|
|
419
|
-
messages,
|
|
420
|
-
summary,
|
|
421
|
-
retrievedTurns,
|
|
422
|
-
windowBudget: split.window,
|
|
423
|
-
summaryBudget: split.summary,
|
|
424
|
-
ragBudget: split.ragHist,
|
|
425
|
-
systemContent: systemPrompt,
|
|
426
|
-
summaryLabel: budget.config.summaryLabel,
|
|
427
|
-
ragIntroLabel: budget.config.ragIntroLabel
|
|
428
|
-
});
|
|
429
|
-
const totalUsed = budget.breakdown.systemPrompt + budget.breakdown.tools + used.summary + used.ragHist + used.window;
|
|
430
|
-
const free = budget.config.contextWindow - totalUsed - budget.config.reservedThinking - budget.config.reservedGeneration - budget.breakdown.autocompactBuffer - budget.config.safetyMargin;
|
|
431
|
-
const breakdown = {
|
|
432
|
-
...budget.breakdown,
|
|
433
|
-
historyAllocated: split,
|
|
434
|
-
historyUsed: used,
|
|
435
|
-
totalUsed,
|
|
436
|
-
free,
|
|
437
|
-
dropped
|
|
438
|
-
};
|
|
439
|
-
return { compactedMessages, breakdown };
|
|
440
|
-
}
|
|
441
|
-
const read = (value) => typeof value === "function" ? value() : value;
|
|
442
|
-
const textOf = (message) => {
|
|
443
|
-
if (typeof message.content === "string" && message.content.length > 0) return message.content;
|
|
444
|
-
return (message.segments ?? []).map((segment) => {
|
|
445
|
-
const content = segment.content;
|
|
446
|
-
return typeof content === "string" ? content : "";
|
|
447
|
-
}).join("\n");
|
|
448
|
-
};
|
|
449
|
-
function createCompactionSelector(options) {
|
|
450
|
-
const minKeep = Math.max(0, options.minKeep ?? 2);
|
|
451
|
-
return (messages) => {
|
|
452
|
-
const contextWindow = read(options.contextWindow);
|
|
453
|
-
if (!contextWindow || contextWindow <= 0) return { keep: messages, drop: [] };
|
|
454
|
-
const budget = computeHistoryBudget({
|
|
455
|
-
systemPrompt: read(options.systemPrompt) ?? "",
|
|
456
|
-
toolsArray: read(options.tools),
|
|
457
|
-
config: { ...options.config, contextWindow }
|
|
458
|
-
});
|
|
459
|
-
const windowBudget = splitHistoryBudget(budget.historyBudget, budget.config).window;
|
|
460
|
-
let used = 0;
|
|
461
|
-
let cut = messages.length;
|
|
462
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
463
|
-
const cost = estimateTokens(textOf(messages[i]));
|
|
464
|
-
const kept = messages.length - 1 - i;
|
|
465
|
-
if (used + cost > windowBudget && kept >= minKeep) break;
|
|
466
|
-
used += cost;
|
|
467
|
-
cut = i;
|
|
468
|
-
}
|
|
469
|
-
if (cut === 0) return { keep: messages, drop: [] };
|
|
470
|
-
return { keep: messages.slice(cut), drop: messages.slice(0, cut) };
|
|
471
|
-
};
|
|
239
|
+
function errorText(error) {
|
|
240
|
+
if (error instanceof Error) return error.message || error.name;
|
|
241
|
+
return typeof error === "string" ? error : String(error);
|
|
472
242
|
}
|
|
473
243
|
export {
|
|
474
|
-
|
|
475
|
-
assembleCompacted,
|
|
476
|
-
compactConversation,
|
|
477
|
-
computeHistoryBudget,
|
|
478
|
-
createCompactionSelector,
|
|
479
|
-
deriveArtifactKind,
|
|
480
|
-
estimateTokens,
|
|
481
|
-
estimateTokensJson,
|
|
482
|
-
runStreamAgent,
|
|
483
|
-
splitHistoryBudget
|
|
244
|
+
runStreamAgent
|
|
484
245
|
};
|
|
485
246
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/agent/parsers/artifact-kind.ts","../src/agent/stream-run.ts","../src/conversation/compactor.ts","../src/conversation/selector.ts"],"sourcesContent":["/**\n * Map an artifact MIME type to a renderer kind — `'react'`, `'html'`, `'js'`, `'css'`,\n * `'svg'`, `'json'`, `'markdown'`, `'csv'`, `'text'` — or `fallback` (default\n * `'unknown'`) for anything else. Anthropic's `application/vnd.ant.*` namespace maps to\n * its suffix; exact standard MIMEs are matched first, then a substring rescue for\n * parameterised or vendor variants (`text/html; charset=utf-8`, `application/ld+json`).\n *\n * THE implementation: `@aparte/core` re-exports it under the same name. It used to\n * live in the XML artifact state machine, which the 2026-08-28 audit's D2 removed —\n * the core parser reads `<artifact>` tags natively — and this is the one piece of\n * that file the built-in `create_artifact` and the artifact card still need.\n *\n * @example\n * deriveArtifactKind('application/vnd.ant.react') // 'react'\n * deriveArtifactKind('text/html; charset=utf-8') // 'html'\n * deriveArtifactKind('font/woff2', 'text') // 'text' (fallback)\n */\nexport function deriveArtifactKind(mimeType: string, fallback = 'unknown'): string {\n const m = (mimeType || '').toLowerCase().trim();\n const ant = m.match(/^application\\/vnd\\.ant\\.([a-z0-9-]+)/);\n if (ant) return ant[1]!;\n if (m === 'text/html' || m === 'application/xhtml+xml') return 'html';\n if (m === 'application/javascript' || m === 'text/javascript') return 'js';\n if (m === 'text/css') return 'css';\n if (m === 'image/svg+xml') return 'svg';\n if (m === 'application/json') return 'json';\n if (m === 'text/markdown') return 'markdown';\n if (m === 'text/csv') return 'csv';\n if (m === 'text/plain') return 'text';\n if (m.includes('react')) return 'react';\n if (m.includes('html')) return 'html';\n if (m.includes('javascript')) return 'js';\n if (m.includes('css')) return 'css';\n if (m.includes('svg')) return 'svg';\n if (m.includes('json')) return 'json';\n if (m.includes('csv')) return 'csv';\n if (m.includes('markdown')) return 'markdown';\n return fallback;\n}\n","/**\n * stream-run.ts — framework-free structured-stream agent loop.\n *\n * The agent loop — extracted from core's inline `_streamLoop`, which it has since\n * replaced (audit 2026-08-28, D1): a `while(tool_use)` loop\n * that consumes a structured `AsyncIterable<StreamChatEvent>` from a transport,\n * runs approved tools, feeds their results back into the history, and re-calls\n * the transport until the model stops asking for tools. It performs **no DOM\n * work** — it emits {@link StreamRunEvent}s (in the inline loop's exact order) that\n * an adapter in `@aparte/core` turns into `targetElement.*` calls.\n *\n * Formerly the parity target of core's inline loop; now the one loop, its call\n * sequences pinned by core's `stream-parity` snapshots.\n * Scope: text · thinking · tool_use (+ HITL) · done · error · the built-in\n * create_artifact · synthetic toolChoice bypass. Code-fence promotion stays\n * adapter-side (it needs the core parser), and so do `<artifact>` tags in the\n * text: the core parser reads them natively. The raw / XML artifact modes and the\n * multi-phase pipeline were removed (audit 2026-08-28, D2) — orchestration is the\n * product's, and a mode nothing in this repository emitted was a contract\n * maintained for nobody.\n */\n\nimport type {\n StreamRunEmitter,\n StreamChatEvent,\n StreamChatRequest,\n StreamAgentMessage,\n StreamToolCall,\n StreamToolHandler,\n StreamToolConfig,\n StreamApprovalResolver,\n StreamUsage,\n} from './stream-events.js';\nimport { deriveArtifactKind } from './parsers/artifact-kind.js';\n\n/** Default per-tool-call handler timeout — mirrors `TOOL_HANDLER_TIMEOUT_MS`. */\nconst DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1000;\n/** Default global turn cap — mirrors `AparteClientOptions.maxTurns ?? 10`. */\nconst DEFAULT_MAX_TURNS = 10;\n\nexport interface StreamRunOptions {\n /** Id of the assistant message being streamed (opaque; carried in events). */\n messageId: string;\n /** Turn-1 request; the loop clones its `messages` and enriches them per turn. */\n baseRequest: StreamChatRequest;\n /**\n * Calls the transport with the (possibly enriched) request. Returns the\n * structured stream, or a plain string for a non-streaming provider. Mirrors\n * `getTransport().chat(provider, request, auth, ctx)` with provider/auth/ctx\n * closed over by the adapter.\n */\n transportCall: (request: StreamChatRequest) => Promise<AsyncIterable<StreamChatEvent> | string>;\n /** Resolves a tool's handler by name (mirrors `aparteGlobalConfig.getToolHandler`). */\n toolLookup: (name: string) => StreamToolHandler | undefined;\n /** Resolves a tool's loop config by name (maxTurns / needsApproval). */\n toolConfigLookup?: (name: string) => StreamToolConfig | undefined;\n /** HITL approval resolver for `needsApproval` tools (default: never called). */\n approvalResolver?: StreamApprovalResolver;\n /** Synchronous, ordered event sink consumed by the adapter. */\n emitter: StreamRunEmitter;\n /** Single abort signal composing `_isAborted` + the stream controller. */\n signal: AbortSignal;\n /** Global turn cap. @default 10 */\n maxTurns?: number;\n /** Per-tool-call handler timeout in ms. @default 300000 */\n toolTimeoutMs?: number;\n /**\n * Called for every turn the loop appends to the history — the grouped\n * `tool_call` envelope, each `tool_result` (resolved or rejected), and a\n * pipeline phase's reply — in order, and always before the transport call that\n * would carry it. Never called for the messages you passed in `baseRequest`.\n *\n * For hosts that own their own transcript. The loop re-sends its `messages`\n * array every turn, which suits stateless message APIs but not a **prefix\n * cache** (llama.cpp slots, vLLM), where turn N+1 must EXTEND turn N byte for\n * byte. Such a host builds its own request in {@link transportCall} — ignoring\n * `request.messages` — and mirrors these notifications into its append-only\n * log, instead of reimplementing the loop's tool bookkeeping. Synchronous and\n * ordered, like {@link emitter}.\n *\n * One rule about the envelope: a turn's `tool_call` message is reported ONCE, when\n * its first call completes, and the calls that complete later in the same turn are\n * already in that same object's `toolCalls` — the array is shared by reference, not\n * copied. Hold the reference, not a snapshot, and every result you receive after it\n * is declared by it.\n */\n onHistoryAppend?: (message: StreamAgentMessage) => void;\n /**\n * Generates artifact segment ids (`prefix` is e.g. `'artifact-raw'`). The\n * default is a deterministic per-run counter; the adapter injects a\n * crypto-based one to match `_streamLoop`'s `artifact-*-<uuid>`. (Tool ids\n * still flow from the stream; only artifacts need generated ids.)\n */\n idGen?: (prefix: string) => string;\n}\n\n/** Warn once per event type for something this version does not map, then skip it. */\nconst warnedUnknownEvents = new Set<string>();\nfunction warnUnknownStreamEvent(event: unknown): void {\n const type = String((event as { type?: unknown })?.type ?? 'undefined');\n if (warnedUnknownEvents.has(type)) return;\n warnedUnknownEvents.add(type);\n console.warn(\n `[runStreamAgent] Ignoring unrecognised stream event \"${type}\". The reply is`\n + ' unaffected; this usually means the provider or its SDK emits a part this'\n + ' version of aparté does not map yet.',\n );\n}\n\n/**\n * Run the structured-stream agent loop. Resolves the last turn's usage (the\n * `done{usage}` last-write-wins, mirroring `_streamLoop`'s return), or\n * `undefined`. Throws on a stream `error` event or a non-abort tool failure —\n * the caller (adapter) routes that to its lifecycle-error handler, exactly as\n * `_handleSend`/`_handleRetry`/`_handleEdit` catch `_streamLoop`.\n *\n * This block has to stay ADJACENT to the function. It used to sit above\n * `warnUnknownStreamEvent`, six lines and a docblock away from what it describes, and a\n * docblock separated from its declaration is attached to nothing: TypeDoc took the\n * helper's comment as the neighbouring one and `@aparte/engine`'s reference page carried\n * the package's headline export with no description at all — the very export its own\n * frontmatter names. Same trap the custom-elements analyser has with a class docblock\n * pushed above an import.\n */\nexport async function runStreamAgent(opts: StreamRunOptions): Promise<StreamUsage | undefined> {\n const {\n transportCall,\n toolLookup,\n toolConfigLookup,\n approvalResolver,\n emitter,\n signal,\n maxTurns = DEFAULT_MAX_TURNS,\n toolTimeoutMs = DEFAULT_TOOL_TIMEOUT_MS,\n } = opts;\n\n // Reassignable: the synthetic-toolChoice bypass strips `toolChoice`/`tools`\n // from it after the forced turn-1 handler runs (mirrors `_streamLoop`'s\n // `baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined }`).\n let baseRequest = opts.baseRequest;\n\n let idSeq = 0;\n const idGen = opts.idGen ?? ((prefix: string) => `${prefix}-${idSeq++}`);\n\n // Mutable history the loop enriches with tool_call/tool_result turns. Every\n // enrichment goes through `append`, so a caller owning its own transcript sees\n // the same turns in the same order (see `onHistoryAppend`).\n const messages: StreamAgentMessage[] = [...baseRequest.messages];\n const append = (message: StreamAgentMessage): void => {\n messages.push(message);\n opts.onHistoryAppend?.(message);\n };\n\n let continueLoop = true;\n let turns = 0;\n let lastUsage: StreamUsage | undefined;\n\n // Leading write before turn 1: mark the message streaming.\n emitter({ type: 'run-start' });\n\n // ── outer (turn) loop — one iteration = one transport call ────────────────\n while (continueLoop) {\n // Abort at the top of a turn: no stream open yet, so no cancel needed.\n if (signal.aborted) {\n emitter({ type: 'run-aborted' });\n break;\n }\n\n turns++;\n if (turns > maxTurns) {\n emitter({ type: 'turn-limit-exceeded', scope: 'global', limit: maxTurns });\n break;\n }\n\n // ── Synthetic toolChoice bypass (mirrors _streamLoop) ──────\n // toolChoice = { name, input } (orchestrator-forced): skip the LLM for\n // turn 1, run the handler directly, inject its result as a tool_result,\n // then strip toolChoice/tools and fall through to the transport call in\n // the SAME turn so the model answers with the tool result already in\n // history. (The adapter's tool-start handler injects renderer CSS here\n // too — a benign, idempotent gain over _streamLoop's synthetic path,\n // which skipped it; reconciled in the parity test.)\n const toolChoice = baseRequest['toolChoice'];\n if (\n turns === 1 &&\n toolChoice &&\n typeof toolChoice === 'object' &&\n !Array.isArray(toolChoice) &&\n (toolChoice as { input?: unknown }).input !== undefined\n ) {\n // Narrowed to the declared shape rather than `input: unknown`: now that\n // `StreamChatRequest.toolChoice` mirrors core's type, the cast can be exact.\n const tc = toolChoice as { name: string; input: Record<string, unknown> };\n const syntheticId = idGen('synthetic-tool');\n emitter({ type: 'tool-start', toolCallId: syntheticId, name: tc.name, input: tc.input });\n\n const handler = toolLookup(tc.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n\n const outcome = await invokeToolHandler(handler, { id: syntheticId, name: tc.name, input: tc.input }, signal, toolTimeoutMs);\n if (outcome.status === 'aborted') {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n emitter({ type: 'tool-resolved', toolCallId: syntheticId, result: outcome.content });\n append({ role: 'tool_call', content: '', toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });\n append({ role: 'tool_result', content: outcome.content, toolCallId: syntheticId });\n baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined };\n }\n\n // A stop that lands before the first event surfaces here as a REJECTION,\n // not as an `error` event — the fetch is aborted while still in flight, so\n // it never reaches the stream the loop reads. Letting it propagate made a\n // deliberate stop indistinguishable from a transport failure for anyone\n // driving this loop directly. (Core's `_streamTurn` has the mirror of this\n // guard; the browser suite is what found the path, after the two\n // event-level guards were already closed.)\n const request: StreamChatRequest = { ...baseRequest, messages };\n let response: Awaited<ReturnType<typeof transportCall>>;\n try {\n response = await transportCall(request);\n } catch (err: unknown) {\n if (signal.aborted || (err as { name?: string } | undefined)?.name === 'AbortError') {\n // `break`, not `return`: the post-loop `run-done` below is documented\n // as running on EVERY exit path, and five of the six abort paths fall\n // through to it. Returning here skipped it, so a consumer wiring\n // `runStreamAgent` + `createStreamAdapter` directly never got the\n // `completed` status and the bubble stayed flagged streaming — which\n // is verbatim the defect the client-side guard was added to fix.\n emitter({ type: 'run-aborted' });\n continueLoop = false;\n break;\n }\n throw err;\n }\n\n // Non-streaming provider: the string IS the full assistant message. The\n // adapter writes it and completes (no done{usage}); spike scenarios\n // always stream, so we finish the run here.\n //\n // `text-flush` is not optional here. It is the adapter's ONLY caller of\n // `parser.finalize()`, and the parser deliberately withholds an ambiguous\n // tail — a lone backtick, a triple backtick, a `<`, or the safe window\n // inside an unterminated fence. Breaking out before the flush dropped all\n // of it, so a non-streaming reply ending on any of those characters lost\n // them and one consisting only of them rendered nothing at all. Core's\n // inline loop does `parse()` AND `finalize()` on its whole-response\n // branch; this is the twin.\n if (typeof response === 'string') {\n emitter({ type: 'text-delta', delta: response });\n emitter({ type: 'text-flush' });\n break;\n }\n\n // New turn: the adapter resets its per-turn parser / thinking / streaming\n // state here (mirrors `_streamLoop` creating a fresh AparteStreamParser +\n // sets each outer iteration). No DOM effect of its own.\n emitter({ type: 'turn-start' });\n\n // Per-turn streaming state.\n let precedingText = '';\n const toolCallsThisTurn: StreamToolCall[] = [];\n /**\n * The turn's ONE `tool_call` envelope, held by reference. Created — and\n * appended, so the host is notified once — the first time a call needs it;\n * every later call of the turn is already in the SAME `toolCalls` array, so a\n * `tool_result` appended afterwards is always declared by it.\n *\n * This replaces an id scan over `messages` that guessed whether the envelope\n * was already there. The `create_artifact` fast path pushed a fresh envelope\n * of its own; the scan then found the artifact's id in it, concluded \"already\n * pushed\", and the next tool's result went out with no call declaring it —\n * the P0 of the 2026-08-28 audit, a history an Anthropic-shaped API rejects\n * outright. A reference cannot be guessed wrong.\n */\n let envelope: StreamAgentMessage | null = null;\n const ensureEnvelope = (): void => {\n if (envelope) return;\n envelope = { role: 'tool_call', content: '', toolCalls: toolCallsThisTurn, precedingText: precedingText.trim() || undefined };\n append(envelope);\n };\n\n // ── inner (SSE) loop — manual iteration so we can abort around each read.\n // Checked on BOTH sides of the read, and the second check is the one that\n // matters: the loop spends nearly all of its time parked on `next()`, so an\n // abort arriving while parked — the user pressing Stop while watching text\n // stream — is invisible to a check that only runs before the await. It also\n // covers both shapes a provider takes on abort: an `error` event or a quiet\n // close. Without it the `error` branch below throws and the caller reports a\n // deliberate stop as a failure. Mirrors `_streamLoop`'s `bailOnAbort`.\n const iterator = response[Symbol.asyncIterator]();\n const bailOnAbort = async (): Promise<boolean> => {\n if (!signal.aborted) return false;\n await iterator.return?.(undefined);\n emitter({ type: 'run-aborted' });\n continueLoop = false;\n return true;\n };\n try {\n while (true) {\n if (await bailOnAbort()) break;\n\n const step = await iterator.next();\n if (await bailOnAbort()) break;\n if (step.done) break;\n const event = step.value;\n\n if (event.type === 'thinking') {\n emitter({ type: 'thinking-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'text') {\n precedingText += event.delta;\n emitter({ type: 'text-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'done') {\n if (event.usage) lastUsage = event.usage;\n continue;\n }\n\n if (event.type === 'error') {\n // Mirror `_streamLoop`: throw; the caller's catch handles it.\n throw new Error(event.message);\n }\n\n // A DISCRIMINANT GUARD, not a comment.\n //\n // This used to be the bare comment `// event.type === 'tool_use'`, so\n // anything the chain above did not recognise fell in here and was\n // treated as a tool call: a `tool-start` / `tool-aborted` pair with an\n // undefined id and name, and — worse — the rest of the stream was\n // never processed, so the reply was truncated too.\n //\n // Core's inline loop had the opposite failure on the same input (it\n // threw and replaced the reply with an error bubble). Both now ignore\n // the event and carry on, which is what forward compatibility with a\n // provider SDK actually requires.\n if (event.type !== 'tool_use') {\n warnUnknownStreamEvent(event);\n continue;\n }\n toolCallsThisTurn.push({ id: event.id, name: event.name, input: event.input });\n\n // Built-in create_artifact: bypass the generic tool path entirely\n // (no tool-start, no approval, no handler) — build the artifact\n // one-shot and inject a success tool_result (mirrors the create_artifact fast-path in _streamLoop).\n if (event.name === 'create_artifact') {\n const input = (event.input ?? {}) as { mimeType?: string; title?: string; content?: string };\n const mimeType = input.mimeType ?? 'text/plain';\n const kind = deriveArtifactKind(mimeType, 'text');\n emitter({ type: 'artifact-ready', id: `artifact-${event.id}`, mimeType, kind, title: input.title ?? kind, content: input.content ?? '' });\n // The turn's envelope, not one of its own: a fresh `[create_artifact]`\n // envelope here is what orphaned the next tool's result.\n ensureEnvelope();\n append({ role: 'tool_result', content: 'Artifact created successfully.', toolCallId: event.id });\n continue;\n }\n\n emitter({ type: 'tool-start', toolCallId: event.id, name: event.name, input: event.input });\n\n const cfg = toolConfigLookup?.(event.name);\n\n // Per-tool maxTurns (note: `>=`, stricter than the global `>`).\n const effectiveMaxTurns = cfg?.maxTurns ?? maxTurns;\n if (turns >= effectiveMaxTurns) {\n emitter({ type: 'turn-limit-exceeded', scope: 'tool', limit: effectiveMaxTurns, toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n const handler = toolLookup(event.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n let effectiveInput = event.input;\n\n // ── HITL approval gate ────────────────────────────────────────\n if (cfg?.needsApproval) {\n emitter({ type: 'tool-awaiting-approval', toolCallId: event.id, name: event.name, input: event.input });\n /*\n * Nothing can ask, so nothing may answer.\n *\n * The default was `async () => ({ approved: false })`, which sent the\n * loop into the refusal branch and appended \"rejected by the user\" —\n * a host that forgot to wire a resolver had not refused anything, and\n * the model was told a person had. A misconfiguration is an aborted\n * call, not a decision.\n */\n if (!approvalResolver) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n /*\n * A stop is not a refusal either. Core's built-in channel resolves\n * `{ approved: false }` on abort, indistinguishable by value from an\n * explicit Reject — so the signal is what tells them apart. No\n * `tool_result`: an aborted call has nothing true to tell the model.\n *\n * Checked on three sides of the wait, because the stop can land on\n * any of them: while the gate was being shown (a host that reacts to\n * `awaiting-approval` synchronously — the panel's own Stop button),\n * inside the resolver (a channel that rejects with an AbortError\n * rather than resolving), or after it resolved. The first and second\n * used to fall through to the run-level abort, which never marked the\n * call: the segment stayed `awaiting-approval` for good.\n */\n let decision: Awaited<ReturnType<typeof approvalResolver>> | undefined;\n if (!signal.aborted) {\n try {\n decision = await approvalResolver(\n { id: event.id, name: event.name, input: event.input as Record<string, unknown> },\n signal,\n );\n } catch (err) {\n if (!signal.aborted && (err as { name?: string } | undefined)?.name !== 'AbortError') throw err;\n }\n }\n if (signal.aborted || !decision) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n if (!decision.approved) {\n /*\n * The user's own words when they gave them, and the fixed sentence\n * otherwise — byte-identical to core's twin, because this is the\n * only thing the model gets to read about the refusal.\n *\n * This branch used to hardcode the generic sentence. The resolver\n * type declared `instruction` and the whole loop never referenced\n * it, so on the engine path — the recommended one — a user who\n * refused and typed \"use the staging bucket instead\" had those\n * words dropped before the model saw them. Handing the model a\n * turn after a refusal exists SO THAT it reads the refusal; here\n * there was nothing to read.\n *\n * The parity suite could not see it: its resolver returned\n * `{ approved }` and never an instruction, so both loops agreed on\n * a case neither exercised. It supplies one now.\n */\n const rejection = decision.instruction\n ? `The user rejected this tool call and said: ${decision.instruction}`\n : 'Tool execution was rejected by the user.';\n emitter({ type: 'tool-rejected', toolCallId: event.id, reason: rejection });\n ensureEnvelope();\n append({ role: 'tool_result', content: rejection, toolCallId: event.id });\n /*\n * `break` WITHOUT `continueLoop = false`, and the asymmetry is the\n * point. The remaining tool calls of this turn must not run — the\n * model asked for several and refusing one cannot license the\n * others — but the loop takes another turn, so the sentence just\n * appended actually reaches the model. It never did: the turn\n * ended here, and telling the assistant what you wanted instead\n * meant retyping it as a new message it read out of order.\n *\n * Core's twin returns `'respond'` for exactly this, and the parity\n * suite asserts the two agree.\n */\n break;\n }\n if (decision.payload && typeof decision.payload === 'object' && !Array.isArray(decision.payload)) {\n effectiveInput = { ...(event.input as Record<string, unknown>), ...(decision.payload as Record<string, unknown>) };\n }\n emitter({ type: 'tool-approved', toolCallId: event.id });\n }\n\n // ── handler invocation with per-call timeout ──────────────────\n const outcome = await invokeToolHandler(\n handler,\n { id: event.id, name: event.name, input: effectiveInput },\n signal,\n toolTimeoutMs,\n );\n if (outcome.status === 'aborted') {\n // Same rule as the rejection / turn-limit / missing-handler\n // exits above, which this branch was missing: a stopped turn\n // must not go on running the tool calls that follow it.\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n } else {\n emitter({ type: 'tool-resolved', toolCallId: event.id, result: outcome.content });\n ensureEnvelope();\n append({ role: 'tool_result', content: outcome.content, toolCallId: event.id });\n }\n }\n\n // Turn boundary: finalize the parser (flush residual text). Mirrors\n // `_streamLoop`'s `textParser.finalize()` call — runs on normal\n // end AND abort-break, but NOT after a thrown `error` (which escapes\n // this try before reaching here, exactly like `_streamLoop`).\n emitter({ type: 'text-flush' });\n\n } finally {\n // Mirror `reader.releaseLock()` in the finally: settle the iterator.\n await iterator.return?.(undefined).catch(() => { /* best effort */ });\n }\n\n // No tool calls this turn → the final answer.\n if (toolCallsThisTurn.length === 0) continueLoop = false;\n }\n\n // Post-loop finalization runs on every exit path (normal / abort / maxTurns).\n emitter({ type: 'run-done', usage: lastUsage });\n return lastUsage;\n}\n\n/**\n * Run a tool handler with a per-call timeout, linked to the run's abort signal.\n * Resolves `{ status: 'resolved', content }` on success, or `{ status: 'aborted' }`\n * if the handler aborts (timeout OR parent abort); any other error is re-thrown so\n * the caller routes it to lifecycle-error, exactly as `_streamLoop` does. Shared\n * by the generic tool path and the synthetic-toolChoice bypass (mirrors the\n * AbortController + `TOOL_HANDLER_TIMEOUT_MS` dance both use in `_streamLoop`).\n */\nasync function invokeToolHandler(\n handler: StreamToolHandler,\n call: StreamToolCall,\n signal: AbortSignal,\n toolTimeoutMs: number,\n): Promise<{ status: 'resolved'; content: string } | { status: 'aborted' }> {\n // If the run was already aborted before we got here, don't invoke the\n // handler at all: a past 'abort' event will never re-fire on the listener\n // below, so the handler would otherwise run to completion despite cancel.\n if (signal.aborted) return { status: 'aborted' };\n\n const controller = new AbortController();\n const onParentAbort = () => controller.abort();\n signal.addEventListener('abort', onParentAbort, { once: true });\n\n /*\n * RACED, not just signalled.\n *\n * Aborting the controller is a request the handler is free to ignore, and the\n * default shape of a consumer tool ignores it —\n * `async () => ({ content: await fetch(...).then(r => r.text()) })` never reads\n * its signal. So the timeout fired, nothing rejected, and the loop waited\n * forever on an option whose JSDoc promises a timeout.\n *\n * The signal still fires first, because a handler that DOES honour it should\n * get the chance to clean up and reject on its own terms; the race is what\n * makes the promise true when it does not.\n */\n let timeout: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<{ status: 'aborted' }>((resolve) => {\n timeout = setTimeout(() => {\n controller.abort();\n resolve({ status: 'aborted' });\n }, toolTimeoutMs);\n });\n\n try {\n const result = await Promise.race([\n handler(call, controller.signal).then((r) => ({ status: 'resolved' as const, content: r.content })),\n timedOut,\n ]);\n return result;\n } catch (err: unknown) {\n if ((err as { name?: string })?.name === 'AbortError') return { status: 'aborted' };\n throw err;\n } finally {\n clearTimeout(timeout);\n signal.removeEventListener('abort', onParentAbort);\n }\n}\n","/**\n * conversation/compactor.ts — adaptive budgeting + context-window assembly.\n *\n * Conversation history budgeting and sliding-window assembly —\n * framework-free and tokenizer-free (char-count heuristic).\n *\n * Pattern (cf. Claude Code /context, OpenAI Agents SDK TrimmingSession):\n * Budget = CONTEXT_WINDOW − systemPrompt − tools − reservedThinking\n * − reservedGeneration − autocompactBuffer − safetyMargin\n *\n * The history budget is then split into:\n * - summary (global summary, LLM-generated, ~10%)\n * - ragHist (older turns retrieved by cosine similarity, ~25%)\n * - window (verbatim sliding window, the rest)\n *\n * Drop priority on overflow:\n * 1. summary (async-regenerable)\n * 2. ragHist (retrievable on the next turn)\n * 3. window oldest turns\n * X. NEVER dropped: currentUser + lastAssistant (absolute working memory)\n *\n * Browser-portable: zero deps, just an estimateTokens heuristic.\n */\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface CompactionMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string;\n}\n\nexport interface RetrievedTurn {\n role: 'user' | 'assistant';\n content: string;\n score?: number;\n}\n\nexport interface CompactionConfig {\n /** Total context window of the active model, in tokens. */\n contextWindow: number;\n /** Reserved budget for the model's thinking/reasoning block, in tokens. */\n reservedThinking: number;\n /** Reserved budget for the assistant response (max_new_tokens cap). */\n reservedGeneration: number;\n /** Fraction of context_window kept as autocompact buffer (0..1). */\n autocompactBufferPct: number;\n /** Hard safety margin in tokens. */\n safetyMargin: number;\n /** Floor for history budget — never compact below this. */\n minHistoryBudget: number;\n /** Ratio of history budget allocated to the summary block. */\n summaryRatio: number;\n /** Hard cap for summary tokens. */\n summaryMaxTokens: number;\n /** Ratio of history budget allocated to the RAG-retrieved old turns. */\n ragHistRatio: number;\n /** Hard cap for ragHist tokens. */\n ragHistMaxTokens: number;\n /** Trigger summarization once history usage reaches this % of the budget. */\n triggerSummaryThresholdPct: number;\n /** Re-run summarization every N user turns. */\n summarizeEveryNTurns: number;\n /** Header prepended to the summary block injected into the model context. English\n * by default — override to localise (the compactor ships no locale system). */\n summaryLabel: string;\n /** Header prepended to the RAG-retrieved old turns injected into the context. */\n ragIntroLabel: string;\n}\n\nexport interface BudgetBreakdown {\n contextWindow: number;\n systemPrompt: number;\n tools: number;\n reservedThinking: number;\n reservedGeneration: number;\n autocompactBuffer: number;\n safetyMargin: number;\n historyAvailable: number;\n}\n\nexport interface BudgetResult {\n historyBudget: number;\n breakdown: BudgetBreakdown;\n config: CompactionConfig;\n}\n\nexport interface SplitBudget {\n summary: number;\n ragHist: number;\n window: number;\n}\n\nexport interface UsageBreakdown {\n system: number;\n summary: number;\n ragHist: number;\n window: number;\n}\n\nexport interface DroppedBreakdown {\n ragHits: number;\n oldTurns: number;\n summary: boolean;\n}\n\nexport interface FullBreakdown extends BudgetBreakdown {\n historyAllocated: SplitBudget;\n historyUsed: UsageBreakdown;\n totalUsed: number;\n /** Free tokens left in the context window after all reservations + actual use. */\n free: number;\n dropped: DroppedBreakdown;\n}\n\nexport interface CompactionInput {\n messages: CompactionMessage[];\n systemPrompt: string;\n /** Tools array passed to apply_chat_template (or null/undefined when no tools). */\n toolsArray?: unknown;\n /** Running summary text (regenerable). */\n summary?: string;\n /** RAG-retrieved old turns (older than the sliding window). */\n retrievedTurns?: RetrievedTurn[];\n /** Partial override of the default config. */\n config?: Partial<CompactionConfig>;\n}\n\nexport interface CompactionResult {\n compactedMessages: CompactionMessage[];\n breakdown: FullBreakdown;\n}\n\n// ─── Constants ──────────────────────────────────────────────────────────────\n\nexport const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {\n // Conservative model-agnostic defaults — the consuming app overrides\n // `contextWindow` / `reservedThinking` with its model's real values.\n contextWindow: 8192,\n reservedThinking: 0,\n reservedGeneration: 2000,\n autocompactBufferPct: 0.10, // ~3300 tok\n safetyMargin: 500,\n minHistoryBudget: 1000,\n\n summaryRatio: 0.10,\n summaryMaxTokens: 400,\n ragHistRatio: 0.25,\n ragHistMaxTokens: 1000,\n\n triggerSummaryThresholdPct: 0.75,\n summarizeEveryNTurns: 5,\n\n summaryLabel: 'Conversation summary:',\n ragIntroLabel: 'Relevant excerpts from earlier in the conversation:',\n};\n\n// ─── Token estimation ──────────────────────────────────────────────────────\n\n/**\n * Token heuristic (FR ~3.5 chars/tok, EN ~4 chars/tok).\n * Accurate to ±10% — enough for budgeting, and it avoids `tokenizer.encode()`,\n * which costs ~5ms per message × N (too slow to run on every turn).\n */\nexport function estimateTokens(text: string | null | undefined): number {\n if (!text) return 0;\n return Math.ceil(text.length / 3.8);\n}\n\n/**\n * Estimate tokens for a JSON-serializable structure (e.g. tools array).\n */\nexport function estimateTokensJson(obj: unknown): number {\n if (!obj) return 0;\n try {\n return estimateTokens(JSON.stringify(obj));\n } catch {\n return 0;\n }\n}\n\n// ─── Budget computation ────────────────────────────────────────────────────\n\n/**\n * Compute the available history budget after subtracting fixed costs.\n */\nexport function computeHistoryBudget(input: {\n systemPrompt: string;\n toolsArray?: unknown;\n config?: Partial<CompactionConfig>;\n}): BudgetResult {\n const cfg: CompactionConfig = { ...DEFAULT_COMPACTION_CONFIG, ...(input.config ?? {}) };\n\n const systemTokens = estimateTokens(input.systemPrompt);\n const toolsTokens = estimateTokensJson(input.toolsArray);\n const autocompactBuffer = Math.floor(cfg.contextWindow * cfg.autocompactBufferPct);\n\n const fixed = systemTokens + toolsTokens\n + cfg.reservedThinking + cfg.reservedGeneration\n + autocompactBuffer + cfg.safetyMargin;\n\n const historyBudget = Math.max(cfg.minHistoryBudget, cfg.contextWindow - fixed);\n\n return {\n historyBudget,\n breakdown: {\n contextWindow: cfg.contextWindow,\n systemPrompt: systemTokens,\n tools: toolsTokens,\n reservedThinking: cfg.reservedThinking,\n reservedGeneration: cfg.reservedGeneration,\n autocompactBuffer,\n safetyMargin: cfg.safetyMargin,\n historyAvailable: historyBudget,\n },\n config: cfg,\n };\n}\n\n/**\n * Split history budget into summary / ragHist / window slots.\n */\nexport function splitHistoryBudget(\n historyBudget: number,\n cfg: CompactionConfig = DEFAULT_COMPACTION_CONFIG,\n): SplitBudget {\n const summary = Math.min(cfg.summaryMaxTokens, Math.floor(historyBudget * cfg.summaryRatio));\n const ragHist = Math.min(cfg.ragHistMaxTokens, Math.floor(historyBudget * cfg.ragHistRatio));\n const window = historyBudget - summary - ragHist;\n return { summary, ragHist, window };\n}\n\n// ─── Message assembly ──────────────────────────────────────────────────────\n\ninterface AssembleParams {\n messages: CompactionMessage[];\n summary?: string;\n retrievedTurns?: RetrievedTurn[];\n windowBudget: number;\n summaryBudget: number;\n ragBudget: number;\n systemContent?: string;\n /** Context-injection headers; default to English (DEFAULT_COMPACTION_CONFIG). */\n summaryLabel?: string;\n ragIntroLabel?: string;\n}\n\ninterface AssembleResult {\n compactedMessages: CompactionMessage[];\n used: UsageBreakdown;\n dropped: DroppedBreakdown;\n}\n\n/**\n * Assemble compacted message list given a budget and conversation state.\n *\n * Output order:\n * [system?] → [summary system msg?] → [ragHist system msg?] → window verbatim\n *\n * Always keeps the last 2 non-system messages verbatim (working memory),\n * regardless of windowBudget — these are the floor that can never be dropped.\n */\nexport function assembleCompacted(params: AssembleParams): AssembleResult {\n const {\n messages,\n summary = '',\n retrievedTurns = [],\n windowBudget,\n summaryBudget,\n ragBudget,\n systemContent,\n summaryLabel = DEFAULT_COMPACTION_CONFIG.summaryLabel,\n ragIntroLabel = DEFAULT_COMPACTION_CONFIG.ragIntroLabel,\n } = params;\n\n const out: CompactionMessage[] = [];\n const used: UsageBreakdown = { system: 0, summary: 0, ragHist: 0, window: 0 };\n const dropped: DroppedBreakdown = { ragHits: 0, oldTurns: 0, summary: false };\n\n // System message first\n if (systemContent) {\n out.push({ role: 'system', content: systemContent });\n used.system = estimateTokens(systemContent);\n }\n\n // Summary\n if (summary) {\n const summaryWrapped = `${summaryLabel}\\n${summary}`;\n const summaryToks = estimateTokens(summaryWrapped);\n if (summaryToks <= summaryBudget) {\n out.push({ role: 'system', content: summaryWrapped });\n used.summary = summaryToks;\n } else {\n const ratio = (summaryBudget / summaryToks) * 0.95;\n const truncated = summary.slice(0, Math.max(0, Math.floor(summary.length * ratio)));\n const wrapped = `${summaryLabel}\\n${truncated}…`;\n out.push({ role: 'system', content: wrapped });\n used.summary = estimateTokens(wrapped);\n dropped.summary = true;\n }\n }\n\n // RAG hist — old turns retrieved by cosine similarity, sorted by score desc\n if (retrievedTurns.length > 0) {\n const sorted = [...retrievedTurns].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));\n const lines: string[] = [];\n const intro = `${ragIntroLabel}\\n`;\n let toks = estimateTokens(intro);\n for (const t of sorted) {\n const line = `[${t.role}] ${t.content}`;\n const lt = estimateTokens(line);\n if (toks + lt > ragBudget) {\n dropped.ragHits++;\n continue;\n }\n lines.push(line);\n toks += lt;\n }\n if (lines.length > 0) {\n const content = intro + lines.join('\\n');\n out.push({ role: 'system', content });\n used.ragHist = estimateTokens(content);\n }\n }\n\n // Sliding window verbatim — newest → oldest, until windowBudget exhausted.\n // Always keep the last 2 non-system messages (working memory floor).\n const nonSystem = messages.filter(m => m.role !== 'system');\n if (nonSystem.length === 0) {\n return { compactedMessages: out, used, dropped };\n }\n\n const reversedWindow: CompactionMessage[] = [];\n let windowToks = 0;\n const minKeep = Math.min(2, nonSystem.length);\n\n for (let i = nonSystem.length - 1; i >= 0; i--) {\n const m = nonSystem[i];\n if (!m) continue;\n const t = estimateTokens(m.content);\n const idxFromEnd = nonSystem.length - 1 - i;\n\n if (idxFromEnd < minKeep) {\n // Working memory floor — kept no matter the budget.\n reversedWindow.unshift(m);\n windowToks += t;\n continue;\n }\n\n if (windowToks + t > windowBudget) {\n dropped.oldTurns += i + 1;\n break;\n }\n reversedWindow.unshift(m);\n windowToks += t;\n }\n\n /*\n * A window must never BEGIN on a `tool` result.\n *\n * The walk is newest-to-oldest and keeps the last `minKeep` messages whatever\n * the budget, so when the second-to-last message is a tool result the compacted\n * history opens with `role: 'tool'` — an orphan whose requesting assistant turn\n * was dropped. Every OpenAI-compatible provider rejects that with a 400\n * (\"messages with role 'tool' must be a response to a preceding message with\n * tool_calls\"), so compaction turned a long conversation into an unusable one.\n *\n * Dropping the orphan rather than reaching further back for its assistant turn\n * is deliberate: `CompactionMessage` is `{ role, content }` and cannot represent\n * the pairing at all — no `toolCallId`, no `toolCalls` — so there is nothing to\n * reconstruct WITH. Its content already survives in the summary the window sits\n * under. Carrying the pairing properly means widening the type, which is a\n * design change and not a bug fix.\n */\n while (reversedWindow.length && reversedWindow[0]?.role === 'tool') {\n const orphan = reversedWindow.shift();\n if (orphan) {\n windowToks -= estimateTokens(orphan.content);\n dropped.oldTurns += 1;\n }\n }\n\n out.push(...reversedWindow);\n used.window = windowToks;\n\n return { compactedMessages: out, used, dropped };\n}\n\n// ─── High-level helper ─────────────────────────────────────────────────────\n\n/**\n * Compute budget + assemble in one call. The primary entry point for\n * consumers (a browser request interceptor, mobile, Node tests).\n */\nexport function compactConversation(input: CompactionInput): CompactionResult {\n const { messages, systemPrompt, toolsArray, summary, retrievedTurns, config } = input;\n\n const budget = computeHistoryBudget({ systemPrompt, toolsArray, config });\n const split = splitHistoryBudget(budget.historyBudget, budget.config);\n\n const { compactedMessages, used, dropped } = assembleCompacted({\n messages,\n summary,\n retrievedTurns,\n windowBudget: split.window,\n summaryBudget: split.summary,\n ragBudget: split.ragHist,\n systemContent: systemPrompt,\n summaryLabel: budget.config.summaryLabel,\n ragIntroLabel: budget.config.ragIntroLabel,\n });\n\n const totalUsed = budget.breakdown.systemPrompt\n + budget.breakdown.tools\n + used.summary\n + used.ragHist\n + used.window;\n\n const free = budget.config.contextWindow - totalUsed\n - budget.config.reservedThinking - budget.config.reservedGeneration\n - budget.breakdown.autocompactBuffer - budget.config.safetyMargin;\n\n const breakdown: FullBreakdown = {\n ...budget.breakdown,\n historyAllocated: split,\n historyUsed: used,\n totalUsed,\n free,\n dropped,\n };\n\n return { compactedMessages, breakdown };\n}\n","/**\n * conversation/selector.ts — the budget-aware `compactionSelector` for `AparteClient`.\n *\n * `AparteClient.compact()` decides what to summarise through one function:\n * `(messages) => { keep, drop }`. Its default drops everything — summarise all,\n * replace all. This is the other half the seam was written for: the newest turns\n * that still fit the history budget stay verbatim, the older ones are summarised.\n *\n * The budget is the compactor's (`computeHistoryBudget` + `splitHistoryBudget`), so\n * the gauge a page shows, the selector `compact()` uses and the window the model\n * declares all speak the same numbers. The budget is closed over here by the\n * consumer — the window is theirs to know, the client's to never guess.\n */\n\nimport { computeHistoryBudget, estimateTokens, splitHistoryBudget, type CompactionConfig } from './compactor.js';\n\n/**\n * The least a message must carry to be costed: its text, or segments with text.\n * Structural on purpose — this package imports nothing from core — and core's\n * `AparteMessage` satisfies it, which is what makes the selector below exactly the\n * `compactionSelector` `AparteClient` takes.\n */\nexport interface CompactableMessage {\n content?: string;\n /**\n * `unknown` elements, read defensively below: core's segment union includes a\n * segment with no `content` at all, and TypeScript refuses a type with no property\n * in common with the all-optional `{ content?: unknown }` (a \"weak type\"), so that\n * tighter shape made `AparteMessage` unassignable here.\n */\n segments?: ReadonlyArray<unknown>;\n}\n\n/** What `compact()` asks for: the messages kept verbatim and the ones to summarise. */\nexport interface CompactionSelection<M> {\n keep: M[];\n drop: M[];\n}\n\n/**\n * The selector itself — generic in the MESSAGE type, so it is assignable to core's\n * `AparteCompactionSelector` (messages of `AparteMessage`) without importing it. The\n * generic sits on the returned function, not on the factory: a factory-level `<M>`\n * has no inference site at `createCompactionSelector({...})` and falls back to the\n * bare `CompactableMessage`, which is not what the client's option type wants back.\n */\nexport type CompactionSelector = <M extends CompactableMessage>(messages: M[]) => CompactionSelection<M>;\n\nexport interface CompactionSelectorOptions {\n /**\n * The active model's context window, in tokens — a number, or a getter read at\n * each call so a model change is picked up (`() => aparteGlobalConfig.getCurrentModel()?.contextWindow`).\n * Unknown (`undefined`) means nothing is dropped: without a window there is no\n * budget to be over.\n */\n contextWindow: number | (() => number | undefined);\n /** The system prompt the request carries, for the budget. A string or a getter. Default none. */\n systemPrompt?: string | (() => string | null | undefined);\n /** The tools declared to the model, for the budget. A value or a getter. Default none. */\n tools?: unknown | (() => unknown);\n /** Partial override of the compactor's config — reserves, ratios, floors. */\n config?: Partial<Omit<CompactionConfig, 'contextWindow'>>;\n /** Never summarise fewer than this many of the newest messages. Default 2 — the last exchange. */\n minKeep?: number;\n}\n\nconst read = <T>(value: T | (() => T)): T => (typeof value === 'function' ? (value as () => T)() : value);\n\n/** What a message costs: its text, or the text of its segments when it has no `content`. */\nconst textOf = (message: CompactableMessage): string => {\n if (typeof message.content === 'string' && message.content.length > 0) return message.content;\n return (message.segments ?? [])\n .map((segment) => {\n const content = (segment as { content?: unknown }).content;\n return typeof content === 'string' ? content : '';\n })\n .join('\\n');\n};\n\n/**\n * Build the selector. Give it to the client:\n *\n * ```ts\n * new AparteClient({\n * compactionSelector: createCompactionSelector({\n * contextWindow: () => aparteGlobalConfig.getCurrentModel()?.contextWindow,\n * systemPrompt: () => aparteGlobalConfig.resolveSystemPrompt(),\n * }),\n * });\n * ```\n *\n * Walks the history from the newest message: what fits the sliding window's budget\n * is kept verbatim, the rest is dropped for summarising. When everything fits,\n * `drop` is empty and `compact()` reports `{ skipped: true }` — nothing to do.\n */\nexport function createCompactionSelector(options: CompactionSelectorOptions): CompactionSelector {\n const minKeep = Math.max(0, options.minKeep ?? 2);\n return <M extends CompactableMessage>(messages: M[]): CompactionSelection<M> => {\n const contextWindow = read(options.contextWindow);\n if (!contextWindow || contextWindow <= 0) return { keep: messages, drop: [] };\n\n const budget = computeHistoryBudget({\n systemPrompt: read(options.systemPrompt) ?? '',\n toolsArray: read(options.tools),\n config: { ...options.config, contextWindow },\n });\n const windowBudget = splitHistoryBudget(budget.historyBudget, budget.config).window;\n\n let used = 0;\n let cut = messages.length;\n for (let i = messages.length - 1; i >= 0; i--) {\n const cost = estimateTokens(textOf(messages[i]!));\n const kept = messages.length - 1 - i;\n if (used + cost > windowBudget && kept >= minKeep) break;\n used += cost;\n cut = i;\n }\n if (cut === 0) return { keep: messages, drop: [] };\n return { keep: messages.slice(cut), drop: messages.slice(0, cut) };\n };\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/agent/stream-run.ts"],"sourcesContent":["/**\n * stream-run.ts — framework-free structured-stream agent loop.\n *\n * The agent loop — extracted from core's inline `_streamLoop`, which it has since\n * replaced (audit 2026-08-28, D1): a `while(tool_use)` loop\n * that consumes a structured `AsyncIterable<StreamChatEvent>` from a transport,\n * runs approved tools, feeds their results back into the history, and re-calls\n * the transport until the model stops asking for tools. It performs **no DOM\n * work** — it emits {@link StreamRunEvent}s (in the inline loop's exact order) that\n * an adapter in `@aparte/core` turns into `targetElement.*` calls.\n *\n * Formerly the parity target of core's inline loop; now the one loop, its call\n * sequences pinned by core's `stream-parity` snapshots.\n * Scope: text · thinking · tool_use (+ HITL) · done · error · synthetic toolChoice\n * bypass. Tagged blocks in the text (`<artifact>`, `<cite>`…) are adapter-side: core's\n * parser reads the grammars registered on the config. The built-in `create_artifact`\n * left with the artifact (D7) — it is a registered tool now. The raw / XML artifact\n * modes and the multi-phase pipeline were removed (audit 2026-08-28, D2) — orchestration is the\n * product's, and a mode nothing in this repository emitted was a contract\n * maintained for nobody.\n */\n\nimport type {\n StreamRunEmitter,\n StreamChatEvent,\n StreamChatRequest,\n StreamAgentMessage,\n StreamToolCall,\n StreamToolHandler,\n StreamToolConfig,\n StreamApprovalResolver,\n StreamUsage,\n} from './stream-events.js';\n\n/** Default per-tool-call handler timeout — mirrors `TOOL_HANDLER_TIMEOUT_MS`. */\nconst DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1000;\n/** Default global turn cap — mirrors `AparteClientOptions.maxTurns ?? 10`. */\nconst DEFAULT_MAX_TURNS = 10;\n\nexport interface StreamRunOptions {\n /** Id of the assistant message being streamed (opaque; carried in events). */\n messageId: string;\n /** Turn-1 request; the loop clones its `messages` and enriches them per turn. */\n baseRequest: StreamChatRequest;\n /**\n * Calls the transport with the (possibly enriched) request. Returns the\n * structured stream, or a plain string for a non-streaming provider. Mirrors\n * `getTransport().chat(provider, request, auth, ctx)` with provider/auth/ctx\n * closed over by the adapter.\n */\n transportCall: (request: StreamChatRequest) => Promise<AsyncIterable<StreamChatEvent> | string>;\n /** Resolves a tool's handler by name (mirrors `aparteGlobalConfig.getToolHandler`). */\n toolLookup: (name: string) => StreamToolHandler | undefined;\n /** Resolves a tool's loop config by name (maxTurns / needsApproval). */\n toolConfigLookup?: (name: string) => StreamToolConfig | undefined;\n /** HITL approval resolver for `needsApproval` tools (default: never called). */\n approvalResolver?: StreamApprovalResolver;\n /** Synchronous, ordered event sink consumed by the adapter. */\n emitter: StreamRunEmitter;\n /** Single abort signal composing `_isAborted` + the stream controller. */\n signal: AbortSignal;\n /** Global turn cap. @default 10 */\n maxTurns?: number;\n /** Per-tool-call handler timeout in ms. @default 300000 */\n toolTimeoutMs?: number;\n /**\n * Called for every turn the loop appends to the history — the grouped\n * `tool_call` envelope, each `tool_result` (resolved or rejected), and a\n * pipeline phase's reply — in order, and always before the transport call that\n * would carry it. Never called for the messages you passed in `baseRequest`.\n *\n * For hosts that own their own transcript. The loop re-sends its `messages`\n * array every turn, which suits stateless message APIs but not a **prefix\n * cache** (llama.cpp slots, vLLM), where turn N+1 must EXTEND turn N byte for\n * byte. Such a host builds its own request in {@link transportCall} — ignoring\n * `request.messages` — and mirrors these notifications into its append-only\n * log, instead of reimplementing the loop's tool bookkeeping. Synchronous and\n * ordered, like {@link emitter}.\n *\n * One rule about the envelope: a turn's `tool_call` message is reported ONCE, when\n * its first call completes, and the calls that complete later in the same turn are\n * already in that same object's `toolCalls` — the array is shared by reference, not\n * copied. Hold the reference, not a snapshot, and every result you receive after it\n * is declared by it.\n */\n onHistoryAppend?: (message: StreamAgentMessage) => void;\n /**\n * Generates the ids the loop mints itself — today only the synthetic call of a\n * forced `toolChoice` (`prefix` is `'synthetic-tool'`). The default is a\n * deterministic per-run counter; core's client injects a crypto-based one. Tool\n * ids otherwise flow from the stream.\n */\n idGen?: (prefix: string) => string;\n}\n\n/** Warn once per event type for something this version does not map, then skip it. */\nconst warnedUnknownEvents = new Set<string>();\nfunction warnUnknownStreamEvent(event: unknown): void {\n const type = String((event as { type?: unknown })?.type ?? 'undefined');\n if (warnedUnknownEvents.has(type)) return;\n warnedUnknownEvents.add(type);\n console.warn(\n `[runStreamAgent] Ignoring unrecognised stream event \"${type}\". The reply is`\n + ' unaffected; this usually means the provider or its SDK emits a part this'\n + ' version of aparté does not map yet.',\n );\n}\n\n/**\n * Run the structured-stream agent loop. Resolves the last turn's usage (the\n * `done{usage}` last-write-wins, mirroring `_streamLoop`'s return), or\n * `undefined`. Throws on a stream `error` event or a non-abort tool failure —\n * the caller (adapter) routes that to its lifecycle-error handler, exactly as\n * `_handleSend`/`_handleRetry`/`_handleEdit` catch `_streamLoop`.\n *\n * This block has to stay ADJACENT to the function. It used to sit above\n * `warnUnknownStreamEvent`, six lines and a docblock away from what it describes, and a\n * docblock separated from its declaration is attached to nothing: TypeDoc took the\n * helper's comment as the neighbouring one and `@aparte/engine`'s reference page carried\n * the package's headline export with no description at all — the very export its own\n * frontmatter names. Same trap the custom-elements analyser has with a class docblock\n * pushed above an import.\n */\nexport async function runStreamAgent(opts: StreamRunOptions): Promise<StreamUsage | undefined> {\n const {\n transportCall,\n toolLookup,\n toolConfigLookup,\n approvalResolver,\n emitter,\n signal,\n maxTurns = DEFAULT_MAX_TURNS,\n toolTimeoutMs = DEFAULT_TOOL_TIMEOUT_MS,\n } = opts;\n\n // Reassignable: the synthetic-toolChoice bypass strips `toolChoice`/`tools`\n // from it after the forced turn-1 handler runs (mirrors `_streamLoop`'s\n // `baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined }`).\n let baseRequest = opts.baseRequest;\n\n let idSeq = 0;\n const idGen = opts.idGen ?? ((prefix: string) => `${prefix}-${idSeq++}`);\n\n // Mutable history the loop enriches with tool_call/tool_result turns. Every\n // enrichment goes through `append`, so a caller owning its own transcript sees\n // the same turns in the same order (see `onHistoryAppend`).\n const messages: StreamAgentMessage[] = [...baseRequest.messages];\n const append = (message: StreamAgentMessage): void => {\n messages.push(message);\n opts.onHistoryAppend?.(message);\n };\n\n let continueLoop = true;\n let turns = 0;\n let lastUsage: StreamUsage | undefined;\n\n // Leading write before turn 1: mark the message streaming.\n emitter({ type: 'run-start' });\n\n // ── outer (turn) loop — one iteration = one transport call ────────────────\n while (continueLoop) {\n // Abort at the top of a turn: no stream open yet, so no cancel needed. The\n // `run-aborted` itself is emitted once, at the loop's exit (see below).\n if (signal.aborted) break;\n\n turns++;\n if (turns > maxTurns) {\n emitter({ type: 'turn-limit-exceeded', scope: 'global', limit: maxTurns });\n break;\n }\n\n // ── Synthetic toolChoice bypass (mirrors _streamLoop) ──────\n // toolChoice = { name, input } (orchestrator-forced): skip the LLM for\n // turn 1, run the handler directly, inject its result as a tool_result,\n // then strip toolChoice/tools and fall through to the transport call in\n // the SAME turn so the model answers with the tool result already in\n // history. (The adapter's tool-start handler injects renderer CSS here\n // too — a benign, idempotent gain over _streamLoop's synthetic path,\n // which skipped it; reconciled in the parity test.)\n const toolChoice = baseRequest['toolChoice'];\n if (\n turns === 1 &&\n toolChoice &&\n typeof toolChoice === 'object' &&\n !Array.isArray(toolChoice) &&\n (toolChoice as { input?: unknown }).input !== undefined\n ) {\n // Narrowed to the declared shape rather than `input: unknown`: now that\n // `StreamChatRequest.toolChoice` mirrors core's type, the cast can be exact.\n const tc = toolChoice as { name: string; input: Record<string, unknown> };\n const syntheticId = idGen('synthetic-tool');\n emitter({ type: 'tool-start', toolCallId: syntheticId, name: tc.name, input: tc.input });\n\n const handler = toolLookup(tc.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n\n const outcome = await invokeToolHandler(handler, { id: syntheticId, name: tc.name, input: tc.input }, signal, toolTimeoutMs);\n if (outcome.status === 'aborted') {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n if (outcome.status === 'failed') {\n emitter({ type: 'tool-failed', toolCallId: syntheticId, error: errorText(outcome.error) });\n throw outcome.error;\n }\n emitter({ type: 'tool-resolved', toolCallId: syntheticId, result: outcome.content, structuredResult: outcome.structuredContent });\n append({ role: 'tool_call', content: '', toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });\n append({ role: 'tool_result', content: outcome.content, toolCallId: syntheticId });\n baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined };\n }\n\n // A stop that lands before the first event surfaces here as a REJECTION,\n // not as an `error` event — the fetch is aborted while still in flight, so\n // it never reaches the stream the loop reads. Letting it propagate made a\n // deliberate stop indistinguishable from a transport failure for anyone\n // driving this loop directly. (Core's `_streamTurn` has the mirror of this\n // guard; the browser suite is what found the path, after the two\n // event-level guards were already closed.)\n const request: StreamChatRequest = { ...baseRequest, messages };\n let response: Awaited<ReturnType<typeof transportCall>>;\n try {\n response = await transportCall(request);\n } catch (err: unknown) {\n if (signal.aborted || (err as { name?: string } | undefined)?.name === 'AbortError') {\n // `break`, not `return`: the post-loop `run-done` below is documented\n // as running on EVERY exit path, and five of the six abort paths fall\n // through to it. Returning here skipped it, so a consumer wiring\n // `runStreamAgent` + `createStreamAdapter` directly never got the\n // `completed` status and the bubble stayed flagged streaming — which\n // is verbatim the defect the client-side guard was added to fix.\n // (`run-aborted` is emitted once, at the loop's exit.)\n continueLoop = false;\n break;\n }\n throw err;\n }\n\n // Non-streaming provider: the string IS the full assistant message. The\n // adapter writes it and completes (no done{usage}); spike scenarios\n // always stream, so we finish the run here.\n //\n // `text-flush` is not optional here. It is the adapter's ONLY caller of\n // `parser.finalize()`, and the parser deliberately withholds an ambiguous\n // tail — a lone backtick, a triple backtick, a `<`, or the safe window\n // inside an unterminated fence. Breaking out before the flush dropped all\n // of it, so a non-streaming reply ending on any of those characters lost\n // them and one consisting only of them rendered nothing at all. Core's\n // inline loop does `parse()` AND `finalize()` on its whole-response\n // branch; this is the twin.\n if (typeof response === 'string') {\n emitter({ type: 'text-delta', delta: response });\n emitter({ type: 'text-flush' });\n break;\n }\n\n // New turn: the adapter resets its per-turn parser / thinking / streaming\n // state here (mirrors `_streamLoop` creating a fresh AparteStreamParser +\n // sets each outer iteration). No DOM effect of its own.\n emitter({ type: 'turn-start' });\n\n // Per-turn streaming state.\n let precedingText = '';\n const toolCallsThisTurn: StreamToolCall[] = [];\n // \"Did this turn ask for tools\" — a different question from \"which calls are\n // declared\" (`toolCallsThisTurn`, which now holds only committed ones).\n let sawToolUse = false;\n /**\n * The turn's ONE `tool_call` envelope, held by reference. Created — and\n * appended, so the host is notified once — the first time a call needs it;\n * every later call of the turn is already in the SAME `toolCalls` array, so a\n * `tool_result` appended afterwards is always declared by it.\n *\n * This replaces an id scan over `messages` that guessed whether the envelope\n * was already there. The `create_artifact` fast path pushed a fresh envelope\n * of its own; the scan then found the artifact's id in it, concluded \"already\n * pushed\", and the next tool's result went out with no call declaring it —\n * the P0 of the 2026-08-28 audit, a history an Anthropic-shaped API rejects\n * outright. A reference cannot be guessed wrong.\n */\n let envelope: StreamAgentMessage | null = null;\n /*\n * A call is DECLARED in the envelope only once it is committed to producing\n * a `tool_result` — at the three sites that append one right after. It used\n * to be pushed the moment the `tool_use` event was read, into the same array\n * the envelope holds by reference, so a call halted before its result (no\n * handler, turn limit, missing resolver, an abort during the wait) still\n * appeared in a history the host was told to hold by reference: a\n * `tool_call` declaring a call that never gets a result.\n */\n const declareCall = (call: StreamToolCall): void => {\n toolCallsThisTurn.push(call);\n if (envelope) return;\n envelope = { role: 'tool_call', content: '', toolCalls: toolCallsThisTurn, precedingText: precedingText.trim() || undefined };\n append(envelope);\n };\n\n // ── inner (SSE) loop — manual iteration so we can abort around each read.\n // Checked on BOTH sides of the read, and the second check is the one that\n // matters: the loop spends nearly all of its time parked on `next()`, so an\n // abort arriving while parked — the user pressing Stop while watching text\n // stream — is invisible to a check that only runs before the await. It also\n // covers both shapes a provider takes on abort: an `error` event or a quiet\n // close. Without it the `error` branch below throws and the caller reports a\n // deliberate stop as a failure. Mirrors `_streamLoop`'s `bailOnAbort`.\n const iterator = response[Symbol.asyncIterator]();\n const bailOnAbort = async (): Promise<boolean> => {\n if (!signal.aborted) return false;\n await iterator.return?.(undefined);\n continueLoop = false;\n return true;\n };\n try {\n while (true) {\n if (await bailOnAbort()) break;\n\n const step = await iterator.next();\n if (await bailOnAbort()) break;\n if (step.done) break;\n const event = step.value;\n\n if (event.type === 'thinking') {\n emitter({ type: 'thinking-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'text') {\n precedingText += event.delta;\n emitter({ type: 'text-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'done') {\n if (event.usage) lastUsage = event.usage;\n continue;\n }\n\n if (event.type === 'error') {\n // Mirror `_streamLoop`: throw; the caller's catch handles it.\n throw new Error(event.message);\n }\n\n // A DISCRIMINANT GUARD, not a comment.\n //\n // This used to be the bare comment `// event.type === 'tool_use'`, so\n // anything the chain above did not recognise fell in here and was\n // treated as a tool call: a `tool-start` / `tool-aborted` pair with an\n // undefined id and name, and — worse — the rest of the stream was\n // never processed, so the reply was truncated too.\n //\n // Core's inline loop had the opposite failure on the same input (it\n // threw and replaced the reply with an error bubble). Both now ignore\n // the event and carry on, which is what forward compatibility with a\n // provider SDK actually requires.\n if (event.type !== 'tool_use') {\n warnUnknownStreamEvent(event);\n continue;\n }\n sawToolUse = true;\n\n // No built-in tool of any name: `create_artifact` used to be dispatched\n // here, before the tool path, with no handler, no approval and its own\n // result. A tool is a tool — the app registers it (`@aparte/plugin-artifacts`\n // does), and it goes through tool-start, the gate, the handler and the\n // envelope like every other.\n emitter({ type: 'tool-start', toolCallId: event.id, name: event.name, input: event.input });\n\n const cfg = toolConfigLookup?.(event.name);\n\n // Per-tool maxTurns — the same arithmetic as the global cap (`>`), so one\n // number means one thing on both knobs. It was `>=`: `maxTurns: 1` on a\n // tool made it un-callable on the very first turn, and `maxTurns: N`\n // allowed N-1 calls against the global option's N.\n const effectiveMaxTurns = cfg?.maxTurns ?? maxTurns;\n if (turns > effectiveMaxTurns) {\n emitter({ type: 'turn-limit-exceeded', scope: 'tool', limit: effectiveMaxTurns, toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n const handler = toolLookup(event.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n let effectiveInput = event.input;\n\n // ── HITL approval gate ────────────────────────────────────────\n const gated = typeof cfg?.needsApproval === 'function'\n ? cfg.needsApproval({ id: event.id, name: event.name, input: event.input as Record<string, unknown> })\n : cfg?.needsApproval;\n if (gated) {\n // Announced only when someone is actually being asked: a `'deny'` is a\n // decision already made, and announcing a pause for it painted\n // `awaiting-approval` on the row and raised `aparte-tool-approval-request`\n // for a call nobody was ever going to be asked about.\n if (gated !== 'deny') {\n emitter({ type: 'tool-awaiting-approval', toolCallId: event.id, name: event.name, input: event.input });\n }\n /*\n * Nothing can ask, so nothing may answer.\n *\n * The default was `async () => ({ approved: false })`, which sent the\n * loop into the refusal branch and appended \"rejected by the user\" —\n * a host that forgot to wire a resolver had not refused anything, and\n * the model was told a person had. A misconfiguration is an aborted\n * call, not a decision.\n */\n if (!approvalResolver) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n /*\n * A stop is not a refusal either. Core's built-in channel resolves\n * `{ approved: false }` on abort, indistinguishable by value from an\n * explicit Reject — so the signal is what tells them apart. No\n * `tool_result`: an aborted call has nothing true to tell the model.\n *\n * Checked on three sides of the wait, because the stop can land on\n * any of them: while the gate was being shown (a host that reacts to\n * `awaiting-approval` synchronously — the panel's own Stop button),\n * inside the resolver (a channel that rejects with an AbortError\n * rather than resolving), or after it resolved. The first and second\n * used to fall through to the run-level abort, which never marked the\n * call: the segment stayed `awaiting-approval` for good.\n */\n let decision: Awaited<ReturnType<typeof approvalResolver>> | undefined;\n if (!signal.aborted) {\n try {\n decision = await approvalResolver(\n { id: event.id, name: event.name, input: event.input as Record<string, unknown> },\n signal,\n );\n } catch (err) {\n if (!signal.aborted && (err as { name?: string } | undefined)?.name !== 'AbortError') throw err;\n }\n }\n if (signal.aborted || !decision) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n if (!decision.approved) {\n /*\n * The user's own words when they gave them, and the fixed sentence\n * otherwise — byte-identical to core's twin, because this is the\n * only thing the model gets to read about the refusal.\n *\n * This branch used to hardcode the generic sentence. The resolver\n * type declared `instruction` and the whole loop never referenced\n * it, so on the engine path — the recommended one — a user who\n * refused and typed \"use the staging bucket instead\" had those\n * words dropped before the model saw them. Handing the model a\n * turn after a refusal exists SO THAT it reads the refusal; here\n * there was nothing to read.\n *\n * The parity suite could not see it: its resolver returned\n * `{ approved }` and never an instruction, so both loops agreed on\n * a case neither exercised. It supplies one now.\n */\n // Truthiness, like `instruction` beside it: an empty `reason` is no\n // reason, not an empty tool_result.\n const rejection = decision.reason?.trim()\n || (decision.instruction\n ? `The user rejected this tool call and said: ${decision.instruction}`\n : 'Tool execution was rejected by the user.');\n emitter({ type: 'tool-rejected', toolCallId: event.id, reason: rejection });\n declareCall({ id: event.id, name: event.name, input: event.input });\n append({ role: 'tool_result', content: rejection, toolCallId: event.id });\n /*\n * `break` WITHOUT `continueLoop = false`, and the asymmetry is the\n * point. The remaining tool calls of this turn must not run — the\n * model asked for several and refusing one cannot license the\n * others — but the loop takes another turn, so the sentence just\n * appended actually reaches the model. It never did: the turn\n * ended here, and telling the assistant what you wanted instead\n * meant retyping it as a new message it read out of order.\n *\n * Core's twin returns `'respond'` for exactly this, and the parity\n * suite asserts the two agree.\n */\n break;\n }\n if (decision.payload && typeof decision.payload === 'object' && !Array.isArray(decision.payload)) {\n effectiveInput = { ...(event.input as Record<string, unknown>), ...(decision.payload as Record<string, unknown>) };\n }\n emitter({ type: 'tool-approved', toolCallId: event.id });\n }\n\n // ── handler invocation with per-call timeout ──────────────────\n const outcome = await invokeToolHandler(\n handler,\n { id: event.id, name: event.name, input: effectiveInput },\n signal,\n toolTimeoutMs,\n );\n if (outcome.status === 'aborted') {\n // Same rule as the rejection / turn-limit / missing-handler\n // exits above, which this branch was missing: a stopped turn\n // must not go on running the tool calls that follow it.\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n } else if (outcome.status === 'failed') {\n // A crash is the handler's, and the run ends on it as before —\n // but the row hears about it first, instead of spinning for good.\n emitter({ type: 'tool-failed', toolCallId: event.id, error: errorText(outcome.error) });\n throw outcome.error;\n } else {\n emitter({ type: 'tool-resolved', toolCallId: event.id, result: outcome.content, structuredResult: outcome.structuredContent });\n declareCall({ id: event.id, name: event.name, input: event.input });\n append({ role: 'tool_result', content: outcome.content, toolCallId: event.id });\n }\n }\n\n // Turn boundary: finalize the parser (flush residual text). Mirrors\n // `_streamLoop`'s `textParser.finalize()` call — runs on normal\n // end AND abort-break, but NOT after a thrown `error` (which escapes\n // this try before reaching here, exactly like `_streamLoop`).\n emitter({ type: 'text-flush' });\n\n } finally {\n // Mirror `reader.releaseLock()` in the finally: settle the iterator.\n await iterator.return?.(undefined).catch(() => { /* best effort */ });\n }\n\n // No tool calls this turn → the final answer.\n if (!sawToolUse) continueLoop = false;\n }\n\n // Post-loop finalization runs on every exit path (normal / abort / maxTurns).\n //\n // `run-aborted` is decided HERE, once, from the signal — not at each site that\n // notices the abort. It used to be emitted at three of the six abort exits\n // (top of turn, transport rejection, mid-stream bail), and the three tool-level\n // ones — a Stop during a handler, during an approval wait, or with no resolver —\n // set `continueLoop = false` and fell through to `run-done` alone: no terminal\n // lifecycle event reached the host, so the typing indicator and the streaming\n // id were never cleared and the turn was never persisted. A tool TIMEOUT aborts\n // only the child controller, so `signal.aborted` stays false and that turn still\n // ends as done, unchanged.\n if (signal.aborted) emitter({ type: 'run-aborted' });\n emitter({ type: 'run-done', usage: lastUsage });\n return lastUsage;\n}\n\n/**\n * Run a tool handler with a per-call timeout, linked to the run's abort signal.\n * Resolves `{ status: 'resolved', content }` on success, or `{ status: 'aborted' }`\n * if the handler aborts (timeout OR parent abort); any other error is re-thrown so\n * the caller routes it to lifecycle-error, exactly as `_streamLoop` does. Shared\n * by the generic tool path and the synthetic-toolChoice bypass (mirrors the\n * AbortController + `TOOL_HANDLER_TIMEOUT_MS` dance both use in `_streamLoop`).\n */\nasync function invokeToolHandler(\n handler: StreamToolHandler,\n call: StreamToolCall,\n signal: AbortSignal,\n toolTimeoutMs: number,\n): Promise<\n | { status: 'resolved'; content: string; structuredContent?: unknown }\n | { status: 'aborted' }\n /** The handler threw something that is not an abort: a crash, reported by the caller then re-thrown. */\n | { status: 'failed'; error: unknown }\n> {\n // If the run was already aborted before we got here, don't invoke the\n // handler at all: a past 'abort' event will never re-fire on the listener\n // below, so the handler would otherwise run to completion despite cancel.\n if (signal.aborted) return { status: 'aborted' };\n\n const controller = new AbortController();\n const onParentAbort = () => controller.abort();\n signal.addEventListener('abort', onParentAbort, { once: true });\n\n /*\n * RACED, not just signalled.\n *\n * Aborting the controller is a request the handler is free to ignore, and the\n * default shape of a consumer tool ignores it —\n * `async () => ({ content: await fetch(...).then(r => r.text()) })` never reads\n * its signal. So the timeout fired, nothing rejected, and the loop waited\n * forever on an option whose JSDoc promises a timeout.\n *\n * The signal still fires first, because a handler that DOES honour it should\n * get the chance to clean up and reject on its own terms; the race is what\n * makes the promise true when it does not.\n */\n let timeout: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<{ status: 'aborted' }>((resolve) => {\n timeout = setTimeout(() => {\n controller.abort();\n resolve({ status: 'aborted' });\n }, toolTimeoutMs);\n });\n\n try {\n const result = await Promise.race([\n handler(call, controller.signal).then((r) => ({ status: 'resolved' as const, content: r.content, structuredContent: r.structuredContent })),\n timedOut,\n ]);\n return result;\n } catch (err: unknown) {\n if ((err as { name?: string })?.name === 'AbortError') return { status: 'aborted' };\n // Not thrown from here: the call was announced with `tool-start`, and the\n // loop's invariant is that an announced call gets exactly one terminal\n // event. Throwing from inside skipped it — the row spun \"Running\" for good\n // while the message showed an error card. The caller reports, then throws.\n return { status: 'failed', error: err };\n } finally {\n clearTimeout(timeout);\n signal.removeEventListener('abort', onParentAbort);\n }\n}\n\n/** The one-line text of a handler's crash, for the `tool-failed` event and the row. */\nfunction errorText(error: unknown): string {\n if (error instanceof Error) return error.message || error.name;\n return typeof error === 'string' ? error : String(error);\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map an artifact MIME type to a renderer kind — `'react'`, `'html'`, `'js'`, `'css'`,
|
|
3
|
-
* `'svg'`, `'json'`, `'markdown'`, `'csv'`, `'text'` — or `fallback` (default
|
|
4
|
-
* `'unknown'`) for anything else. Anthropic's `application/vnd.ant.*` namespace maps to
|
|
5
|
-
* its suffix; exact standard MIMEs are matched first, then a substring rescue for
|
|
6
|
-
* parameterised or vendor variants (`text/html; charset=utf-8`, `application/ld+json`).
|
|
7
|
-
*
|
|
8
|
-
* THE implementation: `@aparte/core` re-exports it under the same name. It used to
|
|
9
|
-
* live in the XML artifact state machine, which the 2026-08-28 audit's D2 removed —
|
|
10
|
-
* the core parser reads `<artifact>` tags natively — and this is the one piece of
|
|
11
|
-
* that file the built-in `create_artifact` and the artifact card still need.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* deriveArtifactKind('application/vnd.ant.react') // 'react'
|
|
15
|
-
* deriveArtifactKind('text/html; charset=utf-8') // 'html'
|
|
16
|
-
* deriveArtifactKind('font/woff2', 'text') // 'text' (fallback)
|
|
17
|
-
*/
|
|
18
|
-
export declare function deriveArtifactKind(mimeType: string, fallback?: string): string;
|
|
19
|
-
//# sourceMappingURL=artifact-kind.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"artifact-kind.d.ts","sourceRoot":"","sources":["../../../src/agent/parsers/artifact-kind.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAY,GAAG,MAAM,CAqBjF"}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* conversation/compactor.ts — adaptive budgeting + context-window assembly.
|
|
3
|
-
*
|
|
4
|
-
* Conversation history budgeting and sliding-window assembly —
|
|
5
|
-
* framework-free and tokenizer-free (char-count heuristic).
|
|
6
|
-
*
|
|
7
|
-
* Pattern (cf. Claude Code /context, OpenAI Agents SDK TrimmingSession):
|
|
8
|
-
* Budget = CONTEXT_WINDOW − systemPrompt − tools − reservedThinking
|
|
9
|
-
* − reservedGeneration − autocompactBuffer − safetyMargin
|
|
10
|
-
*
|
|
11
|
-
* The history budget is then split into:
|
|
12
|
-
* - summary (global summary, LLM-generated, ~10%)
|
|
13
|
-
* - ragHist (older turns retrieved by cosine similarity, ~25%)
|
|
14
|
-
* - window (verbatim sliding window, the rest)
|
|
15
|
-
*
|
|
16
|
-
* Drop priority on overflow:
|
|
17
|
-
* 1. summary (async-regenerable)
|
|
18
|
-
* 2. ragHist (retrievable on the next turn)
|
|
19
|
-
* 3. window oldest turns
|
|
20
|
-
* X. NEVER dropped: currentUser + lastAssistant (absolute working memory)
|
|
21
|
-
*
|
|
22
|
-
* Browser-portable: zero deps, just an estimateTokens heuristic.
|
|
23
|
-
*/
|
|
24
|
-
export interface CompactionMessage {
|
|
25
|
-
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
26
|
-
content: string;
|
|
27
|
-
}
|
|
28
|
-
export interface RetrievedTurn {
|
|
29
|
-
role: 'user' | 'assistant';
|
|
30
|
-
content: string;
|
|
31
|
-
score?: number;
|
|
32
|
-
}
|
|
33
|
-
export interface CompactionConfig {
|
|
34
|
-
/** Total context window of the active model, in tokens. */
|
|
35
|
-
contextWindow: number;
|
|
36
|
-
/** Reserved budget for the model's thinking/reasoning block, in tokens. */
|
|
37
|
-
reservedThinking: number;
|
|
38
|
-
/** Reserved budget for the assistant response (max_new_tokens cap). */
|
|
39
|
-
reservedGeneration: number;
|
|
40
|
-
/** Fraction of context_window kept as autocompact buffer (0..1). */
|
|
41
|
-
autocompactBufferPct: number;
|
|
42
|
-
/** Hard safety margin in tokens. */
|
|
43
|
-
safetyMargin: number;
|
|
44
|
-
/** Floor for history budget — never compact below this. */
|
|
45
|
-
minHistoryBudget: number;
|
|
46
|
-
/** Ratio of history budget allocated to the summary block. */
|
|
47
|
-
summaryRatio: number;
|
|
48
|
-
/** Hard cap for summary tokens. */
|
|
49
|
-
summaryMaxTokens: number;
|
|
50
|
-
/** Ratio of history budget allocated to the RAG-retrieved old turns. */
|
|
51
|
-
ragHistRatio: number;
|
|
52
|
-
/** Hard cap for ragHist tokens. */
|
|
53
|
-
ragHistMaxTokens: number;
|
|
54
|
-
/** Trigger summarization once history usage reaches this % of the budget. */
|
|
55
|
-
triggerSummaryThresholdPct: number;
|
|
56
|
-
/** Re-run summarization every N user turns. */
|
|
57
|
-
summarizeEveryNTurns: number;
|
|
58
|
-
/** Header prepended to the summary block injected into the model context. English
|
|
59
|
-
* by default — override to localise (the compactor ships no locale system). */
|
|
60
|
-
summaryLabel: string;
|
|
61
|
-
/** Header prepended to the RAG-retrieved old turns injected into the context. */
|
|
62
|
-
ragIntroLabel: string;
|
|
63
|
-
}
|
|
64
|
-
export interface BudgetBreakdown {
|
|
65
|
-
contextWindow: number;
|
|
66
|
-
systemPrompt: number;
|
|
67
|
-
tools: number;
|
|
68
|
-
reservedThinking: number;
|
|
69
|
-
reservedGeneration: number;
|
|
70
|
-
autocompactBuffer: number;
|
|
71
|
-
safetyMargin: number;
|
|
72
|
-
historyAvailable: number;
|
|
73
|
-
}
|
|
74
|
-
export interface BudgetResult {
|
|
75
|
-
historyBudget: number;
|
|
76
|
-
breakdown: BudgetBreakdown;
|
|
77
|
-
config: CompactionConfig;
|
|
78
|
-
}
|
|
79
|
-
export interface SplitBudget {
|
|
80
|
-
summary: number;
|
|
81
|
-
ragHist: number;
|
|
82
|
-
window: number;
|
|
83
|
-
}
|
|
84
|
-
export interface UsageBreakdown {
|
|
85
|
-
system: number;
|
|
86
|
-
summary: number;
|
|
87
|
-
ragHist: number;
|
|
88
|
-
window: number;
|
|
89
|
-
}
|
|
90
|
-
export interface DroppedBreakdown {
|
|
91
|
-
ragHits: number;
|
|
92
|
-
oldTurns: number;
|
|
93
|
-
summary: boolean;
|
|
94
|
-
}
|
|
95
|
-
export interface FullBreakdown extends BudgetBreakdown {
|
|
96
|
-
historyAllocated: SplitBudget;
|
|
97
|
-
historyUsed: UsageBreakdown;
|
|
98
|
-
totalUsed: number;
|
|
99
|
-
/** Free tokens left in the context window after all reservations + actual use. */
|
|
100
|
-
free: number;
|
|
101
|
-
dropped: DroppedBreakdown;
|
|
102
|
-
}
|
|
103
|
-
export interface CompactionInput {
|
|
104
|
-
messages: CompactionMessage[];
|
|
105
|
-
systemPrompt: string;
|
|
106
|
-
/** Tools array passed to apply_chat_template (or null/undefined when no tools). */
|
|
107
|
-
toolsArray?: unknown;
|
|
108
|
-
/** Running summary text (regenerable). */
|
|
109
|
-
summary?: string;
|
|
110
|
-
/** RAG-retrieved old turns (older than the sliding window). */
|
|
111
|
-
retrievedTurns?: RetrievedTurn[];
|
|
112
|
-
/** Partial override of the default config. */
|
|
113
|
-
config?: Partial<CompactionConfig>;
|
|
114
|
-
}
|
|
115
|
-
export interface CompactionResult {
|
|
116
|
-
compactedMessages: CompactionMessage[];
|
|
117
|
-
breakdown: FullBreakdown;
|
|
118
|
-
}
|
|
119
|
-
export declare const DEFAULT_COMPACTION_CONFIG: CompactionConfig;
|
|
120
|
-
/**
|
|
121
|
-
* Token heuristic (FR ~3.5 chars/tok, EN ~4 chars/tok).
|
|
122
|
-
* Accurate to ±10% — enough for budgeting, and it avoids `tokenizer.encode()`,
|
|
123
|
-
* which costs ~5ms per message × N (too slow to run on every turn).
|
|
124
|
-
*/
|
|
125
|
-
export declare function estimateTokens(text: string | null | undefined): number;
|
|
126
|
-
/**
|
|
127
|
-
* Estimate tokens for a JSON-serializable structure (e.g. tools array).
|
|
128
|
-
*/
|
|
129
|
-
export declare function estimateTokensJson(obj: unknown): number;
|
|
130
|
-
/**
|
|
131
|
-
* Compute the available history budget after subtracting fixed costs.
|
|
132
|
-
*/
|
|
133
|
-
export declare function computeHistoryBudget(input: {
|
|
134
|
-
systemPrompt: string;
|
|
135
|
-
toolsArray?: unknown;
|
|
136
|
-
config?: Partial<CompactionConfig>;
|
|
137
|
-
}): BudgetResult;
|
|
138
|
-
/**
|
|
139
|
-
* Split history budget into summary / ragHist / window slots.
|
|
140
|
-
*/
|
|
141
|
-
export declare function splitHistoryBudget(historyBudget: number, cfg?: CompactionConfig): SplitBudget;
|
|
142
|
-
interface AssembleParams {
|
|
143
|
-
messages: CompactionMessage[];
|
|
144
|
-
summary?: string;
|
|
145
|
-
retrievedTurns?: RetrievedTurn[];
|
|
146
|
-
windowBudget: number;
|
|
147
|
-
summaryBudget: number;
|
|
148
|
-
ragBudget: number;
|
|
149
|
-
systemContent?: string;
|
|
150
|
-
/** Context-injection headers; default to English (DEFAULT_COMPACTION_CONFIG). */
|
|
151
|
-
summaryLabel?: string;
|
|
152
|
-
ragIntroLabel?: string;
|
|
153
|
-
}
|
|
154
|
-
interface AssembleResult {
|
|
155
|
-
compactedMessages: CompactionMessage[];
|
|
156
|
-
used: UsageBreakdown;
|
|
157
|
-
dropped: DroppedBreakdown;
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Assemble compacted message list given a budget and conversation state.
|
|
161
|
-
*
|
|
162
|
-
* Output order:
|
|
163
|
-
* [system?] → [summary system msg?] → [ragHist system msg?] → window verbatim
|
|
164
|
-
*
|
|
165
|
-
* Always keeps the last 2 non-system messages verbatim (working memory),
|
|
166
|
-
* regardless of windowBudget — these are the floor that can never be dropped.
|
|
167
|
-
*/
|
|
168
|
-
export declare function assembleCompacted(params: AssembleParams): AssembleResult;
|
|
169
|
-
/**
|
|
170
|
-
* Compute budget + assemble in one call. The primary entry point for
|
|
171
|
-
* consumers (a browser request interceptor, mobile, Node tests).
|
|
172
|
-
*/
|
|
173
|
-
export declare function compactConversation(input: CompactionInput): CompactionResult;
|
|
174
|
-
export {};
|
|
175
|
-
//# sourceMappingURL=compactor.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"compactor.d.ts","sourceRoot":"","sources":["../../src/conversation/compactor.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* conversation/selector.ts — the budget-aware `compactionSelector` for `AparteClient`.
|
|
3
|
-
*
|
|
4
|
-
* `AparteClient.compact()` decides what to summarise through one function:
|
|
5
|
-
* `(messages) => { keep, drop }`. Its default drops everything — summarise all,
|
|
6
|
-
* replace all. This is the other half the seam was written for: the newest turns
|
|
7
|
-
* that still fit the history budget stay verbatim, the older ones are summarised.
|
|
8
|
-
*
|
|
9
|
-
* The budget is the compactor's (`computeHistoryBudget` + `splitHistoryBudget`), so
|
|
10
|
-
* the gauge a page shows, the selector `compact()` uses and the window the model
|
|
11
|
-
* declares all speak the same numbers. The budget is closed over here by the
|
|
12
|
-
* consumer — the window is theirs to know, the client's to never guess.
|
|
13
|
-
*/
|
|
14
|
-
import { type CompactionConfig } from './compactor.js';
|
|
15
|
-
/**
|
|
16
|
-
* The least a message must carry to be costed: its text, or segments with text.
|
|
17
|
-
* Structural on purpose — this package imports nothing from core — and core's
|
|
18
|
-
* `AparteMessage` satisfies it, which is what makes the selector below exactly the
|
|
19
|
-
* `compactionSelector` `AparteClient` takes.
|
|
20
|
-
*/
|
|
21
|
-
export interface CompactableMessage {
|
|
22
|
-
content?: string;
|
|
23
|
-
/**
|
|
24
|
-
* `unknown` elements, read defensively below: core's segment union includes a
|
|
25
|
-
* segment with no `content` at all, and TypeScript refuses a type with no property
|
|
26
|
-
* in common with the all-optional `{ content?: unknown }` (a "weak type"), so that
|
|
27
|
-
* tighter shape made `AparteMessage` unassignable here.
|
|
28
|
-
*/
|
|
29
|
-
segments?: ReadonlyArray<unknown>;
|
|
30
|
-
}
|
|
31
|
-
/** What `compact()` asks for: the messages kept verbatim and the ones to summarise. */
|
|
32
|
-
export interface CompactionSelection<M> {
|
|
33
|
-
keep: M[];
|
|
34
|
-
drop: M[];
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* The selector itself — generic in the MESSAGE type, so it is assignable to core's
|
|
38
|
-
* `AparteCompactionSelector` (messages of `AparteMessage`) without importing it. The
|
|
39
|
-
* generic sits on the returned function, not on the factory: a factory-level `<M>`
|
|
40
|
-
* has no inference site at `createCompactionSelector({...})` and falls back to the
|
|
41
|
-
* bare `CompactableMessage`, which is not what the client's option type wants back.
|
|
42
|
-
*/
|
|
43
|
-
export type CompactionSelector = <M extends CompactableMessage>(messages: M[]) => CompactionSelection<M>;
|
|
44
|
-
export interface CompactionSelectorOptions {
|
|
45
|
-
/**
|
|
46
|
-
* The active model's context window, in tokens — a number, or a getter read at
|
|
47
|
-
* each call so a model change is picked up (`() => aparteGlobalConfig.getCurrentModel()?.contextWindow`).
|
|
48
|
-
* Unknown (`undefined`) means nothing is dropped: without a window there is no
|
|
49
|
-
* budget to be over.
|
|
50
|
-
*/
|
|
51
|
-
contextWindow: number | (() => number | undefined);
|
|
52
|
-
/** The system prompt the request carries, for the budget. A string or a getter. Default none. */
|
|
53
|
-
systemPrompt?: string | (() => string | null | undefined);
|
|
54
|
-
/** The tools declared to the model, for the budget. A value or a getter. Default none. */
|
|
55
|
-
tools?: unknown | (() => unknown);
|
|
56
|
-
/** Partial override of the compactor's config — reserves, ratios, floors. */
|
|
57
|
-
config?: Partial<Omit<CompactionConfig, 'contextWindow'>>;
|
|
58
|
-
/** Never summarise fewer than this many of the newest messages. Default 2 — the last exchange. */
|
|
59
|
-
minKeep?: number;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Build the selector. Give it to the client:
|
|
63
|
-
*
|
|
64
|
-
* ```ts
|
|
65
|
-
* new AparteClient({
|
|
66
|
-
* compactionSelector: createCompactionSelector({
|
|
67
|
-
* contextWindow: () => aparteGlobalConfig.getCurrentModel()?.contextWindow,
|
|
68
|
-
* systemPrompt: () => aparteGlobalConfig.resolveSystemPrompt(),
|
|
69
|
-
* }),
|
|
70
|
-
* });
|
|
71
|
-
* ```
|
|
72
|
-
*
|
|
73
|
-
* Walks the history from the newest message: what fits the sliding window's budget
|
|
74
|
-
* is kept verbatim, the rest is dropped for summarising. When everything fits,
|
|
75
|
-
* `drop` is empty and `compact()` reports `{ skipped: true }` — nothing to do.
|
|
76
|
-
*/
|
|
77
|
-
export declare function createCompactionSelector(options: CompactionSelectorOptions): CompactionSelector;
|
|
78
|
-
//# sourceMappingURL=selector.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"selector.d.ts","sourceRoot":"","sources":["../../src/conversation/selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAA4D,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEjH;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;CACrC;AAED,uFAAuF;AACvF,MAAM,WAAW,mBAAmB,CAAC,CAAC;IAClC,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,IAAI,EAAE,CAAC,EAAE,CAAC;CACb;AAED;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,SAAS,kBAAkB,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAEzG,MAAM,WAAW,yBAAyB;IACtC;;;;;OAKG;IACH,aAAa,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC;IACnD,iGAAiG;IACjG,YAAY,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAC1D,0FAA0F;IAC1F,KAAK,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC;IAClC,6EAA6E;IAC7E,MAAM,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC,CAAC;IAC1D,kGAAkG;IAClG,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAeD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,yBAAyB,GAAG,kBAAkB,CAyB/F"}
|