@aparte/engine 0.2.0-alpha.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.
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @aparte/engine — the framework-agnostic agent loop.
3
+ *
4
+ * Zero runtime dependencies, no DOM: usable from any in-browser or Node AI-chat app.
5
+ * The headline export is `runStreamAgent`, the headless extraction of
6
+ * `AparteClient._streamLoop` — inject it via core's `streamRunner` seam and core
7
+ * renders its events through `createStreamAdapter`. Parity between the two is proven
8
+ * by the stream-parity suite.
9
+ *
10
+ * Deliberately just the loop core drives, plus the agnostic context compactor. Opt-in
11
+ * *tools* (ask-question / RAG / skills / code) belong in `plugins/*`; product behaviour
12
+ * (memory, intent orchestration) and the not-yet-wired text agent loop live elsewhere.
13
+ */
14
+ export * from './agent/stream-events.js';
15
+ export * from './agent/stream-run.js';
16
+ export * from './agent/parsers/artifact-xml-state-machine.js';
17
+ export * from './conversation/compactor.js';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,+CAA+C,CAAC;AAG9D,cAAc,6BAA6B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,545 @@
1
+ const CLOSE_TAG = "</artifact>";
2
+ const OPEN_TAG = "<artifact";
3
+ const INLINE_MAX_LINES = 15;
4
+ function deriveArtifactKind(mimeType, fallback = "unknown") {
5
+ const m = (mimeType || "").toLowerCase().trim();
6
+ const ant = m.match(/^application\/vnd\.ant\.([a-z0-9-]+)/);
7
+ if (ant) return ant[1];
8
+ if (m === "text/html" || m === "application/xhtml+xml") return "html";
9
+ if (m === "application/javascript" || m === "text/javascript") return "js";
10
+ if (m === "text/css") return "css";
11
+ if (m === "image/svg+xml") return "svg";
12
+ if (m === "application/json") return "json";
13
+ if (m === "text/markdown") return "markdown";
14
+ if (m === "text/csv") return "csv";
15
+ if (m === "text/plain") return "text";
16
+ if (m.includes("react")) return "react";
17
+ if (m.includes("html")) return "html";
18
+ if (m.includes("javascript")) return "js";
19
+ if (m.includes("css")) return "css";
20
+ if (m.includes("svg")) return "svg";
21
+ if (m.includes("json")) return "json";
22
+ if (m.includes("csv")) return "csv";
23
+ if (m.includes("markdown")) return "markdown";
24
+ return fallback;
25
+ }
26
+ class ArtifactXmlStateMachine {
27
+ constructor(hint, idGen) {
28
+ this.hint = hint;
29
+ this.idGen = idGen ?? (() => `artifact-xml-${this.seq++}`);
30
+ }
31
+ state = "normal";
32
+ /** Buffers the opening tag until its `>` arrives (may span deltas). */
33
+ scanBuf = "";
34
+ /** Buffers the tail that might be the start of a split `</artifact>`. */
35
+ closeBuf = "";
36
+ segId = null;
37
+ content = "";
38
+ mime = "";
39
+ kind = "";
40
+ title = "";
41
+ seq = 0;
42
+ idGen;
43
+ /** Feed one text delta; returns the ordered micro-events it produced. */
44
+ feed(delta) {
45
+ const out = [];
46
+ let remaining = delta;
47
+ while (remaining.length > 0) {
48
+ if (this.state === "normal") {
49
+ const tagStart = remaining.indexOf(OPEN_TAG);
50
+ if (tagStart === -1) {
51
+ out.push({ type: "chat-text", text: remaining });
52
+ remaining = "";
53
+ } else {
54
+ const before = remaining.slice(0, tagStart);
55
+ if (before) out.push({ type: "chat-text", text: before, reduced: true });
56
+ this.scanBuf = remaining.slice(tagStart);
57
+ remaining = "";
58
+ this.state = "scanning";
59
+ }
60
+ } else if (this.state === "scanning") {
61
+ this.scanBuf += remaining;
62
+ remaining = "";
63
+ const gtIdx = this.scanBuf.indexOf(">");
64
+ if (gtIdx !== -1) {
65
+ const tag = this.scanBuf.slice(0, gtIdx + 1);
66
+ this.mime = /mimeType=['"]([^'"]+)['"]/.exec(tag)?.[1] ?? this.hint.mimeType;
67
+ this.title = /title=['"]([^'"]+)['"]/.exec(tag)?.[1] ?? this.hint.kind;
68
+ this.kind = deriveArtifactKind(this.mime, this.hint.kind);
69
+ this.segId = this.idGen();
70
+ this.content = "";
71
+ out.push({ type: "artifact-open", id: this.segId, mimeType: this.mime, kind: this.kind, title: this.title });
72
+ this.state = "in-artifact";
73
+ remaining = this.scanBuf.slice(gtIdx + 1);
74
+ this.scanBuf = "";
75
+ }
76
+ } else {
77
+ const combined = this.closeBuf + remaining;
78
+ const closeIdx = combined.indexOf(CLOSE_TAG);
79
+ if (closeIdx !== -1) {
80
+ this.content += combined.slice(0, closeIdx);
81
+ const inline = this.content.split("\n").length < INLINE_MAX_LINES;
82
+ out.push({ type: "artifact-close", id: this.segId, content: this.content, inline });
83
+ this.state = "normal";
84
+ this.closeBuf = "";
85
+ remaining = combined.slice(closeIdx + CLOSE_TAG.length);
86
+ } else {
87
+ const safeLen = Math.max(0, combined.length - CLOSE_TAG.length + 1);
88
+ this.content += combined.slice(0, safeLen);
89
+ this.closeBuf = combined.slice(safeLen);
90
+ remaining = "";
91
+ if (this.segId) out.push({ type: "artifact-chunk", id: this.segId, content: this.content });
92
+ }
93
+ }
94
+ }
95
+ return out;
96
+ }
97
+ /**
98
+ * Flush a truncated artifact: if the stream ended mid-body (model cut off
99
+ * before `</artifact>`), emit a close with whatever was buffered. Mirrors
100
+ * `_streamLoop`'s finalize block (:1658-1669).
101
+ */
102
+ finalize() {
103
+ if (this.state === "in-artifact" && this.segId) {
104
+ this.content += this.closeBuf;
105
+ const inline = this.content.split("\n").length < INLINE_MAX_LINES;
106
+ const ev = { type: "artifact-close", id: this.segId, content: this.content, inline };
107
+ this.state = "normal";
108
+ this.closeBuf = "";
109
+ return [ev];
110
+ }
111
+ return [];
112
+ }
113
+ /** Current parser state (for the adapter to decide finalize routing). */
114
+ get currentState() {
115
+ return this.state;
116
+ }
117
+ }
118
+ const DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1e3;
119
+ const DEFAULT_MAX_TURNS = 10;
120
+ async function runStreamAgent(opts) {
121
+ const {
122
+ transportCall,
123
+ toolLookup,
124
+ toolConfigLookup,
125
+ approvalResolver,
126
+ emitter,
127
+ signal,
128
+ maxTurns = DEFAULT_MAX_TURNS,
129
+ toolTimeoutMs = DEFAULT_TOOL_TIMEOUT_MS
130
+ } = opts;
131
+ let baseRequest = opts.baseRequest;
132
+ let idSeq = 0;
133
+ const idGen = opts.idGen ?? ((prefix) => `${prefix}-${idSeq++}`);
134
+ const messages = [...baseRequest.messages];
135
+ const pipeline = baseRequest["_meta"]?.["pipeline"];
136
+ let pipelineIndex = 0;
137
+ let continueLoop = true;
138
+ let turns = 0;
139
+ let lastUsage;
140
+ emitter({ type: "run-start" });
141
+ while (continueLoop) {
142
+ if (signal.aborted) {
143
+ emitter({ type: "run-aborted" });
144
+ break;
145
+ }
146
+ turns++;
147
+ if (turns > maxTurns) {
148
+ emitter({ type: "turn-limit-exceeded", scope: "global", limit: maxTurns });
149
+ break;
150
+ }
151
+ const toolChoice = baseRequest["toolChoice"];
152
+ if (turns === 1 && toolChoice && typeof toolChoice === "object" && !Array.isArray(toolChoice) && toolChoice.input !== void 0) {
153
+ const tc = toolChoice;
154
+ const syntheticId = idGen("synthetic-tool");
155
+ emitter({ type: "tool-start", toolCallId: syntheticId, name: tc.name, input: tc.input });
156
+ const handler = toolLookup(tc.name);
157
+ if (!handler) {
158
+ emitter({ type: "tool-aborted", toolCallId: syntheticId });
159
+ continueLoop = false;
160
+ continue;
161
+ }
162
+ const outcome = await invokeToolHandler(handler, { id: syntheticId, name: tc.name, input: tc.input }, signal, toolTimeoutMs);
163
+ if (outcome.status === "aborted") {
164
+ emitter({ type: "tool-aborted", toolCallId: syntheticId });
165
+ continueLoop = false;
166
+ continue;
167
+ }
168
+ emitter({ type: "tool-resolved", toolCallId: syntheticId, result: outcome.content });
169
+ messages.push({ role: "tool_call", content: "", toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });
170
+ messages.push({ role: "tool_result", content: outcome.content, toolCallId: syntheticId });
171
+ baseRequest = { ...baseRequest, toolChoice: "none", tools: void 0 };
172
+ }
173
+ let phaseMessages = messages;
174
+ let phaseMeta = baseRequest["_meta"];
175
+ if (pipeline && pipelineIndex < pipeline.length) {
176
+ const phase = pipeline[pipelineIndex];
177
+ phaseMessages = [{ role: "system", content: phase.system }, ...messages];
178
+ if (phase.mode === "artifact") {
179
+ phaseMeta = { ...phaseMeta, artifactRaw: { mimeType: phase.mimeType, kind: phase.kind } };
180
+ } else {
181
+ const rest = { ...phaseMeta ?? {} };
182
+ delete rest["artifactRaw"];
183
+ delete rest["pipeline"];
184
+ phaseMeta = rest;
185
+ }
186
+ }
187
+ const request = { ...baseRequest, messages: phaseMessages, _meta: phaseMeta };
188
+ const response = await transportCall(request);
189
+ if (typeof response === "string") {
190
+ emitter({ type: "text-delta", delta: response });
191
+ break;
192
+ }
193
+ emitter({ type: "turn-start" });
194
+ const rawHint = request["_meta"]?.["artifactRaw"];
195
+ let rawSegId = null;
196
+ let rawContent = "";
197
+ if (rawHint) {
198
+ rawSegId = idGen("artifact-raw");
199
+ emitter({ type: "artifact-open", id: rawSegId, mimeType: rawHint.mimeType, kind: rawHint.kind, title: rawHint.kind });
200
+ }
201
+ const xmlHint = request["_meta"]?.["artifactXml"];
202
+ const xmlMachine = xmlHint && !rawHint ? new ArtifactXmlStateMachine(xmlHint, () => idGen("artifact-xml")) : null;
203
+ const emitXml = (events) => {
204
+ for (const ev of events) {
205
+ if (ev.type === "chat-text") emitter({ type: "text-delta", delta: ev.text, ...ev.reduced ? { reduced: true } : {} });
206
+ else if (ev.type === "artifact-open") emitter({ type: "artifact-open", id: ev.id, mimeType: ev.mimeType, kind: ev.kind, title: ev.title });
207
+ else if (ev.type === "artifact-chunk") emitter({ type: "artifact-chunk", id: ev.id, content: ev.content });
208
+ else emitter({ type: "artifact-close", id: ev.id, content: ev.content, inline: ev.inline });
209
+ }
210
+ };
211
+ let precedingText = "";
212
+ const toolCallsThisTurn = [];
213
+ const iterator = response[Symbol.asyncIterator]();
214
+ try {
215
+ while (true) {
216
+ if (signal.aborted) {
217
+ await iterator.return?.(void 0);
218
+ emitter({ type: "run-aborted" });
219
+ continueLoop = false;
220
+ break;
221
+ }
222
+ const step = await iterator.next();
223
+ if (step.done) break;
224
+ const event = step.value;
225
+ if (event.type === "thinking") {
226
+ emitter({ type: "thinking-delta", delta: event.delta });
227
+ continue;
228
+ }
229
+ if (event.type === "text") {
230
+ precedingText += event.delta;
231
+ if (rawSegId) {
232
+ rawContent += event.delta;
233
+ emitter({ type: "artifact-chunk", id: rawSegId, content: rawContent });
234
+ continue;
235
+ }
236
+ if (xmlMachine) {
237
+ emitXml(xmlMachine.feed(event.delta));
238
+ continue;
239
+ }
240
+ emitter({ type: "text-delta", delta: event.delta });
241
+ continue;
242
+ }
243
+ if (event.type === "done") {
244
+ if (event.usage) lastUsage = event.usage;
245
+ continue;
246
+ }
247
+ if (event.type === "error") {
248
+ throw new Error(event.message);
249
+ }
250
+ toolCallsThisTurn.push({ id: event.id, name: event.name, input: event.input });
251
+ if (event.name === "create_artifact") {
252
+ const input = event.input ?? {};
253
+ const mimeType = input.mimeType ?? "text/plain";
254
+ const kind = deriveArtifactKind(mimeType, "text");
255
+ emitter({ type: "artifact-ready", id: `artifact-${event.id}`, mimeType, kind, title: input.title ?? kind, content: input.content ?? "" });
256
+ messages.push({ role: "tool_call", content: "", toolCalls: [{ id: event.id, name: event.name, input: event.input }] });
257
+ messages.push({ role: "tool_result", content: "Artifact created successfully.", toolCallId: event.id });
258
+ continue;
259
+ }
260
+ emitter({ type: "tool-start", toolCallId: event.id, name: event.name, input: event.input });
261
+ const cfg = toolConfigLookup?.(event.name);
262
+ const effectiveMaxTurns = cfg?.maxTurns ?? maxTurns;
263
+ if (turns >= effectiveMaxTurns) {
264
+ emitter({ type: "turn-limit-exceeded", scope: "tool", limit: effectiveMaxTurns, toolCallId: event.id });
265
+ continueLoop = false;
266
+ break;
267
+ }
268
+ const handler = toolLookup(event.name);
269
+ if (!handler) {
270
+ emitter({ type: "tool-aborted", toolCallId: event.id });
271
+ continueLoop = false;
272
+ break;
273
+ }
274
+ let effectiveInput = event.input;
275
+ if (cfg?.needsApproval) {
276
+ emitter({ type: "tool-awaiting-approval", toolCallId: event.id, name: event.name, input: event.input });
277
+ const resolve = approvalResolver ?? (async () => ({ approved: false }));
278
+ const decision = await resolve(event.id, signal);
279
+ if (!decision.approved) {
280
+ const rejection = "Tool execution was rejected by the user.";
281
+ emitter({ type: "tool-rejected", toolCallId: event.id, reason: rejection });
282
+ pushToolCallEnvelope(messages, toolCallsThisTurn, precedingText);
283
+ messages.push({ role: "tool_result", content: rejection, toolCallId: event.id });
284
+ continueLoop = false;
285
+ break;
286
+ }
287
+ if (decision.payload && typeof decision.payload === "object" && !Array.isArray(decision.payload)) {
288
+ effectiveInput = { ...event.input, ...decision.payload };
289
+ }
290
+ emitter({ type: "tool-approved", toolCallId: event.id });
291
+ }
292
+ const outcome = await invokeToolHandler(
293
+ handler,
294
+ { id: event.id, name: event.name, input: effectiveInput },
295
+ signal,
296
+ toolTimeoutMs
297
+ );
298
+ if (outcome.status === "aborted") {
299
+ emitter({ type: "tool-aborted", toolCallId: event.id });
300
+ continueLoop = false;
301
+ } else {
302
+ emitter({ type: "tool-resolved", toolCallId: event.id, result: outcome.content });
303
+ pushToolCallEnvelope(messages, toolCallsThisTurn, precedingText);
304
+ messages.push({ role: "tool_result", content: outcome.content, toolCallId: event.id });
305
+ }
306
+ }
307
+ emitter({ type: "text-flush" });
308
+ if (rawSegId) {
309
+ const inline = rawContent.split("\n").length < 15;
310
+ emitter({ type: "artifact-close", id: rawSegId, content: rawContent, inline });
311
+ }
312
+ if (xmlMachine) emitXml(xmlMachine.finalize());
313
+ } finally {
314
+ await iterator.return?.(void 0).catch(() => {
315
+ });
316
+ }
317
+ if (toolCallsThisTurn.length === 0) {
318
+ if (pipeline && pipelineIndex < pipeline.length - 1) {
319
+ if (precedingText.trim()) {
320
+ messages.push({ role: "assistant", content: precedingText.trim() });
321
+ }
322
+ pipelineIndex++;
323
+ emitter({ type: "phase-advance", index: pipelineIndex });
324
+ } else {
325
+ continueLoop = false;
326
+ }
327
+ }
328
+ }
329
+ emitter({ type: "run-done", usage: lastUsage });
330
+ return lastUsage;
331
+ }
332
+ function pushToolCallEnvelope(messages, toolCallsThisTurn, precedingText) {
333
+ const exists = messages.some(
334
+ (m) => m.role === "tool_call" && m.toolCalls?.some((tc) => toolCallsThisTurn.some((t) => t.id === tc.id))
335
+ );
336
+ if (exists) return;
337
+ messages.push({
338
+ role: "tool_call",
339
+ content: "",
340
+ toolCalls: toolCallsThisTurn,
341
+ precedingText: precedingText.trim() || void 0
342
+ });
343
+ }
344
+ async function invokeToolHandler(handler, call, signal, toolTimeoutMs) {
345
+ if (signal.aborted) return { status: "aborted" };
346
+ const controller = new AbortController();
347
+ const onParentAbort = () => controller.abort();
348
+ signal.addEventListener("abort", onParentAbort, { once: true });
349
+ const timeout = setTimeout(() => controller.abort(), toolTimeoutMs);
350
+ try {
351
+ const result = await handler(call, controller.signal);
352
+ return { status: "resolved", content: result.content };
353
+ } catch (err) {
354
+ if (err?.name === "AbortError") return { status: "aborted" };
355
+ throw err;
356
+ } finally {
357
+ clearTimeout(timeout);
358
+ signal.removeEventListener("abort", onParentAbort);
359
+ }
360
+ }
361
+ const DEFAULT_COMPACTION_CONFIG = {
362
+ // Conservative model-agnostic defaults — the consuming app overrides
363
+ // `contextWindow` / `reservedThinking` with its model's real values.
364
+ contextWindow: 8192,
365
+ reservedThinking: 0,
366
+ reservedGeneration: 2e3,
367
+ autocompactBufferPct: 0.1,
368
+ // ~3300 tok
369
+ safetyMargin: 500,
370
+ minHistoryBudget: 1e3,
371
+ summaryRatio: 0.1,
372
+ summaryMaxTokens: 400,
373
+ ragHistRatio: 0.25,
374
+ ragHistMaxTokens: 1e3,
375
+ triggerSummaryThresholdPct: 0.75,
376
+ summarizeEveryNTurns: 5,
377
+ summaryLabel: "Conversation summary:",
378
+ ragIntroLabel: "Relevant excerpts from earlier in the conversation:"
379
+ };
380
+ function estimateTokens(text) {
381
+ if (!text) return 0;
382
+ return Math.ceil(text.length / 3.8);
383
+ }
384
+ function estimateTokensJson(obj) {
385
+ if (!obj) return 0;
386
+ try {
387
+ return estimateTokens(JSON.stringify(obj));
388
+ } catch {
389
+ return 0;
390
+ }
391
+ }
392
+ function computeHistoryBudget(input) {
393
+ const cfg = { ...DEFAULT_COMPACTION_CONFIG, ...input.config ?? {} };
394
+ const systemTokens = estimateTokens(input.systemPrompt);
395
+ const toolsTokens = estimateTokensJson(input.toolsArray);
396
+ const autocompactBuffer = Math.floor(cfg.contextWindow * cfg.autocompactBufferPct);
397
+ const fixed = systemTokens + toolsTokens + cfg.reservedThinking + cfg.reservedGeneration + autocompactBuffer + cfg.safetyMargin;
398
+ const historyBudget = Math.max(cfg.minHistoryBudget, cfg.contextWindow - fixed);
399
+ return {
400
+ historyBudget,
401
+ breakdown: {
402
+ contextWindow: cfg.contextWindow,
403
+ systemPrompt: systemTokens,
404
+ tools: toolsTokens,
405
+ reservedThinking: cfg.reservedThinking,
406
+ reservedGeneration: cfg.reservedGeneration,
407
+ autocompactBuffer,
408
+ safetyMargin: cfg.safetyMargin,
409
+ historyAvailable: historyBudget
410
+ },
411
+ config: cfg
412
+ };
413
+ }
414
+ function splitHistoryBudget(historyBudget, cfg = DEFAULT_COMPACTION_CONFIG) {
415
+ const summary = Math.min(cfg.summaryMaxTokens, Math.floor(historyBudget * cfg.summaryRatio));
416
+ const ragHist = Math.min(cfg.ragHistMaxTokens, Math.floor(historyBudget * cfg.ragHistRatio));
417
+ const window = historyBudget - summary - ragHist;
418
+ return { summary, ragHist, window };
419
+ }
420
+ function assembleCompacted(params) {
421
+ const {
422
+ messages,
423
+ summary = "",
424
+ retrievedTurns = [],
425
+ windowBudget,
426
+ summaryBudget,
427
+ ragBudget,
428
+ systemContent,
429
+ summaryLabel = DEFAULT_COMPACTION_CONFIG.summaryLabel,
430
+ ragIntroLabel = DEFAULT_COMPACTION_CONFIG.ragIntroLabel
431
+ } = params;
432
+ const out = [];
433
+ const used = { system: 0, summary: 0, ragHist: 0, window: 0 };
434
+ const dropped = { ragHits: 0, oldTurns: 0, summary: false };
435
+ if (systemContent) {
436
+ out.push({ role: "system", content: systemContent });
437
+ used.system = estimateTokens(systemContent);
438
+ }
439
+ if (summary) {
440
+ const summaryWrapped = `${summaryLabel}
441
+ ${summary}`;
442
+ const summaryToks = estimateTokens(summaryWrapped);
443
+ if (summaryToks <= summaryBudget) {
444
+ out.push({ role: "system", content: summaryWrapped });
445
+ used.summary = summaryToks;
446
+ } else {
447
+ const ratio = summaryBudget / summaryToks * 0.95;
448
+ const truncated = summary.slice(0, Math.max(0, Math.floor(summary.length * ratio)));
449
+ const wrapped = `${summaryLabel}
450
+ ${truncated}…`;
451
+ out.push({ role: "system", content: wrapped });
452
+ used.summary = estimateTokens(wrapped);
453
+ dropped.summary = true;
454
+ }
455
+ }
456
+ if (retrievedTurns.length > 0) {
457
+ const sorted = [...retrievedTurns].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
458
+ const lines = [];
459
+ const intro = `${ragIntroLabel}
460
+ `;
461
+ let toks = estimateTokens(intro);
462
+ for (const t of sorted) {
463
+ const line = `[${t.role}] ${t.content}`;
464
+ const lt = estimateTokens(line);
465
+ if (toks + lt > ragBudget) {
466
+ dropped.ragHits++;
467
+ continue;
468
+ }
469
+ lines.push(line);
470
+ toks += lt;
471
+ }
472
+ if (lines.length > 0) {
473
+ const content = intro + lines.join("\n");
474
+ out.push({ role: "system", content });
475
+ used.ragHist = estimateTokens(content);
476
+ }
477
+ }
478
+ const nonSystem = messages.filter((m) => m.role !== "system");
479
+ if (nonSystem.length === 0) {
480
+ return { compactedMessages: out, used, dropped };
481
+ }
482
+ const reversedWindow = [];
483
+ let windowToks = 0;
484
+ const minKeep = Math.min(2, nonSystem.length);
485
+ for (let i = nonSystem.length - 1; i >= 0; i--) {
486
+ const m = nonSystem[i];
487
+ if (!m) continue;
488
+ const t = estimateTokens(m.content);
489
+ const idxFromEnd = nonSystem.length - 1 - i;
490
+ if (idxFromEnd < minKeep) {
491
+ reversedWindow.unshift(m);
492
+ windowToks += t;
493
+ continue;
494
+ }
495
+ if (windowToks + t > windowBudget) {
496
+ dropped.oldTurns += i + 1;
497
+ break;
498
+ }
499
+ reversedWindow.unshift(m);
500
+ windowToks += t;
501
+ }
502
+ out.push(...reversedWindow);
503
+ used.window = windowToks;
504
+ return { compactedMessages: out, used, dropped };
505
+ }
506
+ function compactConversation(input) {
507
+ const { messages, systemPrompt, toolsArray, summary, retrievedTurns, config } = input;
508
+ const budget = computeHistoryBudget({ systemPrompt, toolsArray, config });
509
+ const split = splitHistoryBudget(budget.historyBudget, budget.config);
510
+ const { compactedMessages, used, dropped } = assembleCompacted({
511
+ messages,
512
+ summary,
513
+ retrievedTurns,
514
+ windowBudget: split.window,
515
+ summaryBudget: split.summary,
516
+ ragBudget: split.ragHist,
517
+ systemContent: systemPrompt,
518
+ summaryLabel: budget.config.summaryLabel,
519
+ ragIntroLabel: budget.config.ragIntroLabel
520
+ });
521
+ const totalUsed = budget.breakdown.systemPrompt + budget.breakdown.tools + used.summary + used.ragHist + used.window;
522
+ const free = budget.config.contextWindow - totalUsed - budget.config.reservedThinking - budget.config.reservedGeneration - budget.breakdown.autocompactBuffer - budget.config.safetyMargin;
523
+ const breakdown = {
524
+ ...budget.breakdown,
525
+ historyAllocated: split,
526
+ historyUsed: used,
527
+ totalUsed,
528
+ free,
529
+ dropped
530
+ };
531
+ return { compactedMessages, breakdown };
532
+ }
533
+ export {
534
+ ArtifactXmlStateMachine,
535
+ DEFAULT_COMPACTION_CONFIG,
536
+ assembleCompacted,
537
+ compactConversation,
538
+ computeHistoryBudget,
539
+ deriveArtifactKind,
540
+ estimateTokens,
541
+ estimateTokensJson,
542
+ runStreamAgent,
543
+ splitHistoryBudget
544
+ };
545
+ //# sourceMappingURL=index.js.map