@aparte/engine 0.3.0-alpha.0 → 0.5.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.
package/README.md CHANGED
@@ -17,4 +17,36 @@ transport and asserts identical output).
17
17
  `@aparte/core` is an **optional peer** — `runStreamAgent` and its parsers import nothing from
18
18
  it; the orchestrator/memory helpers use core's config/types when present.
19
19
 
20
+ ## Owning the history (prefix-cache hosts)
21
+
22
+ By default the loop keeps the message list and re-sends it every turn, enriched with the
23
+ `tool_call` / `tool_result` turns it produced. That is right for a stateless message API, and
24
+ wrong for a **prefix cache** — llama.cpp slots, vLLM — where turn N+1 must *extend* turn N
25
+ byte for byte or the cache is thrown away.
26
+
27
+ Such a host owns its own transcript. It already controls the request (`transportCall` receives
28
+ the built request and may ignore `request.messages`); `onHistoryAppend` is the other half —
29
+ it reports each turn the loop appends, in order, before the call that would carry it, so you
30
+ don't reimplement the loop's tool bookkeeping:
31
+
32
+ ```ts
33
+ const log = new PromptLog(); // your append-only transcript
34
+
35
+ await runStreamAgent({
36
+ // …messageId, emitter, signal, toolLookup
37
+ baseRequest: { messages: [] }, // unused by your transport
38
+ onHistoryAppend: (turn) => log.append(turn), // tool_call · tool_result · phase reply
39
+ transportCall: () => myCompletion(log.render()), // your own bytes, extended not rebuilt
40
+ });
41
+ ```
42
+
43
+ Wiring it through core's seam needs no core change — augment the options at injection:
44
+
45
+ ```ts
46
+ new AparteClient({ streamRunner: (opts) => runStreamAgent({ ...opts, onHistoryAppend }) });
47
+ ```
48
+
49
+ Serializing tool inventories/calls/results into a raw prompt is still yours to write: the
50
+ providers that do it target message-based APIs.
51
+
20
52
  > Part of the [aparté](https://github.com/apartejs/aparte) monorepo. ESM-only.
@@ -13,7 +13,7 @@
13
13
  * XML / create_artifact) · multi-phase pipeline · synthetic toolChoice bypass.
14
14
  * Code-fence promotion stays adapter-side (it needs the core parser).
15
15
  */
16
- import type { StreamRunEmitter, StreamChatEvent, StreamChatRequest, StreamToolHandler, StreamToolConfig, StreamApprovalResolver, StreamUsage } from './stream-events.js';
16
+ import type { StreamRunEmitter, StreamChatEvent, StreamChatRequest, StreamAgentMessage, StreamToolHandler, StreamToolConfig, StreamApprovalResolver, StreamUsage } from './stream-events.js';
17
17
  export interface StreamRunOptions {
18
18
  /** Id of the assistant message being streamed (opaque; carried in events). */
19
19
  messageId: string;
@@ -40,6 +40,21 @@ export interface StreamRunOptions {
40
40
  maxTurns?: number;
41
41
  /** Per-tool-call handler timeout in ms. @default 300000 */
42
42
  toolTimeoutMs?: number;
43
+ /**
44
+ * Called for every turn the loop appends to the history — the grouped
45
+ * `tool_call` envelope, each `tool_result` (resolved or rejected), and a
46
+ * pipeline phase's reply — in order, and always before the transport call that
47
+ * would carry it. Never called for the messages you passed in `baseRequest`.
48
+ *
49
+ * For hosts that own their own transcript. The loop re-sends its `messages`
50
+ * array every turn, which suits stateless message APIs but not a **prefix
51
+ * cache** (llama.cpp slots, vLLM), where turn N+1 must EXTEND turn N byte for
52
+ * byte. Such a host builds its own request in {@link transportCall} — ignoring
53
+ * `request.messages` — and mirrors these notifications into its append-only
54
+ * log, instead of reimplementing the loop's tool bookkeeping. Synchronous and
55
+ * ordered, like {@link emitter}.
56
+ */
57
+ onHistoryAppend?: (message: StreamAgentMessage) => void;
43
58
  /**
44
59
  * Generates artifact segment ids (`prefix` is e.g. `'artifact-raw'`). The
45
60
  * default is a deterministic per-run counter; the adapter injects a
@@ -1 +1 @@
1
- {"version":3,"file":"stream-run.d.ts","sourceRoot":"","sources":["../../src/agent/stream-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EAGjB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,WAAW,EACd,MAAM,oBAAoB,CAAC;AAmB5B,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,iFAAiF;IACjF,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;;;;;OAKG;IACH,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAmU7F"}
1
+ {"version":3,"file":"stream-run.d.ts","sourceRoot":"","sources":["../../src/agent/stream-run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EACR,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAElB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,WAAW,EACd,MAAM,oBAAoB,CAAC;AAmB5B,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,iFAAiF;IACjF,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;;;;;;;;;;;;;OAaG;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;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAyU7F"}
package/dist/index.js CHANGED
@@ -132,6 +132,10 @@ async function runStreamAgent(opts) {
132
132
  let idSeq = 0;
133
133
  const idGen = opts.idGen ?? ((prefix) => `${prefix}-${idSeq++}`);
134
134
  const messages = [...baseRequest.messages];
135
+ const append = (message) => {
136
+ messages.push(message);
137
+ opts.onHistoryAppend?.(message);
138
+ };
135
139
  const pipeline = baseRequest["_meta"]?.["pipeline"];
136
140
  let pipelineIndex = 0;
137
141
  let continueLoop = true;
@@ -166,8 +170,8 @@ async function runStreamAgent(opts) {
166
170
  continue;
167
171
  }
168
172
  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 });
173
+ append({ role: "tool_call", content: "", toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });
174
+ append({ role: "tool_result", content: outcome.content, toolCallId: syntheticId });
171
175
  baseRequest = { ...baseRequest, toolChoice: "none", tools: void 0 };
172
176
  }
173
177
  let phaseMessages = messages;
@@ -253,8 +257,8 @@ async function runStreamAgent(opts) {
253
257
  const mimeType = input.mimeType ?? "text/plain";
254
258
  const kind = deriveArtifactKind(mimeType, "text");
255
259
  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 });
260
+ append({ role: "tool_call", content: "", toolCalls: [{ id: event.id, name: event.name, input: event.input }] });
261
+ append({ role: "tool_result", content: "Artifact created successfully.", toolCallId: event.id });
258
262
  continue;
259
263
  }
260
264
  emitter({ type: "tool-start", toolCallId: event.id, name: event.name, input: event.input });
@@ -279,8 +283,8 @@ async function runStreamAgent(opts) {
279
283
  if (!decision.approved) {
280
284
  const rejection = "Tool execution was rejected by the user.";
281
285
  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 });
286
+ pushToolCallEnvelope(messages, append, toolCallsThisTurn, precedingText);
287
+ append({ role: "tool_result", content: rejection, toolCallId: event.id });
284
288
  continueLoop = false;
285
289
  break;
286
290
  }
@@ -300,8 +304,8 @@ async function runStreamAgent(opts) {
300
304
  continueLoop = false;
301
305
  } else {
302
306
  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 });
307
+ pushToolCallEnvelope(messages, append, toolCallsThisTurn, precedingText);
308
+ append({ role: "tool_result", content: outcome.content, toolCallId: event.id });
305
309
  }
306
310
  }
307
311
  emitter({ type: "text-flush" });
@@ -317,7 +321,7 @@ async function runStreamAgent(opts) {
317
321
  if (toolCallsThisTurn.length === 0) {
318
322
  if (pipeline && pipelineIndex < pipeline.length - 1) {
319
323
  if (precedingText.trim()) {
320
- messages.push({ role: "assistant", content: precedingText.trim() });
324
+ append({ role: "assistant", content: precedingText.trim() });
321
325
  }
322
326
  pipelineIndex++;
323
327
  emitter({ type: "phase-advance", index: pipelineIndex });
@@ -329,12 +333,12 @@ async function runStreamAgent(opts) {
329
333
  emitter({ type: "run-done", usage: lastUsage });
330
334
  return lastUsage;
331
335
  }
332
- function pushToolCallEnvelope(messages, toolCallsThisTurn, precedingText) {
336
+ function pushToolCallEnvelope(messages, append, toolCallsThisTurn, precedingText) {
333
337
  const exists = messages.some(
334
338
  (m) => m.role === "tool_call" && m.toolCalls?.some((tc) => toolCallsThisTurn.some((t) => t.id === tc.id))
335
339
  );
336
340
  if (exists) return;
337
- messages.push({
341
+ append({
338
342
  role: "tool_call",
339
343
  content: "",
340
344
  toolCalls: toolCallsThisTurn,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/agent/parsers/artifact-xml-state-machine.ts","../src/agent/stream-run.ts","../src/conversation/compactor.ts"],"sourcesContent":["/**\n * artifact-xml-state-machine.ts — streaming `<artifact>` XML parser (pure).\n *\n * The framework-free extraction of the XML-artifact branch inside\n * `AparteClient._streamLoop` (aparte-client.ts :1268-1392 + finalize :1658-1669).\n * Small models emit artifacts as `…chat text… <artifact mimeType=\"…\" title=\"…\">\n * BODY</artifact> …more text…`, streamed in arbitrary chunks that can split the\n * opening tag, the body, or the closing tag across delta boundaries.\n *\n * This module owns only the PARSING (state + buffering); it emits DOM-free\n * micro-events. The `@aparte/core` adapter turns them into segment/lifecycle calls\n * (addSegment / updateSegment / _dispatchArtifactLifecycle) exactly as\n * `_streamLoop` does — so no DOM, no `@aparte/core` import here.\n *\n * Ported faithfully, including quirks: a `<artifact` split as `<arti`+`fact`\n * (so `indexOf('<artifact')` misses it) is treated as chat text, matching the\n * source. Providers emit the tag atomically in practice.\n */\n\n/** Where the streaming parser is between artifacts. */\nexport type XmlArtifactState = 'normal' | 'scanning' | 'in-artifact';\n\n/** Fallback mime/kind for an artifact whose open tag omits the attribute. */\nexport interface XmlArtifactHint {\n mimeType: string;\n kind: string;\n}\n\n/** DOM-free micro-events the machine emits; the adapter renders them. */\nexport type XmlArtifactEvent =\n // `reduced: true` marks chat text that precedes an `<artifact>` open tag in\n // the same delta. `_streamLoop` renders that text through a REDUCED path\n // (aparte-client.ts :1300-1313: only completed segments are added; the trailing\n // active segment is NOT rendered until a later, tag-free delta). The adapter\n // must honor it or the pre-artifact text streams one update too eagerly.\n | { type: 'chat-text'; text: string; reduced?: boolean }\n | { type: 'artifact-open'; id: string; mimeType: string; kind: string; title: string }\n | { type: 'artifact-chunk'; id: string; content: string }\n | { type: 'artifact-close'; id: string; content: string; inline: boolean };\n\nconst CLOSE_TAG = '</artifact>';\nconst OPEN_TAG = '<artifact';\n/** Artifacts under this many lines render inline (mirrors `lineCount < 15`). */\nconst INLINE_MAX_LINES = 15;\n\n/**\n * Map an artifact mimeType to a renderer kind. Byte-identical copy of core's\n * canonical `deriveArtifactKind` (parsers/aparte-stream-parser.ts) — the\n * duplicate stays because `@aparte/core` is an OPTIONAL peer of the engine, so\n * no runtime import is possible. Kept in sync mechanically by\n * `__tests__/derive-artifact-kind-parity.test.ts`.\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/**\n * A stateful streaming parser for one turn's `<artifact>` blocks. Feed it each\n * text delta; drain the returned events. Call {@link finalize} when the stream\n * ends to flush a truncated (unclosed) artifact.\n */\nexport class ArtifactXmlStateMachine {\n private state: XmlArtifactState = 'normal';\n /** Buffers the opening tag until its `>` arrives (may span deltas). */\n private scanBuf = '';\n /** Buffers the tail that might be the start of a split `</artifact>`. */\n private closeBuf = '';\n private segId: string | null = null;\n private content = '';\n private mime = '';\n private kind = '';\n private title = '';\n private seq = 0;\n private readonly idGen: () => string;\n\n constructor(private readonly hint: XmlArtifactHint, idGen?: () => string) {\n this.idGen = idGen ?? (() => `artifact-xml-${this.seq++}`);\n }\n\n /** Feed one text delta; returns the ordered micro-events it produced. */\n feed(delta: string): XmlArtifactEvent[] {\n const out: XmlArtifactEvent[] = [];\n let remaining = delta;\n\n while (remaining.length > 0) {\n if (this.state === 'normal') {\n const tagStart = remaining.indexOf(OPEN_TAG);\n if (tagStart === -1) {\n out.push({ type: 'chat-text', text: remaining });\n remaining = '';\n } else {\n const before = remaining.slice(0, tagStart);\n if (before) out.push({ type: 'chat-text', text: before, reduced: true });\n this.scanBuf = remaining.slice(tagStart);\n remaining = '';\n this.state = 'scanning';\n }\n } else if (this.state === 'scanning') {\n // Accumulate until we have the full opening tag (ends with `>`).\n this.scanBuf += remaining;\n remaining = '';\n const gtIdx = this.scanBuf.indexOf('>');\n if (gtIdx !== -1) {\n const tag = this.scanBuf.slice(0, gtIdx + 1);\n this.mime = /mimeType=['\"]([^'\"]+)['\"]/.exec(tag)?.[1] ?? this.hint.mimeType;\n this.title = /title=['\"]([^'\"]+)['\"]/.exec(tag)?.[1] ?? this.hint.kind;\n this.kind = deriveArtifactKind(this.mime, this.hint.kind);\n this.segId = this.idGen();\n this.content = '';\n out.push({ type: 'artifact-open', id: this.segId, mimeType: this.mime, kind: this.kind, title: this.title });\n this.state = 'in-artifact';\n remaining = this.scanBuf.slice(gtIdx + 1);\n this.scanBuf = '';\n }\n } else {\n // in-artifact — stream body until `</artifact>` (may be split).\n const combined = this.closeBuf + remaining;\n const closeIdx = combined.indexOf(CLOSE_TAG);\n if (closeIdx !== -1) {\n this.content += combined.slice(0, closeIdx);\n const inline = this.content.split('\\n').length < INLINE_MAX_LINES;\n out.push({ type: 'artifact-close', id: this.segId!, content: this.content, inline });\n this.state = 'normal';\n this.closeBuf = '';\n remaining = combined.slice(closeIdx + CLOSE_TAG.length);\n } else {\n // Keep the last (CLOSE_TAG.length - 1) chars buffered in case the\n // closing tag is split across this and the next delta.\n const safeLen = Math.max(0, combined.length - CLOSE_TAG.length + 1);\n this.content += combined.slice(0, safeLen);\n this.closeBuf = combined.slice(safeLen);\n remaining = '';\n if (this.segId) out.push({ type: 'artifact-chunk', id: this.segId, content: this.content });\n }\n }\n }\n\n return out;\n }\n\n /**\n * Flush a truncated artifact: if the stream ended mid-body (model cut off\n * before `</artifact>`), emit a close with whatever was buffered. Mirrors\n * `_streamLoop`'s finalize block (:1658-1669).\n */\n finalize(): XmlArtifactEvent[] {\n if (this.state === 'in-artifact' && this.segId) {\n this.content += this.closeBuf;\n const inline = this.content.split('\\n').length < INLINE_MAX_LINES;\n const ev: XmlArtifactEvent = { type: 'artifact-close', id: this.segId, content: this.content, inline };\n this.state = 'normal';\n this.closeBuf = '';\n return [ev];\n }\n return [];\n }\n\n /** Current parser state (for the adapter to decide finalize routing). */\n get currentState(): XmlArtifactState {\n return this.state;\n }\n}\n","/**\n * stream-run.ts — framework-free structured-stream agent loop.\n *\n * The headless extraction of `AparteClient._streamLoop`: 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 `_streamLoop`'s exact order) that\n * an adapter in `@aparte/core` turns into `targetElement.*` calls.\n *\n * Parity target: `@aparte/core`'s `AparteClient._streamLoop`.\n * Scope: text · thinking · tool_use (+ HITL) · done · error · artifacts (raw /\n * XML / create_artifact) · multi-phase pipeline · synthetic toolChoice bypass.\n * Code-fence promotion stays adapter-side (it needs the core parser).\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 { ArtifactXmlStateMachine, deriveArtifactKind, type XmlArtifactEvent } from './parsers/artifact-xml-state-machine.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\n/**\n * One phase of a multi-phase pipeline (mirrors the shape of `ApartePipelinePhase`,\n * the pipeline-phase type defined in core's `types/chat.ts`). Supplied via\n * `baseRequest._meta.pipeline`; each phase runs as one turn with its own system\n * message, and an `'artifact'` phase streams the whole turn into a raw artifact.\n */\ntype PipelinePhase =\n | { mode: 'text'; system: string }\n | { mode: 'thinking'; system: string; label?: string }\n | { mode: 'artifact'; system: string; mimeType: string; kind: string };\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 `AparteConfig.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 * 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/**\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 */\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.\n const messages: StreamAgentMessage[] = [...baseRequest.messages];\n\n // Pipeline mode (mirrors _streamLoop): each phase is one turn with\n // its own system message; phase N's reply is appended before phase N+1.\n const pipeline = (baseRequest['_meta'] as Record<string, unknown> | undefined)?.['pipeline'] as\n | PipelinePhase[]\n | undefined;\n let pipelineIndex = 0;\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 const tc = toolChoice as { name: string; input: 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 messages.push({ role: 'tool_call', content: '', toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });\n messages.push({ role: 'tool_result', content: outcome.content, toolCallId: syntheticId });\n baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined };\n }\n\n // ── Per-phase request build when pipeline is active ──────\n // Prepend the current phase's system message and, for an 'artifact'\n // phase, inject the artifactRaw hint the streaming loop below reads.\n let phaseMessages = messages;\n let phaseMeta = baseRequest['_meta'] as Record<string, unknown> | undefined;\n if (pipeline && pipelineIndex < pipeline.length) {\n const phase = pipeline[pipelineIndex]!;\n phaseMessages = [{ role: 'system', content: phase.system }, ...messages];\n if (phase.mode === 'artifact') {\n phaseMeta = { ...phaseMeta, artifactRaw: { mimeType: phase.mimeType, kind: phase.kind } };\n } else {\n // Drop any stale artifactRaw / pipeline keys from a text phase.\n const rest = { ...(phaseMeta ?? {}) };\n delete rest['artifactRaw'];\n delete rest['pipeline'];\n phaseMeta = rest;\n }\n }\n\n const request: StreamChatRequest = { ...baseRequest, messages: phaseMessages, _meta: phaseMeta };\n const response = await transportCall(request);\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 if (typeof response === 'string') {\n emitter({ type: 'text-delta', delta: response });\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 // artifactRaw mode: the WHOLE turn's text streams into one artifact\n // segment (mirrors _streamLoop). Open it up-front, before the\n // first delta, exactly as _streamLoop does in its per-turn setup.\n const rawHint = (request['_meta'] as Record<string, unknown> | undefined)?.['artifactRaw'] as\n | { mimeType: string; kind: string }\n | undefined;\n let rawSegId: string | null = null;\n let rawContent = '';\n if (rawHint) {\n rawSegId = idGen('artifact-raw');\n emitter({ type: 'artifact-open', id: rawSegId, mimeType: rawHint.mimeType, kind: rawHint.kind, title: rawHint.kind });\n }\n\n // artifactXml mode (mutually exclusive with raw; raw wins, matching the\n // _streamLoop branch order). The E2 state machine parses inline\n // `<artifact>` tags; its micro-events map 1:1 to StreamRunEvents.\n const xmlHint = (request['_meta'] as Record<string, unknown> | undefined)?.['artifactXml'] as\n | { mimeType: string; kind: string }\n | undefined;\n const xmlMachine = (xmlHint && !rawHint)\n ? new ArtifactXmlStateMachine(xmlHint, () => idGen('artifact-xml'))\n : null;\n const emitXml = (events: XmlArtifactEvent[]): void => {\n for (const ev of events) {\n if (ev.type === 'chat-text') emitter({ type: 'text-delta', delta: ev.text, ...(ev.reduced ? { reduced: true } : {}) });\n else if (ev.type === 'artifact-open') emitter({ type: 'artifact-open', id: ev.id, mimeType: ev.mimeType, kind: ev.kind, title: ev.title });\n else if (ev.type === 'artifact-chunk') emitter({ type: 'artifact-chunk', id: ev.id, content: ev.content });\n else emitter({ type: 'artifact-close', id: ev.id, content: ev.content, inline: ev.inline });\n }\n };\n\n // Per-turn streaming state.\n let precedingText = '';\n const toolCallsThisTurn: StreamToolCall[] = [];\n\n // ── inner (SSE) loop — manual iteration so we can abort before each read\n const iterator = response[Symbol.asyncIterator]();\n try {\n while (true) {\n if (signal.aborted) {\n await iterator.return?.(undefined);\n emitter({ type: 'run-aborted' });\n continueLoop = false;\n break;\n }\n\n const step = await iterator.next();\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 if (rawSegId) {\n // artifactRaw: route the whole delta into the artifact,\n // never through the text parser (mirrors the raw-artifact path in _streamLoop).\n rawContent += event.delta;\n emitter({ type: 'artifact-chunk', id: rawSegId, content: rawContent });\n continue;\n }\n if (xmlMachine) {\n // artifactXml: run the delta through the state machine\n // (mirrors the XML-artifact path in _streamLoop); it splits chat text from artifacts.\n emitXml(xmlMachine.feed(event.delta));\n continue;\n }\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 // event.type === 'tool_use'\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 messages.push({ role: 'tool_call', content: '', toolCalls: [{ id: event.id, name: event.name, input: event.input }] });\n messages.push({ 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 const resolve = approvalResolver ?? (async () => ({ approved: false }));\n const decision = await resolve(event.id, signal);\n\n if (!decision.approved) {\n const rejection = 'Tool execution was rejected by the user.';\n emitter({ type: 'tool-rejected', toolCallId: event.id, reason: rejection });\n pushToolCallEnvelope(messages, toolCallsThisTurn, precedingText);\n messages.push({ role: 'tool_result', content: rejection, toolCallId: event.id });\n continueLoop = false;\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 emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n } else {\n emitter({ type: 'tool-resolved', toolCallId: event.id, result: outcome.content });\n pushToolCallEnvelope(messages, toolCallsThisTurn, precedingText);\n messages.push({ 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 // artifactRaw close comes right after the parser flush, matching the\n // finalize-block order in _streamLoop (text-flush, then artifact-close).\n if (rawSegId) {\n const inline = rawContent.split('\\n').length < 15;\n emitter({ type: 'artifact-close', id: rawSegId, content: rawContent, inline });\n }\n // artifactXml finalize flushes a truncated (unclosed) artifact —\n // after text-flush, mirroring the finalize-block order in _streamLoop.\n if (xmlMachine) emitXml(xmlMachine.finalize());\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 → final answer, OR advance to the next pipeline\n // phase (mirrors _streamLoop).\n if (toolCallsThisTurn.length === 0) {\n if (pipeline && pipelineIndex < pipeline.length - 1) {\n // Feed this phase's reply into history as context for the next.\n if (precedingText.trim()) {\n messages.push({ role: 'assistant', content: precedingText.trim() });\n }\n pipelineIndex++;\n emitter({ type: 'phase-advance', index: pipelineIndex });\n // continueLoop stays true — the next iteration runs the new phase.\n } else {\n continueLoop = false;\n }\n }\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 * Push the single grouped `tool_call` envelope for the turn — but only once,\n * even when the turn has several tool calls (each call's `tool_result` is pushed\n * separately). Mirrors `_streamLoop`'s `existingToolCallMsg` guard.\n */\nfunction pushToolCallEnvelope(\n messages: StreamAgentMessage[],\n toolCallsThisTurn: StreamToolCall[],\n precedingText: string,\n): void {\n const exists = messages.some(\n m => m.role === 'tool_call' && m.toolCalls?.some(tc => toolCallsThisTurn.some(t => t.id === tc.id)),\n );\n if (exists) return;\n messages.push({\n role: 'tool_call',\n content: '',\n toolCalls: toolCallsThisTurn,\n precedingText: precedingText.trim() || undefined,\n });\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 const timeout = setTimeout(() => controller.abort(), toolTimeoutMs);\n try {\n const result = await handler(call, controller.signal);\n return { status: 'resolved', content: result.content };\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 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"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/agent/parsers/artifact-xml-state-machine.ts","../src/agent/stream-run.ts","../src/conversation/compactor.ts"],"sourcesContent":["/**\n * artifact-xml-state-machine.ts — streaming `<artifact>` XML parser (pure).\n *\n * The framework-free extraction of the XML-artifact branch inside\n * `AparteClient._streamLoop` (aparte-client.ts :1268-1392 + finalize :1658-1669).\n * Small models emit artifacts as `…chat text… <artifact mimeType=\"…\" title=\"…\">\n * BODY</artifact> …more text…`, streamed in arbitrary chunks that can split the\n * opening tag, the body, or the closing tag across delta boundaries.\n *\n * This module owns only the PARSING (state + buffering); it emits DOM-free\n * micro-events. The `@aparte/core` adapter turns them into segment/lifecycle calls\n * (addSegment / updateSegment / _dispatchArtifactLifecycle) exactly as\n * `_streamLoop` does — so no DOM, no `@aparte/core` import here.\n *\n * Ported faithfully, including quirks: a `<artifact` split as `<arti`+`fact`\n * (so `indexOf('<artifact')` misses it) is treated as chat text, matching the\n * source. Providers emit the tag atomically in practice.\n */\n\n/** Where the streaming parser is between artifacts. */\nexport type XmlArtifactState = 'normal' | 'scanning' | 'in-artifact';\n\n/** Fallback mime/kind for an artifact whose open tag omits the attribute. */\nexport interface XmlArtifactHint {\n mimeType: string;\n kind: string;\n}\n\n/** DOM-free micro-events the machine emits; the adapter renders them. */\nexport type XmlArtifactEvent =\n // `reduced: true` marks chat text that precedes an `<artifact>` open tag in\n // the same delta. `_streamLoop` renders that text through a REDUCED path\n // (aparte-client.ts :1300-1313: only completed segments are added; the trailing\n // active segment is NOT rendered until a later, tag-free delta). The adapter\n // must honor it or the pre-artifact text streams one update too eagerly.\n | { type: 'chat-text'; text: string; reduced?: boolean }\n | { type: 'artifact-open'; id: string; mimeType: string; kind: string; title: string }\n | { type: 'artifact-chunk'; id: string; content: string }\n | { type: 'artifact-close'; id: string; content: string; inline: boolean };\n\nconst CLOSE_TAG = '</artifact>';\nconst OPEN_TAG = '<artifact';\n/** Artifacts under this many lines render inline (mirrors `lineCount < 15`). */\nconst INLINE_MAX_LINES = 15;\n\n/**\n * Map an artifact mimeType to a renderer kind. Byte-identical copy of core's\n * canonical `deriveArtifactKind` (parsers/aparte-stream-parser.ts) — the\n * duplicate stays because `@aparte/core` is an OPTIONAL peer of the engine, so\n * no runtime import is possible. Kept in sync mechanically by\n * `__tests__/derive-artifact-kind-parity.test.ts`.\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/**\n * A stateful streaming parser for one turn's `<artifact>` blocks. Feed it each\n * text delta; drain the returned events. Call {@link finalize} when the stream\n * ends to flush a truncated (unclosed) artifact.\n */\nexport class ArtifactXmlStateMachine {\n private state: XmlArtifactState = 'normal';\n /** Buffers the opening tag until its `>` arrives (may span deltas). */\n private scanBuf = '';\n /** Buffers the tail that might be the start of a split `</artifact>`. */\n private closeBuf = '';\n private segId: string | null = null;\n private content = '';\n private mime = '';\n private kind = '';\n private title = '';\n private seq = 0;\n private readonly idGen: () => string;\n\n constructor(private readonly hint: XmlArtifactHint, idGen?: () => string) {\n this.idGen = idGen ?? (() => `artifact-xml-${this.seq++}`);\n }\n\n /** Feed one text delta; returns the ordered micro-events it produced. */\n feed(delta: string): XmlArtifactEvent[] {\n const out: XmlArtifactEvent[] = [];\n let remaining = delta;\n\n while (remaining.length > 0) {\n if (this.state === 'normal') {\n const tagStart = remaining.indexOf(OPEN_TAG);\n if (tagStart === -1) {\n out.push({ type: 'chat-text', text: remaining });\n remaining = '';\n } else {\n const before = remaining.slice(0, tagStart);\n if (before) out.push({ type: 'chat-text', text: before, reduced: true });\n this.scanBuf = remaining.slice(tagStart);\n remaining = '';\n this.state = 'scanning';\n }\n } else if (this.state === 'scanning') {\n // Accumulate until we have the full opening tag (ends with `>`).\n this.scanBuf += remaining;\n remaining = '';\n const gtIdx = this.scanBuf.indexOf('>');\n if (gtIdx !== -1) {\n const tag = this.scanBuf.slice(0, gtIdx + 1);\n this.mime = /mimeType=['\"]([^'\"]+)['\"]/.exec(tag)?.[1] ?? this.hint.mimeType;\n this.title = /title=['\"]([^'\"]+)['\"]/.exec(tag)?.[1] ?? this.hint.kind;\n this.kind = deriveArtifactKind(this.mime, this.hint.kind);\n this.segId = this.idGen();\n this.content = '';\n out.push({ type: 'artifact-open', id: this.segId, mimeType: this.mime, kind: this.kind, title: this.title });\n this.state = 'in-artifact';\n remaining = this.scanBuf.slice(gtIdx + 1);\n this.scanBuf = '';\n }\n } else {\n // in-artifact — stream body until `</artifact>` (may be split).\n const combined = this.closeBuf + remaining;\n const closeIdx = combined.indexOf(CLOSE_TAG);\n if (closeIdx !== -1) {\n this.content += combined.slice(0, closeIdx);\n const inline = this.content.split('\\n').length < INLINE_MAX_LINES;\n out.push({ type: 'artifact-close', id: this.segId!, content: this.content, inline });\n this.state = 'normal';\n this.closeBuf = '';\n remaining = combined.slice(closeIdx + CLOSE_TAG.length);\n } else {\n // Keep the last (CLOSE_TAG.length - 1) chars buffered in case the\n // closing tag is split across this and the next delta.\n const safeLen = Math.max(0, combined.length - CLOSE_TAG.length + 1);\n this.content += combined.slice(0, safeLen);\n this.closeBuf = combined.slice(safeLen);\n remaining = '';\n if (this.segId) out.push({ type: 'artifact-chunk', id: this.segId, content: this.content });\n }\n }\n }\n\n return out;\n }\n\n /**\n * Flush a truncated artifact: if the stream ended mid-body (model cut off\n * before `</artifact>`), emit a close with whatever was buffered. Mirrors\n * `_streamLoop`'s finalize block (:1658-1669).\n */\n finalize(): XmlArtifactEvent[] {\n if (this.state === 'in-artifact' && this.segId) {\n this.content += this.closeBuf;\n const inline = this.content.split('\\n').length < INLINE_MAX_LINES;\n const ev: XmlArtifactEvent = { type: 'artifact-close', id: this.segId, content: this.content, inline };\n this.state = 'normal';\n this.closeBuf = '';\n return [ev];\n }\n return [];\n }\n\n /** Current parser state (for the adapter to decide finalize routing). */\n get currentState(): XmlArtifactState {\n return this.state;\n }\n}\n","/**\n * stream-run.ts — framework-free structured-stream agent loop.\n *\n * The headless extraction of `AparteClient._streamLoop`: 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 `_streamLoop`'s exact order) that\n * an adapter in `@aparte/core` turns into `targetElement.*` calls.\n *\n * Parity target: `@aparte/core`'s `AparteClient._streamLoop`.\n * Scope: text · thinking · tool_use (+ HITL) · done · error · artifacts (raw /\n * XML / create_artifact) · multi-phase pipeline · synthetic toolChoice bypass.\n * Code-fence promotion stays adapter-side (it needs the core parser).\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 { ArtifactXmlStateMachine, deriveArtifactKind, type XmlArtifactEvent } from './parsers/artifact-xml-state-machine.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\n/**\n * One phase of a multi-phase pipeline (mirrors the shape of `ApartePipelinePhase`,\n * the pipeline-phase type defined in core's `types/chat.ts`). Supplied via\n * `baseRequest._meta.pipeline`; each phase runs as one turn with its own system\n * message, and an `'artifact'` phase streams the whole turn into a raw artifact.\n */\ntype PipelinePhase =\n | { mode: 'text'; system: string }\n | { mode: 'thinking'; system: string; label?: string }\n | { mode: 'artifact'; system: string; mimeType: string; kind: string };\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 `AparteConfig.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 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/**\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 */\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 // Pipeline mode (mirrors _streamLoop): each phase is one turn with\n // its own system message; phase N's reply is appended before phase N+1.\n const pipeline = (baseRequest['_meta'] as Record<string, unknown> | undefined)?.['pipeline'] as\n | PipelinePhase[]\n | undefined;\n let pipelineIndex = 0;\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 const tc = toolChoice as { name: string; input: 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 // ── Per-phase request build when pipeline is active ──────\n // Prepend the current phase's system message and, for an 'artifact'\n // phase, inject the artifactRaw hint the streaming loop below reads.\n let phaseMessages = messages;\n let phaseMeta = baseRequest['_meta'] as Record<string, unknown> | undefined;\n if (pipeline && pipelineIndex < pipeline.length) {\n const phase = pipeline[pipelineIndex]!;\n phaseMessages = [{ role: 'system', content: phase.system }, ...messages];\n if (phase.mode === 'artifact') {\n phaseMeta = { ...phaseMeta, artifactRaw: { mimeType: phase.mimeType, kind: phase.kind } };\n } else {\n // Drop any stale artifactRaw / pipeline keys from a text phase.\n const rest = { ...(phaseMeta ?? {}) };\n delete rest['artifactRaw'];\n delete rest['pipeline'];\n phaseMeta = rest;\n }\n }\n\n const request: StreamChatRequest = { ...baseRequest, messages: phaseMessages, _meta: phaseMeta };\n const response = await transportCall(request);\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 if (typeof response === 'string') {\n emitter({ type: 'text-delta', delta: response });\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 // artifactRaw mode: the WHOLE turn's text streams into one artifact\n // segment (mirrors _streamLoop). Open it up-front, before the\n // first delta, exactly as _streamLoop does in its per-turn setup.\n const rawHint = (request['_meta'] as Record<string, unknown> | undefined)?.['artifactRaw'] as\n | { mimeType: string; kind: string }\n | undefined;\n let rawSegId: string | null = null;\n let rawContent = '';\n if (rawHint) {\n rawSegId = idGen('artifact-raw');\n emitter({ type: 'artifact-open', id: rawSegId, mimeType: rawHint.mimeType, kind: rawHint.kind, title: rawHint.kind });\n }\n\n // artifactXml mode (mutually exclusive with raw; raw wins, matching the\n // _streamLoop branch order). The E2 state machine parses inline\n // `<artifact>` tags; its micro-events map 1:1 to StreamRunEvents.\n const xmlHint = (request['_meta'] as Record<string, unknown> | undefined)?.['artifactXml'] as\n | { mimeType: string; kind: string }\n | undefined;\n const xmlMachine = (xmlHint && !rawHint)\n ? new ArtifactXmlStateMachine(xmlHint, () => idGen('artifact-xml'))\n : null;\n const emitXml = (events: XmlArtifactEvent[]): void => {\n for (const ev of events) {\n if (ev.type === 'chat-text') emitter({ type: 'text-delta', delta: ev.text, ...(ev.reduced ? { reduced: true } : {}) });\n else if (ev.type === 'artifact-open') emitter({ type: 'artifact-open', id: ev.id, mimeType: ev.mimeType, kind: ev.kind, title: ev.title });\n else if (ev.type === 'artifact-chunk') emitter({ type: 'artifact-chunk', id: ev.id, content: ev.content });\n else emitter({ type: 'artifact-close', id: ev.id, content: ev.content, inline: ev.inline });\n }\n };\n\n // Per-turn streaming state.\n let precedingText = '';\n const toolCallsThisTurn: StreamToolCall[] = [];\n\n // ── inner (SSE) loop — manual iteration so we can abort before each read\n const iterator = response[Symbol.asyncIterator]();\n try {\n while (true) {\n if (signal.aborted) {\n await iterator.return?.(undefined);\n emitter({ type: 'run-aborted' });\n continueLoop = false;\n break;\n }\n\n const step = await iterator.next();\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 if (rawSegId) {\n // artifactRaw: route the whole delta into the artifact,\n // never through the text parser (mirrors the raw-artifact path in _streamLoop).\n rawContent += event.delta;\n emitter({ type: 'artifact-chunk', id: rawSegId, content: rawContent });\n continue;\n }\n if (xmlMachine) {\n // artifactXml: run the delta through the state machine\n // (mirrors the XML-artifact path in _streamLoop); it splits chat text from artifacts.\n emitXml(xmlMachine.feed(event.delta));\n continue;\n }\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 // event.type === 'tool_use'\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 append({ role: 'tool_call', content: '', toolCalls: [{ id: event.id, name: event.name, input: event.input }] });\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 const resolve = approvalResolver ?? (async () => ({ approved: false }));\n const decision = await resolve(event.id, signal);\n\n if (!decision.approved) {\n const rejection = 'Tool execution was rejected by the user.';\n emitter({ type: 'tool-rejected', toolCallId: event.id, reason: rejection });\n pushToolCallEnvelope(messages, append, toolCallsThisTurn, precedingText);\n append({ role: 'tool_result', content: rejection, toolCallId: event.id });\n continueLoop = false;\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 emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n } else {\n emitter({ type: 'tool-resolved', toolCallId: event.id, result: outcome.content });\n pushToolCallEnvelope(messages, append, toolCallsThisTurn, precedingText);\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 // artifactRaw close comes right after the parser flush, matching the\n // finalize-block order in _streamLoop (text-flush, then artifact-close).\n if (rawSegId) {\n const inline = rawContent.split('\\n').length < 15;\n emitter({ type: 'artifact-close', id: rawSegId, content: rawContent, inline });\n }\n // artifactXml finalize flushes a truncated (unclosed) artifact —\n // after text-flush, mirroring the finalize-block order in _streamLoop.\n if (xmlMachine) emitXml(xmlMachine.finalize());\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 → final answer, OR advance to the next pipeline\n // phase (mirrors _streamLoop).\n if (toolCallsThisTurn.length === 0) {\n if (pipeline && pipelineIndex < pipeline.length - 1) {\n // Feed this phase's reply into history as context for the next.\n if (precedingText.trim()) {\n append({ role: 'assistant', content: precedingText.trim() });\n }\n pipelineIndex++;\n emitter({ type: 'phase-advance', index: pipelineIndex });\n // continueLoop stays true — the next iteration runs the new phase.\n } else {\n continueLoop = false;\n }\n }\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 * Push the single grouped `tool_call` envelope for the turn — but only once,\n * even when the turn has several tool calls (each call's `tool_result` is pushed\n * separately). Mirrors `_streamLoop`'s `existingToolCallMsg` guard. Reads\n * `messages` for that guard and writes through `append`, so the duplicate\n * suppression also applies to the `onHistoryAppend` notification.\n */\nfunction pushToolCallEnvelope(\n messages: StreamAgentMessage[],\n append: (message: StreamAgentMessage) => void,\n toolCallsThisTurn: StreamToolCall[],\n precedingText: string,\n): void {\n const exists = messages.some(\n m => m.role === 'tool_call' && m.toolCalls?.some(tc => toolCallsThisTurn.some(t => t.id === tc.id)),\n );\n if (exists) return;\n append({\n role: 'tool_call',\n content: '',\n toolCalls: toolCallsThisTurn,\n precedingText: precedingText.trim() || undefined,\n });\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 const timeout = setTimeout(() => controller.abort(), toolTimeoutMs);\n try {\n const result = await handler(call, controller.signal);\n return { status: 'resolved', content: result.content };\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 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"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aparte/engine",
3
- "version": "0.3.0-alpha.0",
3
+ "version": "0.5.0-alpha.0",
4
4
  "description": "Framework-agnostic AI agent loop (runStreamAgent) + parsers/orchestrator — zero runtime deps, headless (browser or Node).",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -24,7 +24,7 @@
24
24
  "node": ">=18"
25
25
  },
26
26
  "peerDependencies": {
27
- "@aparte/core": "0.3.0-alpha.0"
27
+ "@aparte/core": "0.5.0-alpha.0"
28
28
  },
29
29
  "peerDependenciesMeta": {
30
30
  "@aparte/core": {
@@ -36,7 +36,7 @@
36
36
  "typescript": "^5.4.0",
37
37
  "vite": "^6.0.0",
38
38
  "vite-plugin-dts": "^4.5.4",
39
- "@aparte/core": "0.3.0-alpha.0"
39
+ "@aparte/core": "0.5.0-alpha.0"
40
40
  },
41
41
  "keywords": [
42
42
  "ai",