@aparte/engine 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 core's `client/xml-artifact-feed.ts` (the\n * scanning half) plus the `XML artifact finalize` block inside\n * `AparteClient._streamLoop`.\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. Core renders that text through a REDUCED path — its twin's\n // `emitChatText(text, syncActive = false)`: only completed segments are added,\n // the trailing active segment is NOT rendered until a later, tag-free delta.\n // The adapter must honor it or the pre-artifact text streams one update too\n // 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\n/**\n * Length of the longest suffix of `text` that is a PROPER prefix of `<artifact`\n * (0 when there is none). Lets the machine hold `…<arti` back instead of\n * emitting it as chat text and losing the artifact that follows it.\n */\nfunction partialOpenTagLength(text: string): number {\n const max = Math.min(text.length, OPEN_TAG.length - 1);\n for (let k = max; k > 0; k--) {\n if (text.endsWith(OPEN_TAG.slice(0, k))) return k;\n }\n return 0;\n}\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 // No whole tag — but the delta may END on a piece of one. `<`\n // and `artifact` are separate tokens in most vocabularies, so\n // `<arti` closing a delta is routine, not exotic. Emitting it\n // as chat text loses the artifact entirely: the machine never\n // enters `scanning`, and the fallback path that then produces\n // the artifact emits no lifecycle events at all.\n const held = partialOpenTagLength(remaining);\n if (held > 0) {\n const before = remaining.slice(0, remaining.length - held);\n if (before) out.push({ type: 'chat-text', text: before, reduced: true });\n this.scanBuf = remaining.slice(remaining.length - held);\n this.state = 'scanning';\n } else {\n out.push({ type: 'chat-text', text: remaining });\n }\n remaining = '';\n } else {\n const before = remaining.slice(0, tagStart);\n if (before) out.push({ type: 'chat-text', text: before, reduced: true });\n // The tail continues through `remaining`, so a complete\n // `<artifact …>…</artifact>` arriving in ONE delta is parsed in the\n // same call. Parking it in `scanBuf` and clearing `remaining` ended\n // the loop, and `finalize()` then handed the buffer back as raw\n // chat text — so the artifact rendered as a literal tag instead of a\n // card. Core had the same parking bug with a different outcome: it\n // dropped the artifact AND the prose after it entirely.\n this.scanBuf = '';\n remaining = remaining.slice(tagStart);\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 // We may have entered on a partial prefix that turns out to be a\n // different tag (`<article>`). As soon as the buffer contradicts\n // OPEN_TAG, give the text back and return to normal.\n const cmp = Math.min(this.scanBuf.length, OPEN_TAG.length);\n if (this.scanBuf.slice(0, cmp) !== OPEN_TAG.slice(0, cmp)) {\n out.push({ type: 'chat-text', text: this.scanBuf });\n this.scanBuf = '';\n this.state = 'normal';\n continue;\n }\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 the\n * `XML artifact finalize` block in `AparteClient._streamLoop`.\n */\n finalize(): XmlArtifactEvent[] {\n // A stream that ends mid-tag — on a held `<arti`, or on an opening tag\n // that never reached its `>` — must give that text back rather than\n // swallow it. `scanBuf` is the only place it lives.\n if (this.state === 'scanning') {\n const text = this.scanBuf;\n this.scanBuf = '';\n this.state = 'normal';\n return text ? [{ type: 'chat-text', text }] : [];\n }\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 `aparteGlobalConfig.getToolHandler`). */\n toolLookup: (name: string) => StreamToolHandler | undefined;\n /** Resolves a tool's loop config by name (maxTurns / needsApproval). */\n toolConfigLookup?: (name: string) => StreamToolConfig | undefined;\n /** HITL approval resolver for `needsApproval` tools (default: never called). */\n approvalResolver?: StreamApprovalResolver;\n /** Synchronous, ordered event sink consumed by the adapter. */\n emitter: StreamRunEmitter;\n /** Single abort signal composing `_isAborted` + the stream controller. */\n signal: AbortSignal;\n /** Global turn cap. @default 10 */\n maxTurns?: number;\n /** Per-tool-call handler timeout in ms. @default 300000 */\n toolTimeoutMs?: number;\n /**\n * Called for every turn the loop appends to the history — the grouped\n * `tool_call` envelope, each `tool_result` (resolved or rejected), and a\n * pipeline phase's reply — in order, and always before the transport call that\n * would carry it. Never called for the messages you passed in `baseRequest`.\n *\n * For hosts that own their own transcript. The loop re-sends its `messages`\n * array every turn, which suits stateless message APIs but not a **prefix\n * cache** (llama.cpp slots, vLLM), where turn N+1 must EXTEND turn N byte for\n * byte. Such a host builds its own request in {@link transportCall} — ignoring\n * `request.messages` — and mirrors these notifications into its append-only\n * log, instead of reimplementing the loop's tool bookkeeping. Synchronous and\n * ordered, like {@link emitter}.\n */\n onHistoryAppend?: (message: StreamAgentMessage) => void;\n /**\n * Generates artifact segment ids (`prefix` is e.g. `'artifact-raw'`). The\n * default is a deterministic per-run counter; the adapter injects a\n * crypto-based one to match `_streamLoop`'s `artifact-*-<uuid>`. (Tool ids\n * still flow from the stream; only artifacts need generated ids.)\n */\n idGen?: (prefix: string) => string;\n}\n\n/** Warn once per event type for something this version does not map, then skip it. */\nconst warnedUnknownEvents = new Set<string>();\nfunction warnUnknownStreamEvent(event: unknown): void {\n const type = String((event as { type?: unknown })?.type ?? 'undefined');\n if (warnedUnknownEvents.has(type)) return;\n warnedUnknownEvents.add(type);\n console.warn(\n `[runStreamAgent] Ignoring unrecognised stream event \"${type}\". The reply is`\n + ' unaffected; this usually means the provider or its SDK emits a part this'\n + ' version of aparté does not map yet.',\n );\n}\n\n/**\n * Run the structured-stream agent loop. Resolves the last turn's usage (the\n * `done{usage}` last-write-wins, mirroring `_streamLoop`'s return), or\n * `undefined`. Throws on a stream `error` event or a non-abort tool failure —\n * the caller (adapter) routes that to its lifecycle-error handler, exactly as\n * `_handleSend`/`_handleRetry`/`_handleEdit` catch `_streamLoop`.\n *\n * This block has to stay ADJACENT to the function. It used to sit above\n * `warnUnknownStreamEvent`, six lines and a docblock away from what it describes, and a\n * docblock separated from its declaration is attached to nothing: TypeDoc took the\n * helper's comment as the neighbouring one and `@aparte/engine`'s reference page carried\n * the package's headline export with no description at all — the very export its own\n * frontmatter names. Same trap the custom-elements analyser has with a class docblock\n * pushed above an import.\n */\nexport async function runStreamAgent(opts: StreamRunOptions): Promise<StreamUsage | undefined> {\n const {\n transportCall,\n toolLookup,\n toolConfigLookup,\n approvalResolver,\n emitter,\n signal,\n maxTurns = DEFAULT_MAX_TURNS,\n toolTimeoutMs = DEFAULT_TOOL_TIMEOUT_MS,\n } = opts;\n\n // Reassignable: the synthetic-toolChoice bypass strips `toolChoice`/`tools`\n // from it after the forced turn-1 handler runs (mirrors `_streamLoop`'s\n // `baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined }`).\n let baseRequest = opts.baseRequest;\n\n let idSeq = 0;\n const idGen = opts.idGen ?? ((prefix: string) => `${prefix}-${idSeq++}`);\n\n // Mutable history the loop enriches with tool_call/tool_result turns. Every\n // enrichment goes through `append`, so a caller owning its own transcript sees\n // the same turns in the same order (see `onHistoryAppend`).\n const messages: StreamAgentMessage[] = [...baseRequest.messages];\n const append = (message: StreamAgentMessage): void => {\n messages.push(message);\n opts.onHistoryAppend?.(message);\n };\n\n // 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 // Narrowed to the declared shape rather than `input: unknown`: now that\n // `StreamChatRequest.toolChoice` mirrors core's type, the cast can be exact.\n const tc = toolChoice as { name: string; input: Record<string, unknown> };\n const syntheticId = idGen('synthetic-tool');\n emitter({ type: 'tool-start', toolCallId: syntheticId, name: tc.name, input: tc.input });\n\n const handler = toolLookup(tc.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n\n const outcome = await invokeToolHandler(handler, { id: syntheticId, name: tc.name, input: tc.input }, signal, toolTimeoutMs);\n if (outcome.status === 'aborted') {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n emitter({ type: 'tool-resolved', toolCallId: syntheticId, result: outcome.content });\n append({ role: 'tool_call', content: '', toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });\n append({ role: 'tool_result', content: outcome.content, toolCallId: syntheticId });\n baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined };\n }\n\n // ── 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 // A stop that lands before the first event surfaces here as a REJECTION,\n // not as an `error` event — the fetch is aborted while still in flight, so\n // it never reaches the stream the loop reads. Letting it propagate made a\n // deliberate stop indistinguishable from a transport failure for anyone\n // driving this loop directly. (Core's `_streamTurn` has the mirror of this\n // guard; the browser suite is what found the path, after the two\n // event-level guards were already closed.)\n let response: Awaited<ReturnType<typeof transportCall>>;\n try {\n response = await transportCall(request);\n } catch (err: unknown) {\n if (signal.aborted || (err as { name?: string } | undefined)?.name === 'AbortError') {\n // `break`, not `return`: the post-loop `run-done` below is documented\n // as running on EVERY exit path, and five of the six abort paths fall\n // through to it. Returning here skipped it, so a consumer wiring\n // `runStreamAgent` + `createStreamAdapter` directly never got the\n // `completed` status and the bubble stayed flagged streaming — which\n // is verbatim the defect the client-side guard was added to fix.\n emitter({ type: 'run-aborted' });\n continueLoop = false;\n break;\n }\n throw err;\n }\n\n // Non-streaming provider: the string IS the full assistant message. The\n // adapter writes it and completes (no done{usage}); spike scenarios\n // always stream, so we finish the run here.\n //\n // `text-flush` is not optional here. It is the adapter's ONLY caller of\n // `parser.finalize()`, and the parser deliberately withholds an ambiguous\n // tail — a lone backtick, a triple backtick, a `<`, or the safe window\n // inside an unterminated fence. Breaking out before the flush dropped all\n // of it, so a non-streaming reply ending on any of those characters lost\n // them and one consisting only of them rendered nothing at all. Core's\n // inline loop does `parse()` AND `finalize()` on its whole-response\n // branch; this is the twin.\n if (typeof response === 'string') {\n emitter({ type: 'text-delta', delta: response });\n emitter({ type: 'text-flush' });\n break;\n }\n\n // New turn: the adapter resets its per-turn parser / thinking / streaming\n // state here (mirrors `_streamLoop` creating a fresh AparteStreamParser +\n // sets each outer iteration). No DOM effect of its own.\n emitter({ type: 'turn-start' });\n\n // 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 around each read.\n // Checked on BOTH sides of the read, and the second check is the one that\n // matters: the loop spends nearly all of its time parked on `next()`, so an\n // abort arriving while parked — the user pressing Stop while watching text\n // stream — is invisible to a check that only runs before the await. It also\n // covers both shapes a provider takes on abort: an `error` event or a quiet\n // close. Without it the `error` branch below throws and the caller reports a\n // deliberate stop as a failure. Mirrors `_streamLoop`'s `bailOnAbort`.\n const iterator = response[Symbol.asyncIterator]();\n const bailOnAbort = async (): Promise<boolean> => {\n if (!signal.aborted) return false;\n await iterator.return?.(undefined);\n emitter({ type: 'run-aborted' });\n continueLoop = false;\n return true;\n };\n try {\n while (true) {\n if (await bailOnAbort()) break;\n\n const step = await iterator.next();\n if (await bailOnAbort()) break;\n if (step.done) break;\n const event = step.value;\n\n if (event.type === 'thinking') {\n emitter({ type: 'thinking-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'text') {\n precedingText += event.delta;\n 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 // A DISCRIMINANT GUARD, not a comment.\n //\n // This used to be the bare comment `// event.type === 'tool_use'`, so\n // anything the chain above did not recognise fell in here and was\n // treated as a tool call: a `tool-start` / `tool-aborted` pair with an\n // undefined id and name, and — worse — the rest of the stream was\n // never processed, so the reply was truncated too.\n //\n // Core's inline loop had the opposite failure on the same input (it\n // threw and replaced the reply with an error bubble). Both now ignore\n // the event and carry on, which is what forward compatibility with a\n // provider SDK actually requires.\n if (event.type !== 'tool_use') {\n warnUnknownStreamEvent(event);\n continue;\n }\n toolCallsThisTurn.push({ id: event.id, name: event.name, input: event.input });\n\n // Built-in create_artifact: bypass the generic tool path entirely\n // (no tool-start, no approval, no handler) — build the artifact\n // one-shot and inject a success tool_result (mirrors the create_artifact fast-path in _streamLoop).\n if (event.name === 'create_artifact') {\n const input = (event.input ?? {}) as { mimeType?: string; title?: string; content?: string };\n const mimeType = input.mimeType ?? 'text/plain';\n const kind = deriveArtifactKind(mimeType, 'text');\n emitter({ type: 'artifact-ready', id: `artifact-${event.id}`, mimeType, kind, title: input.title ?? kind, content: input.content ?? '' });\n 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 /*\n * Nothing can ask, so nothing may answer.\n *\n * The default was `async () => ({ approved: false })`, which sent the\n * loop into the refusal branch and appended \"rejected by the user\" —\n * a host that forgot to wire a resolver had not refused anything, and\n * the model was told a person had. A misconfiguration is an aborted\n * call, not a decision.\n */\n if (!approvalResolver) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n const decision = await approvalResolver(\n { id: event.id, name: event.name, input: event.input as Record<string, unknown> },\n signal,\n );\n\n /*\n * A stop is not a refusal either. Core's built-in channel resolves\n * `{ approved: false }` on abort, indistinguishable by value from an\n * explicit Reject — so the signal is what tells them apart. No\n * `tool_result`: an aborted call has nothing true to tell the model.\n */\n if (signal.aborted) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n if (!decision.approved) {\n /*\n * The user's own words when they gave them, and the fixed sentence\n * otherwise — byte-identical to core's twin, because this is the\n * only thing the model gets to read about the refusal.\n *\n * This branch used to hardcode the generic sentence. The resolver\n * type declared `instruction` and the whole loop never referenced\n * it, so on the engine path — the recommended one — a user who\n * refused and typed \"use the staging bucket instead\" had those\n * words dropped before the model saw them. Handing the model a\n * turn after a refusal exists SO THAT it reads the refusal; here\n * there was nothing to read.\n *\n * The parity suite could not see it: its resolver returned\n * `{ approved }` and never an instruction, so both loops agreed on\n * a case neither exercised. It supplies one now.\n */\n const rejection = decision.instruction\n ? `The user rejected this tool call and said: ${decision.instruction}`\n : 'Tool execution was rejected by the user.';\n emitter({ type: 'tool-rejected', toolCallId: event.id, reason: rejection });\n pushToolCallEnvelope(messages, append, toolCallsThisTurn, precedingText);\n append({ role: 'tool_result', content: rejection, toolCallId: event.id });\n /*\n * `break` WITHOUT `continueLoop = false`, and the asymmetry is the\n * point. The remaining tool calls of this turn must not run — the\n * model asked for several and refusing one cannot license the\n * others — but the loop takes another turn, so the sentence just\n * appended actually reaches the model. It never did: the turn\n * ended here, and telling the assistant what you wanted instead\n * meant retyping it as a new message it read out of order.\n *\n * Core's twin returns `'respond'` for exactly this, and the parity\n * suite asserts the two agree.\n */\n break;\n }\n if (decision.payload && typeof decision.payload === 'object' && !Array.isArray(decision.payload)) {\n effectiveInput = { ...(event.input as Record<string, unknown>), ...(decision.payload as Record<string, unknown>) };\n }\n emitter({ type: 'tool-approved', toolCallId: event.id });\n }\n\n // ── handler invocation with per-call timeout ──────────────────\n const outcome = await invokeToolHandler(\n handler,\n { id: event.id, name: event.name, input: effectiveInput },\n signal,\n toolTimeoutMs,\n );\n if (outcome.status === 'aborted') {\n // Same rule as the rejection / turn-limit / missing-handler\n // exits above, which this branch was missing: a stopped turn\n // must not go on running the tool calls that follow it.\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n } else {\n emitter({ type: 'tool-resolved', toolCallId: event.id, result: outcome.content });\n pushToolCallEnvelope(messages, append, toolCallsThisTurn, precedingText);\n append({ role: 'tool_result', content: outcome.content, toolCallId: event.id });\n }\n }\n\n // artifactXml finalize comes FIRST, and the order is load-bearing.\n //\n // Its `scanning` branch hands back text the machine was holding — a\n // proper prefix of `<artifact` it could not yet classify. That text\n // travels as `chat-text` -> `text-delta`, and the adapter feeds every\n // `text-delta` into the parser. Emitted AFTER `text-flush` it reached a\n // parser that had already been finalized and never would be again, and\n // since every held value is a prefix of the open tag the parser\n // withholds all of them: the loss was total, not probabilistic.\n //\n // Core's twin sidesteps this by bypassing its parser entirely for held\n // text. This side cannot — the adapter owns the parser — so it flushes\n // in the right order instead.\n if (xmlMachine) emitXml(xmlMachine.finalize());\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 ran above, before the parser flush — its held\n // text has to reach the parser while the parser can still be flushed.)\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\n /*\n * RACED, not just signalled.\n *\n * Aborting the controller is a request the handler is free to ignore, and the\n * default shape of a consumer tool ignores it —\n * `async () => ({ content: await fetch(...).then(r => r.text()) })` never reads\n * its signal. So the timeout fired, nothing rejected, and the loop waited\n * forever on an option whose JSDoc promises a timeout.\n *\n * The signal still fires first, because a handler that DOES honour it should\n * get the chance to clean up and reject on its own terms; the race is what\n * makes the promise true when it does not.\n */\n let timeout: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<{ status: 'aborted' }>((resolve) => {\n timeout = setTimeout(() => {\n controller.abort();\n resolve({ status: 'aborted' });\n }, toolTimeoutMs);\n });\n\n try {\n const result = await Promise.race([\n handler(call, controller.signal).then((r) => ({ status: 'resolved' as const, content: r.content })),\n timedOut,\n ]);\n return result;\n } catch (err: unknown) {\n if ((err as { name?: string })?.name === 'AbortError') return { status: 'aborted' };\n throw err;\n } finally {\n clearTimeout(timeout);\n signal.removeEventListener('abort', onParentAbort);\n }\n}\n","/**\n * conversation/compactor.ts — adaptive budgeting + context-window assembly.\n *\n * Conversation history budgeting and sliding-window assembly —\n * framework-free and tokenizer-free (char-count heuristic).\n *\n * Pattern (cf. Claude Code /context, OpenAI Agents SDK TrimmingSession):\n * Budget = CONTEXT_WINDOW − systemPrompt − tools − reservedThinking\n * − reservedGeneration − autocompactBuffer − safetyMargin\n *\n * The history budget is then split into:\n * - summary (global summary, LLM-generated, ~10%)\n * - ragHist (older turns retrieved by cosine similarity, ~25%)\n * - window (verbatim sliding window, the rest)\n *\n * Drop priority on overflow:\n * 1. summary (async-regenerable)\n * 2. ragHist (retrievable on the next turn)\n * 3. window oldest turns\n * X. NEVER dropped: currentUser + lastAssistant (absolute working memory)\n *\n * Browser-portable: zero deps, just an estimateTokens heuristic.\n */\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface CompactionMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string;\n}\n\nexport interface RetrievedTurn {\n role: 'user' | 'assistant';\n content: string;\n score?: number;\n}\n\nexport interface CompactionConfig {\n /** Total context window of the active model, in tokens. */\n contextWindow: number;\n /** Reserved budget for the model's thinking/reasoning block, in tokens. */\n reservedThinking: number;\n /** Reserved budget for the assistant response (max_new_tokens cap). */\n reservedGeneration: number;\n /** Fraction of context_window kept as autocompact buffer (0..1). */\n autocompactBufferPct: number;\n /** Hard safety margin in tokens. */\n safetyMargin: number;\n /** Floor for history budget — never compact below this. */\n minHistoryBudget: number;\n /** Ratio of history budget allocated to the summary block. */\n summaryRatio: number;\n /** Hard cap for summary tokens. */\n summaryMaxTokens: number;\n /** Ratio of history budget allocated to the RAG-retrieved old turns. */\n ragHistRatio: number;\n /** Hard cap for ragHist tokens. */\n ragHistMaxTokens: number;\n /** Trigger summarization once history usage reaches this % of the budget. */\n triggerSummaryThresholdPct: number;\n /** Re-run summarization every N user turns. */\n summarizeEveryNTurns: number;\n /** Header prepended to the summary block injected into the model context. English\n * by default — override to localise (the compactor ships no locale system). */\n summaryLabel: string;\n /** Header prepended to the RAG-retrieved old turns injected into the context. */\n ragIntroLabel: string;\n}\n\nexport interface BudgetBreakdown {\n contextWindow: number;\n systemPrompt: number;\n tools: number;\n reservedThinking: number;\n reservedGeneration: number;\n autocompactBuffer: number;\n safetyMargin: number;\n historyAvailable: number;\n}\n\nexport interface BudgetResult {\n historyBudget: number;\n breakdown: BudgetBreakdown;\n config: CompactionConfig;\n}\n\nexport interface SplitBudget {\n summary: number;\n ragHist: number;\n window: number;\n}\n\nexport interface UsageBreakdown {\n system: number;\n summary: number;\n ragHist: number;\n window: number;\n}\n\nexport interface DroppedBreakdown {\n ragHits: number;\n oldTurns: number;\n summary: boolean;\n}\n\nexport interface FullBreakdown extends BudgetBreakdown {\n historyAllocated: SplitBudget;\n historyUsed: UsageBreakdown;\n totalUsed: number;\n /** Free tokens left in the context window after all reservations + actual use. */\n free: number;\n dropped: DroppedBreakdown;\n}\n\nexport interface CompactionInput {\n messages: CompactionMessage[];\n systemPrompt: string;\n /** Tools array passed to apply_chat_template (or null/undefined when no tools). */\n toolsArray?: unknown;\n /** Running summary text (regenerable). */\n summary?: string;\n /** RAG-retrieved old turns (older than the sliding window). */\n retrievedTurns?: RetrievedTurn[];\n /** Partial override of the default config. */\n config?: Partial<CompactionConfig>;\n}\n\nexport interface CompactionResult {\n compactedMessages: CompactionMessage[];\n breakdown: FullBreakdown;\n}\n\n// ─── Constants ──────────────────────────────────────────────────────────────\n\nexport const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {\n // Conservative model-agnostic defaults — the consuming app overrides\n // `contextWindow` / `reservedThinking` with its model's real values.\n contextWindow: 8192,\n reservedThinking: 0,\n reservedGeneration: 2000,\n autocompactBufferPct: 0.10, // ~3300 tok\n safetyMargin: 500,\n minHistoryBudget: 1000,\n\n summaryRatio: 0.10,\n summaryMaxTokens: 400,\n ragHistRatio: 0.25,\n ragHistMaxTokens: 1000,\n\n triggerSummaryThresholdPct: 0.75,\n summarizeEveryNTurns: 5,\n\n summaryLabel: 'Conversation summary:',\n ragIntroLabel: 'Relevant excerpts from earlier in the conversation:',\n};\n\n// ─── Token estimation ──────────────────────────────────────────────────────\n\n/**\n * Token heuristic (FR ~3.5 chars/tok, EN ~4 chars/tok).\n * Accurate to ±10% — enough for budgeting, and it avoids `tokenizer.encode()`,\n * which costs ~5ms per message × N (too slow to run on every turn).\n */\nexport function estimateTokens(text: string | null | undefined): number {\n if (!text) return 0;\n return Math.ceil(text.length / 3.8);\n}\n\n/**\n * Estimate tokens for a JSON-serializable structure (e.g. tools array).\n */\nexport function estimateTokensJson(obj: unknown): number {\n if (!obj) return 0;\n try {\n return estimateTokens(JSON.stringify(obj));\n } catch {\n return 0;\n }\n}\n\n// ─── Budget computation ────────────────────────────────────────────────────\n\n/**\n * Compute the available history budget after subtracting fixed costs.\n */\nexport function computeHistoryBudget(input: {\n systemPrompt: string;\n toolsArray?: unknown;\n config?: Partial<CompactionConfig>;\n}): BudgetResult {\n const cfg: CompactionConfig = { ...DEFAULT_COMPACTION_CONFIG, ...(input.config ?? {}) };\n\n const systemTokens = estimateTokens(input.systemPrompt);\n const toolsTokens = estimateTokensJson(input.toolsArray);\n const autocompactBuffer = Math.floor(cfg.contextWindow * cfg.autocompactBufferPct);\n\n const fixed = systemTokens + toolsTokens\n + cfg.reservedThinking + cfg.reservedGeneration\n + autocompactBuffer + cfg.safetyMargin;\n\n const historyBudget = Math.max(cfg.minHistoryBudget, cfg.contextWindow - fixed);\n\n return {\n historyBudget,\n breakdown: {\n contextWindow: cfg.contextWindow,\n systemPrompt: systemTokens,\n tools: toolsTokens,\n reservedThinking: cfg.reservedThinking,\n reservedGeneration: cfg.reservedGeneration,\n autocompactBuffer,\n safetyMargin: cfg.safetyMargin,\n historyAvailable: historyBudget,\n },\n config: cfg,\n };\n}\n\n/**\n * Split history budget into summary / ragHist / window slots.\n */\nexport function splitHistoryBudget(\n historyBudget: number,\n cfg: CompactionConfig = DEFAULT_COMPACTION_CONFIG,\n): SplitBudget {\n const summary = Math.min(cfg.summaryMaxTokens, Math.floor(historyBudget * cfg.summaryRatio));\n const ragHist = Math.min(cfg.ragHistMaxTokens, Math.floor(historyBudget * cfg.ragHistRatio));\n const window = historyBudget - summary - ragHist;\n return { summary, ragHist, window };\n}\n\n// ─── Message assembly ──────────────────────────────────────────────────────\n\ninterface AssembleParams {\n messages: CompactionMessage[];\n summary?: string;\n retrievedTurns?: RetrievedTurn[];\n windowBudget: number;\n summaryBudget: number;\n ragBudget: number;\n systemContent?: string;\n /** Context-injection headers; default to English (DEFAULT_COMPACTION_CONFIG). */\n summaryLabel?: string;\n ragIntroLabel?: string;\n}\n\ninterface AssembleResult {\n compactedMessages: CompactionMessage[];\n used: UsageBreakdown;\n dropped: DroppedBreakdown;\n}\n\n/**\n * Assemble compacted message list given a budget and conversation state.\n *\n * Output order:\n * [system?] → [summary system msg?] → [ragHist system msg?] → window verbatim\n *\n * Always keeps the last 2 non-system messages verbatim (working memory),\n * regardless of windowBudget — these are the floor that can never be dropped.\n */\nexport function assembleCompacted(params: AssembleParams): AssembleResult {\n const {\n messages,\n summary = '',\n retrievedTurns = [],\n windowBudget,\n summaryBudget,\n ragBudget,\n systemContent,\n summaryLabel = DEFAULT_COMPACTION_CONFIG.summaryLabel,\n ragIntroLabel = DEFAULT_COMPACTION_CONFIG.ragIntroLabel,\n } = params;\n\n const out: CompactionMessage[] = [];\n const used: UsageBreakdown = { system: 0, summary: 0, ragHist: 0, window: 0 };\n const dropped: DroppedBreakdown = { ragHits: 0, oldTurns: 0, summary: false };\n\n // System message first\n if (systemContent) {\n out.push({ role: 'system', content: systemContent });\n used.system = estimateTokens(systemContent);\n }\n\n // Summary\n if (summary) {\n const summaryWrapped = `${summaryLabel}\\n${summary}`;\n const summaryToks = estimateTokens(summaryWrapped);\n if (summaryToks <= summaryBudget) {\n out.push({ role: 'system', content: summaryWrapped });\n used.summary = summaryToks;\n } else {\n const ratio = (summaryBudget / summaryToks) * 0.95;\n const truncated = summary.slice(0, Math.max(0, Math.floor(summary.length * ratio)));\n const wrapped = `${summaryLabel}\\n${truncated}…`;\n out.push({ role: 'system', content: wrapped });\n used.summary = estimateTokens(wrapped);\n dropped.summary = true;\n }\n }\n\n // RAG hist — old turns retrieved by cosine similarity, sorted by score desc\n if (retrievedTurns.length > 0) {\n const sorted = [...retrievedTurns].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));\n const lines: string[] = [];\n const intro = `${ragIntroLabel}\\n`;\n let toks = estimateTokens(intro);\n for (const t of sorted) {\n const line = `[${t.role}] ${t.content}`;\n const lt = estimateTokens(line);\n if (toks + lt > ragBudget) {\n dropped.ragHits++;\n continue;\n }\n lines.push(line);\n toks += lt;\n }\n if (lines.length > 0) {\n const content = intro + lines.join('\\n');\n out.push({ role: 'system', content });\n used.ragHist = estimateTokens(content);\n }\n }\n\n // Sliding window verbatim — newest → oldest, until windowBudget exhausted.\n // Always keep the last 2 non-system messages (working memory floor).\n const nonSystem = messages.filter(m => m.role !== 'system');\n if (nonSystem.length === 0) {\n return { compactedMessages: out, used, dropped };\n }\n\n const reversedWindow: CompactionMessage[] = [];\n let windowToks = 0;\n const minKeep = Math.min(2, nonSystem.length);\n\n for (let i = nonSystem.length - 1; i >= 0; i--) {\n const m = nonSystem[i];\n if (!m) continue;\n const t = estimateTokens(m.content);\n const idxFromEnd = nonSystem.length - 1 - i;\n\n if (idxFromEnd < minKeep) {\n // Working memory floor — kept no matter the budget.\n reversedWindow.unshift(m);\n windowToks += t;\n continue;\n }\n\n if (windowToks + t > windowBudget) {\n dropped.oldTurns += i + 1;\n break;\n }\n reversedWindow.unshift(m);\n windowToks += t;\n }\n\n /*\n * A window must never BEGIN on a `tool` result.\n *\n * The walk is newest-to-oldest and keeps the last `minKeep` messages whatever\n * the budget, so when the second-to-last message is a tool result the compacted\n * history opens with `role: 'tool'` — an orphan whose requesting assistant turn\n * was dropped. Every OpenAI-compatible provider rejects that with a 400\n * (\"messages with role 'tool' must be a response to a preceding message with\n * tool_calls\"), so compaction turned a long conversation into an unusable one.\n *\n * Dropping the orphan rather than reaching further back for its assistant turn\n * is deliberate: `CompactionMessage` is `{ role, content }` and cannot represent\n * the pairing at all — no `toolCallId`, no `toolCalls` — so there is nothing to\n * reconstruct WITH. Its content already survives in the summary the window sits\n * under. Carrying the pairing properly means widening the type, which is a\n * design change and not a bug fix.\n */\n while (reversedWindow.length && reversedWindow[0]?.role === 'tool') {\n const orphan = reversedWindow.shift();\n if (orphan) {\n windowToks -= estimateTokens(orphan.content);\n dropped.oldTurns += 1;\n }\n }\n\n out.push(...reversedWindow);\n used.window = windowToks;\n\n return { compactedMessages: out, used, dropped };\n}\n\n// ─── High-level helper ─────────────────────────────────────────────────────\n\n/**\n * Compute budget + assemble in one call. The primary entry point for\n * consumers (a browser request interceptor, mobile, Node tests).\n */\nexport function compactConversation(input: CompactionInput): CompactionResult {\n const { messages, systemPrompt, toolsArray, summary, retrievedTurns, config } = input;\n\n const budget = computeHistoryBudget({ systemPrompt, toolsArray, config });\n const split = splitHistoryBudget(budget.historyBudget, budget.config);\n\n const { compactedMessages, used, dropped } = assembleCompacted({\n messages,\n summary,\n retrievedTurns,\n windowBudget: split.window,\n summaryBudget: split.summary,\n ragBudget: split.ragHist,\n systemContent: systemPrompt,\n summaryLabel: budget.config.summaryLabel,\n ragIntroLabel: budget.config.ragIntroLabel,\n });\n\n const totalUsed = budget.breakdown.systemPrompt\n + budget.breakdown.tools\n + used.summary\n + used.ragHist\n + used.window;\n\n const free = budget.config.contextWindow - totalUsed\n - budget.config.reservedThinking - budget.config.reservedGeneration\n - budget.breakdown.autocompactBuffer - budget.config.safetyMargin;\n\n const breakdown: FullBreakdown = {\n ...budget.breakdown,\n historyAllocated: split,\n historyUsed: used,\n totalUsed,\n free,\n dropped,\n };\n\n return { compactedMessages, breakdown };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/agent/parsers/artifact-kind.ts","../src/agent/stream-run.ts","../src/conversation/compactor.ts","../src/conversation/selector.ts"],"sourcesContent":["/**\n * Map an artifact MIME type to a renderer kind — `'react'`, `'html'`, `'js'`, `'css'`,\n * `'svg'`, `'json'`, `'markdown'`, `'csv'`, `'text'` — or `fallback` (default\n * `'unknown'`) for anything else. Anthropic's `application/vnd.ant.*` namespace maps to\n * its suffix; exact standard MIMEs are matched first, then a substring rescue for\n * parameterised or vendor variants (`text/html; charset=utf-8`, `application/ld+json`).\n *\n * THE implementation: `@aparte/core` re-exports it under the same name. It used to\n * live in the XML artifact state machine, which the 2026-08-28 audit's D2 removed —\n * the core parser reads `<artifact>` tags natively — and this is the one piece of\n * that file the built-in `create_artifact` and the artifact card still need.\n *\n * @example\n * deriveArtifactKind('application/vnd.ant.react') // 'react'\n * deriveArtifactKind('text/html; charset=utf-8') // 'html'\n * deriveArtifactKind('font/woff2', 'text') // 'text' (fallback)\n */\nexport function deriveArtifactKind(mimeType: string, fallback = 'unknown'): string {\n const m = (mimeType || '').toLowerCase().trim();\n const ant = m.match(/^application\\/vnd\\.ant\\.([a-z0-9-]+)/);\n if (ant) return ant[1]!;\n if (m === 'text/html' || m === 'application/xhtml+xml') return 'html';\n if (m === 'application/javascript' || m === 'text/javascript') return 'js';\n if (m === 'text/css') return 'css';\n if (m === 'image/svg+xml') return 'svg';\n if (m === 'application/json') return 'json';\n if (m === 'text/markdown') return 'markdown';\n if (m === 'text/csv') return 'csv';\n if (m === 'text/plain') return 'text';\n if (m.includes('react')) return 'react';\n if (m.includes('html')) return 'html';\n if (m.includes('javascript')) return 'js';\n if (m.includes('css')) return 'css';\n if (m.includes('svg')) return 'svg';\n if (m.includes('json')) return 'json';\n if (m.includes('csv')) return 'csv';\n if (m.includes('markdown')) return 'markdown';\n return fallback;\n}\n","/**\n * stream-run.ts — framework-free structured-stream agent loop.\n *\n * The agent loop — extracted from core's inline `_streamLoop`, which it has since\n * replaced (audit 2026-08-28, D1): a `while(tool_use)` loop\n * that consumes a structured `AsyncIterable<StreamChatEvent>` from a transport,\n * runs approved tools, feeds their results back into the history, and re-calls\n * the transport until the model stops asking for tools. It performs **no DOM\n * work** — it emits {@link StreamRunEvent}s (in the inline loop's exact order) that\n * an adapter in `@aparte/core` turns into `targetElement.*` calls.\n *\n * Formerly the parity target of core's inline loop; now the one loop, its call\n * sequences pinned by core's `stream-parity` snapshots.\n * Scope: text · thinking · tool_use (+ HITL) · done · error · the built-in\n * create_artifact · synthetic toolChoice bypass. Code-fence promotion stays\n * adapter-side (it needs the core parser), and so do `<artifact>` tags in the\n * text: the core parser reads them natively. The raw / XML artifact modes and the\n * multi-phase pipeline were removed (audit 2026-08-28, D2) — orchestration is the\n * product's, and a mode nothing in this repository emitted was a contract\n * maintained for nobody.\n */\n\nimport type {\n StreamRunEmitter,\n StreamChatEvent,\n StreamChatRequest,\n StreamAgentMessage,\n StreamToolCall,\n StreamToolHandler,\n StreamToolConfig,\n StreamApprovalResolver,\n StreamUsage,\n} from './stream-events.js';\nimport { deriveArtifactKind } from './parsers/artifact-kind.js';\n\n/** Default per-tool-call handler timeout — mirrors `TOOL_HANDLER_TIMEOUT_MS`. */\nconst DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1000;\n/** Default global turn cap — mirrors `AparteClientOptions.maxTurns ?? 10`. */\nconst DEFAULT_MAX_TURNS = 10;\n\nexport interface StreamRunOptions {\n /** Id of the assistant message being streamed (opaque; carried in events). */\n messageId: string;\n /** Turn-1 request; the loop clones its `messages` and enriches them per turn. */\n baseRequest: StreamChatRequest;\n /**\n * Calls the transport with the (possibly enriched) request. Returns the\n * structured stream, or a plain string for a non-streaming provider. Mirrors\n * `getTransport().chat(provider, request, auth, ctx)` with provider/auth/ctx\n * closed over by the adapter.\n */\n transportCall: (request: StreamChatRequest) => Promise<AsyncIterable<StreamChatEvent> | string>;\n /** Resolves a tool's handler by name (mirrors `aparteGlobalConfig.getToolHandler`). */\n toolLookup: (name: string) => StreamToolHandler | undefined;\n /** Resolves a tool's loop config by name (maxTurns / needsApproval). */\n toolConfigLookup?: (name: string) => StreamToolConfig | undefined;\n /** HITL approval resolver for `needsApproval` tools (default: never called). */\n approvalResolver?: StreamApprovalResolver;\n /** Synchronous, ordered event sink consumed by the adapter. */\n emitter: StreamRunEmitter;\n /** Single abort signal composing `_isAborted` + the stream controller. */\n signal: AbortSignal;\n /** Global turn cap. @default 10 */\n maxTurns?: number;\n /** Per-tool-call handler timeout in ms. @default 300000 */\n toolTimeoutMs?: number;\n /**\n * Called for every turn the loop appends to the history — the grouped\n * `tool_call` envelope, each `tool_result` (resolved or rejected), and a\n * pipeline phase's reply — in order, and always before the transport call that\n * would carry it. Never called for the messages you passed in `baseRequest`.\n *\n * For hosts that own their own transcript. The loop re-sends its `messages`\n * array every turn, which suits stateless message APIs but not a **prefix\n * cache** (llama.cpp slots, vLLM), where turn N+1 must EXTEND turn N byte for\n * byte. Such a host builds its own request in {@link transportCall} — ignoring\n * `request.messages` — and mirrors these notifications into its append-only\n * log, instead of reimplementing the loop's tool bookkeeping. Synchronous and\n * ordered, like {@link emitter}.\n *\n * One rule about the envelope: a turn's `tool_call` message is reported ONCE, when\n * its first call completes, and the calls that complete later in the same turn are\n * already in that same object's `toolCalls` — the array is shared by reference, not\n * copied. Hold the reference, not a snapshot, and every result you receive after it\n * is declared by it.\n */\n onHistoryAppend?: (message: StreamAgentMessage) => void;\n /**\n * Generates artifact segment ids (`prefix` is e.g. `'artifact-raw'`). The\n * default is a deterministic per-run counter; the adapter injects a\n * crypto-based one to match `_streamLoop`'s `artifact-*-<uuid>`. (Tool ids\n * still flow from the stream; only artifacts need generated ids.)\n */\n idGen?: (prefix: string) => string;\n}\n\n/** Warn once per event type for something this version does not map, then skip it. */\nconst warnedUnknownEvents = new Set<string>();\nfunction warnUnknownStreamEvent(event: unknown): void {\n const type = String((event as { type?: unknown })?.type ?? 'undefined');\n if (warnedUnknownEvents.has(type)) return;\n warnedUnknownEvents.add(type);\n console.warn(\n `[runStreamAgent] Ignoring unrecognised stream event \"${type}\". The reply is`\n + ' unaffected; this usually means the provider or its SDK emits a part this'\n + ' version of aparté does not map yet.',\n );\n}\n\n/**\n * Run the structured-stream agent loop. Resolves the last turn's usage (the\n * `done{usage}` last-write-wins, mirroring `_streamLoop`'s return), or\n * `undefined`. Throws on a stream `error` event or a non-abort tool failure —\n * the caller (adapter) routes that to its lifecycle-error handler, exactly as\n * `_handleSend`/`_handleRetry`/`_handleEdit` catch `_streamLoop`.\n *\n * This block has to stay ADJACENT to the function. It used to sit above\n * `warnUnknownStreamEvent`, six lines and a docblock away from what it describes, and a\n * docblock separated from its declaration is attached to nothing: TypeDoc took the\n * helper's comment as the neighbouring one and `@aparte/engine`'s reference page carried\n * the package's headline export with no description at all — the very export its own\n * frontmatter names. Same trap the custom-elements analyser has with a class docblock\n * pushed above an import.\n */\nexport async function runStreamAgent(opts: StreamRunOptions): Promise<StreamUsage | undefined> {\n const {\n transportCall,\n toolLookup,\n toolConfigLookup,\n approvalResolver,\n emitter,\n signal,\n maxTurns = DEFAULT_MAX_TURNS,\n toolTimeoutMs = DEFAULT_TOOL_TIMEOUT_MS,\n } = opts;\n\n // Reassignable: the synthetic-toolChoice bypass strips `toolChoice`/`tools`\n // from it after the forced turn-1 handler runs (mirrors `_streamLoop`'s\n // `baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined }`).\n let baseRequest = opts.baseRequest;\n\n let idSeq = 0;\n const idGen = opts.idGen ?? ((prefix: string) => `${prefix}-${idSeq++}`);\n\n // Mutable history the loop enriches with tool_call/tool_result turns. Every\n // enrichment goes through `append`, so a caller owning its own transcript sees\n // the same turns in the same order (see `onHistoryAppend`).\n const messages: StreamAgentMessage[] = [...baseRequest.messages];\n const append = (message: StreamAgentMessage): void => {\n messages.push(message);\n opts.onHistoryAppend?.(message);\n };\n\n let continueLoop = true;\n let turns = 0;\n let lastUsage: StreamUsage | undefined;\n\n // Leading write before turn 1: mark the message streaming.\n emitter({ type: 'run-start' });\n\n // ── outer (turn) loop — one iteration = one transport call ────────────────\n while (continueLoop) {\n // Abort at the top of a turn: no stream open yet, so no cancel needed.\n if (signal.aborted) {\n emitter({ type: 'run-aborted' });\n break;\n }\n\n turns++;\n if (turns > maxTurns) {\n emitter({ type: 'turn-limit-exceeded', scope: 'global', limit: maxTurns });\n break;\n }\n\n // ── Synthetic toolChoice bypass (mirrors _streamLoop) ──────\n // toolChoice = { name, input } (orchestrator-forced): skip the LLM for\n // turn 1, run the handler directly, inject its result as a tool_result,\n // then strip toolChoice/tools and fall through to the transport call in\n // the SAME turn so the model answers with the tool result already in\n // history. (The adapter's tool-start handler injects renderer CSS here\n // too — a benign, idempotent gain over _streamLoop's synthetic path,\n // which skipped it; reconciled in the parity test.)\n const toolChoice = baseRequest['toolChoice'];\n if (\n turns === 1 &&\n toolChoice &&\n typeof toolChoice === 'object' &&\n !Array.isArray(toolChoice) &&\n (toolChoice as { input?: unknown }).input !== undefined\n ) {\n // Narrowed to the declared shape rather than `input: unknown`: now that\n // `StreamChatRequest.toolChoice` mirrors core's type, the cast can be exact.\n const tc = toolChoice as { name: string; input: Record<string, unknown> };\n const syntheticId = idGen('synthetic-tool');\n emitter({ type: 'tool-start', toolCallId: syntheticId, name: tc.name, input: tc.input });\n\n const handler = toolLookup(tc.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n\n const outcome = await invokeToolHandler(handler, { id: syntheticId, name: tc.name, input: tc.input }, signal, toolTimeoutMs);\n if (outcome.status === 'aborted') {\n emitter({ type: 'tool-aborted', toolCallId: syntheticId });\n continueLoop = false;\n continue;\n }\n emitter({ type: 'tool-resolved', toolCallId: syntheticId, result: outcome.content });\n append({ role: 'tool_call', content: '', toolCalls: [{ id: syntheticId, name: tc.name, input: tc.input }] });\n append({ role: 'tool_result', content: outcome.content, toolCallId: syntheticId });\n baseRequest = { ...baseRequest, toolChoice: 'none', tools: undefined };\n }\n\n // A stop that lands before the first event surfaces here as a REJECTION,\n // not as an `error` event — the fetch is aborted while still in flight, so\n // it never reaches the stream the loop reads. Letting it propagate made a\n // deliberate stop indistinguishable from a transport failure for anyone\n // driving this loop directly. (Core's `_streamTurn` has the mirror of this\n // guard; the browser suite is what found the path, after the two\n // event-level guards were already closed.)\n const request: StreamChatRequest = { ...baseRequest, messages };\n let response: Awaited<ReturnType<typeof transportCall>>;\n try {\n response = await transportCall(request);\n } catch (err: unknown) {\n if (signal.aborted || (err as { name?: string } | undefined)?.name === 'AbortError') {\n // `break`, not `return`: the post-loop `run-done` below is documented\n // as running on EVERY exit path, and five of the six abort paths fall\n // through to it. Returning here skipped it, so a consumer wiring\n // `runStreamAgent` + `createStreamAdapter` directly never got the\n // `completed` status and the bubble stayed flagged streaming — which\n // is verbatim the defect the client-side guard was added to fix.\n emitter({ type: 'run-aborted' });\n continueLoop = false;\n break;\n }\n throw err;\n }\n\n // Non-streaming provider: the string IS the full assistant message. The\n // adapter writes it and completes (no done{usage}); spike scenarios\n // always stream, so we finish the run here.\n //\n // `text-flush` is not optional here. It is the adapter's ONLY caller of\n // `parser.finalize()`, and the parser deliberately withholds an ambiguous\n // tail — a lone backtick, a triple backtick, a `<`, or the safe window\n // inside an unterminated fence. Breaking out before the flush dropped all\n // of it, so a non-streaming reply ending on any of those characters lost\n // them and one consisting only of them rendered nothing at all. Core's\n // inline loop does `parse()` AND `finalize()` on its whole-response\n // branch; this is the twin.\n if (typeof response === 'string') {\n emitter({ type: 'text-delta', delta: response });\n emitter({ type: 'text-flush' });\n break;\n }\n\n // New turn: the adapter resets its per-turn parser / thinking / streaming\n // state here (mirrors `_streamLoop` creating a fresh AparteStreamParser +\n // sets each outer iteration). No DOM effect of its own.\n emitter({ type: 'turn-start' });\n\n // Per-turn streaming state.\n let precedingText = '';\n const toolCallsThisTurn: StreamToolCall[] = [];\n /**\n * The turn's ONE `tool_call` envelope, held by reference. Created — and\n * appended, so the host is notified once — the first time a call needs it;\n * every later call of the turn is already in the SAME `toolCalls` array, so a\n * `tool_result` appended afterwards is always declared by it.\n *\n * This replaces an id scan over `messages` that guessed whether the envelope\n * was already there. The `create_artifact` fast path pushed a fresh envelope\n * of its own; the scan then found the artifact's id in it, concluded \"already\n * pushed\", and the next tool's result went out with no call declaring it —\n * the P0 of the 2026-08-28 audit, a history an Anthropic-shaped API rejects\n * outright. A reference cannot be guessed wrong.\n */\n let envelope: StreamAgentMessage | null = null;\n const ensureEnvelope = (): void => {\n if (envelope) return;\n envelope = { role: 'tool_call', content: '', toolCalls: toolCallsThisTurn, precedingText: precedingText.trim() || undefined };\n append(envelope);\n };\n\n // ── inner (SSE) loop — manual iteration so we can abort around each read.\n // Checked on BOTH sides of the read, and the second check is the one that\n // matters: the loop spends nearly all of its time parked on `next()`, so an\n // abort arriving while parked — the user pressing Stop while watching text\n // stream — is invisible to a check that only runs before the await. It also\n // covers both shapes a provider takes on abort: an `error` event or a quiet\n // close. Without it the `error` branch below throws and the caller reports a\n // deliberate stop as a failure. Mirrors `_streamLoop`'s `bailOnAbort`.\n const iterator = response[Symbol.asyncIterator]();\n const bailOnAbort = async (): Promise<boolean> => {\n if (!signal.aborted) return false;\n await iterator.return?.(undefined);\n emitter({ type: 'run-aborted' });\n continueLoop = false;\n return true;\n };\n try {\n while (true) {\n if (await bailOnAbort()) break;\n\n const step = await iterator.next();\n if (await bailOnAbort()) break;\n if (step.done) break;\n const event = step.value;\n\n if (event.type === 'thinking') {\n emitter({ type: 'thinking-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'text') {\n precedingText += event.delta;\n emitter({ type: 'text-delta', delta: event.delta });\n continue;\n }\n\n if (event.type === 'done') {\n if (event.usage) lastUsage = event.usage;\n continue;\n }\n\n if (event.type === 'error') {\n // Mirror `_streamLoop`: throw; the caller's catch handles it.\n throw new Error(event.message);\n }\n\n // A DISCRIMINANT GUARD, not a comment.\n //\n // This used to be the bare comment `// event.type === 'tool_use'`, so\n // anything the chain above did not recognise fell in here and was\n // treated as a tool call: a `tool-start` / `tool-aborted` pair with an\n // undefined id and name, and — worse — the rest of the stream was\n // never processed, so the reply was truncated too.\n //\n // Core's inline loop had the opposite failure on the same input (it\n // threw and replaced the reply with an error bubble). Both now ignore\n // the event and carry on, which is what forward compatibility with a\n // provider SDK actually requires.\n if (event.type !== 'tool_use') {\n warnUnknownStreamEvent(event);\n continue;\n }\n toolCallsThisTurn.push({ id: event.id, name: event.name, input: event.input });\n\n // Built-in create_artifact: bypass the generic tool path entirely\n // (no tool-start, no approval, no handler) — build the artifact\n // one-shot and inject a success tool_result (mirrors the create_artifact fast-path in _streamLoop).\n if (event.name === 'create_artifact') {\n const input = (event.input ?? {}) as { mimeType?: string; title?: string; content?: string };\n const mimeType = input.mimeType ?? 'text/plain';\n const kind = deriveArtifactKind(mimeType, 'text');\n emitter({ type: 'artifact-ready', id: `artifact-${event.id}`, mimeType, kind, title: input.title ?? kind, content: input.content ?? '' });\n // The turn's envelope, not one of its own: a fresh `[create_artifact]`\n // envelope here is what orphaned the next tool's result.\n ensureEnvelope();\n append({ role: 'tool_result', content: 'Artifact created successfully.', toolCallId: event.id });\n continue;\n }\n\n emitter({ type: 'tool-start', toolCallId: event.id, name: event.name, input: event.input });\n\n const cfg = toolConfigLookup?.(event.name);\n\n // Per-tool maxTurns (note: `>=`, stricter than the global `>`).\n const effectiveMaxTurns = cfg?.maxTurns ?? maxTurns;\n if (turns >= effectiveMaxTurns) {\n emitter({ type: 'turn-limit-exceeded', scope: 'tool', limit: effectiveMaxTurns, toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n const handler = toolLookup(event.name);\n if (!handler) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n let effectiveInput = event.input;\n\n // ── HITL approval gate ────────────────────────────────────────\n if (cfg?.needsApproval) {\n emitter({ type: 'tool-awaiting-approval', toolCallId: event.id, name: event.name, input: event.input });\n /*\n * Nothing can ask, so nothing may answer.\n *\n * The default was `async () => ({ approved: false })`, which sent the\n * loop into the refusal branch and appended \"rejected by the user\" —\n * a host that forgot to wire a resolver had not refused anything, and\n * the model was told a person had. A misconfiguration is an aborted\n * call, not a decision.\n */\n if (!approvalResolver) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n /*\n * A stop is not a refusal either. Core's built-in channel resolves\n * `{ approved: false }` on abort, indistinguishable by value from an\n * explicit Reject — so the signal is what tells them apart. No\n * `tool_result`: an aborted call has nothing true to tell the model.\n *\n * Checked on three sides of the wait, because the stop can land on\n * any of them: while the gate was being shown (a host that reacts to\n * `awaiting-approval` synchronously — the panel's own Stop button),\n * inside the resolver (a channel that rejects with an AbortError\n * rather than resolving), or after it resolved. The first and second\n * used to fall through to the run-level abort, which never marked the\n * call: the segment stayed `awaiting-approval` for good.\n */\n let decision: Awaited<ReturnType<typeof approvalResolver>> | undefined;\n if (!signal.aborted) {\n try {\n decision = await approvalResolver(\n { id: event.id, name: event.name, input: event.input as Record<string, unknown> },\n signal,\n );\n } catch (err) {\n if (!signal.aborted && (err as { name?: string } | undefined)?.name !== 'AbortError') throw err;\n }\n }\n if (signal.aborted || !decision) {\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n }\n\n if (!decision.approved) {\n /*\n * The user's own words when they gave them, and the fixed sentence\n * otherwise — byte-identical to core's twin, because this is the\n * only thing the model gets to read about the refusal.\n *\n * This branch used to hardcode the generic sentence. The resolver\n * type declared `instruction` and the whole loop never referenced\n * it, so on the engine path — the recommended one — a user who\n * refused and typed \"use the staging bucket instead\" had those\n * words dropped before the model saw them. Handing the model a\n * turn after a refusal exists SO THAT it reads the refusal; here\n * there was nothing to read.\n *\n * The parity suite could not see it: its resolver returned\n * `{ approved }` and never an instruction, so both loops agreed on\n * a case neither exercised. It supplies one now.\n */\n const rejection = decision.instruction\n ? `The user rejected this tool call and said: ${decision.instruction}`\n : 'Tool execution was rejected by the user.';\n emitter({ type: 'tool-rejected', toolCallId: event.id, reason: rejection });\n ensureEnvelope();\n append({ role: 'tool_result', content: rejection, toolCallId: event.id });\n /*\n * `break` WITHOUT `continueLoop = false`, and the asymmetry is the\n * point. The remaining tool calls of this turn must not run — the\n * model asked for several and refusing one cannot license the\n * others — but the loop takes another turn, so the sentence just\n * appended actually reaches the model. It never did: the turn\n * ended here, and telling the assistant what you wanted instead\n * meant retyping it as a new message it read out of order.\n *\n * Core's twin returns `'respond'` for exactly this, and the parity\n * suite asserts the two agree.\n */\n break;\n }\n if (decision.payload && typeof decision.payload === 'object' && !Array.isArray(decision.payload)) {\n effectiveInput = { ...(event.input as Record<string, unknown>), ...(decision.payload as Record<string, unknown>) };\n }\n emitter({ type: 'tool-approved', toolCallId: event.id });\n }\n\n // ── handler invocation with per-call timeout ──────────────────\n const outcome = await invokeToolHandler(\n handler,\n { id: event.id, name: event.name, input: effectiveInput },\n signal,\n toolTimeoutMs,\n );\n if (outcome.status === 'aborted') {\n // Same rule as the rejection / turn-limit / missing-handler\n // exits above, which this branch was missing: a stopped turn\n // must not go on running the tool calls that follow it.\n emitter({ type: 'tool-aborted', toolCallId: event.id });\n continueLoop = false;\n break;\n } else {\n emitter({ type: 'tool-resolved', toolCallId: event.id, result: outcome.content });\n ensureEnvelope();\n append({ role: 'tool_result', content: outcome.content, toolCallId: event.id });\n }\n }\n\n // Turn boundary: finalize the parser (flush residual text). Mirrors\n // `_streamLoop`'s `textParser.finalize()` call — runs on normal\n // end AND abort-break, but NOT after a thrown `error` (which escapes\n // this try before reaching here, exactly like `_streamLoop`).\n emitter({ type: 'text-flush' });\n\n } finally {\n // Mirror `reader.releaseLock()` in the finally: settle the iterator.\n await iterator.return?.(undefined).catch(() => { /* best effort */ });\n }\n\n // No tool calls this turn → the final answer.\n if (toolCallsThisTurn.length === 0) continueLoop = false;\n }\n\n // Post-loop finalization runs on every exit path (normal / abort / maxTurns).\n emitter({ type: 'run-done', usage: lastUsage });\n return lastUsage;\n}\n\n/**\n * Run a tool handler with a per-call timeout, linked to the run's abort signal.\n * Resolves `{ status: 'resolved', content }` on success, or `{ status: 'aborted' }`\n * if the handler aborts (timeout OR parent abort); any other error is re-thrown so\n * the caller routes it to lifecycle-error, exactly as `_streamLoop` does. Shared\n * by the generic tool path and the synthetic-toolChoice bypass (mirrors the\n * AbortController + `TOOL_HANDLER_TIMEOUT_MS` dance both use in `_streamLoop`).\n */\nasync function invokeToolHandler(\n handler: StreamToolHandler,\n call: StreamToolCall,\n signal: AbortSignal,\n toolTimeoutMs: number,\n): Promise<{ status: 'resolved'; content: string } | { status: 'aborted' }> {\n // If the run was already aborted before we got here, don't invoke the\n // handler at all: a past 'abort' event will never re-fire on the listener\n // below, so the handler would otherwise run to completion despite cancel.\n if (signal.aborted) return { status: 'aborted' };\n\n const controller = new AbortController();\n const onParentAbort = () => controller.abort();\n signal.addEventListener('abort', onParentAbort, { once: true });\n\n /*\n * RACED, not just signalled.\n *\n * Aborting the controller is a request the handler is free to ignore, and the\n * default shape of a consumer tool ignores it —\n * `async () => ({ content: await fetch(...).then(r => r.text()) })` never reads\n * its signal. So the timeout fired, nothing rejected, and the loop waited\n * forever on an option whose JSDoc promises a timeout.\n *\n * The signal still fires first, because a handler that DOES honour it should\n * get the chance to clean up and reject on its own terms; the race is what\n * makes the promise true when it does not.\n */\n let timeout: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<{ status: 'aborted' }>((resolve) => {\n timeout = setTimeout(() => {\n controller.abort();\n resolve({ status: 'aborted' });\n }, toolTimeoutMs);\n });\n\n try {\n const result = await Promise.race([\n handler(call, controller.signal).then((r) => ({ status: 'resolved' as const, content: r.content })),\n timedOut,\n ]);\n return result;\n } catch (err: unknown) {\n if ((err as { name?: string })?.name === 'AbortError') return { status: 'aborted' };\n throw err;\n } finally {\n clearTimeout(timeout);\n signal.removeEventListener('abort', onParentAbort);\n }\n}\n","/**\n * conversation/compactor.ts — adaptive budgeting + context-window assembly.\n *\n * Conversation history budgeting and sliding-window assembly —\n * framework-free and tokenizer-free (char-count heuristic).\n *\n * Pattern (cf. Claude Code /context, OpenAI Agents SDK TrimmingSession):\n * Budget = CONTEXT_WINDOW − systemPrompt − tools − reservedThinking\n * − reservedGeneration − autocompactBuffer − safetyMargin\n *\n * The history budget is then split into:\n * - summary (global summary, LLM-generated, ~10%)\n * - ragHist (older turns retrieved by cosine similarity, ~25%)\n * - window (verbatim sliding window, the rest)\n *\n * Drop priority on overflow:\n * 1. summary (async-regenerable)\n * 2. ragHist (retrievable on the next turn)\n * 3. window oldest turns\n * X. NEVER dropped: currentUser + lastAssistant (absolute working memory)\n *\n * Browser-portable: zero deps, just an estimateTokens heuristic.\n */\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface CompactionMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string;\n}\n\nexport interface RetrievedTurn {\n role: 'user' | 'assistant';\n content: string;\n score?: number;\n}\n\nexport interface CompactionConfig {\n /** Total context window of the active model, in tokens. */\n contextWindow: number;\n /** Reserved budget for the model's thinking/reasoning block, in tokens. */\n reservedThinking: number;\n /** Reserved budget for the assistant response (max_new_tokens cap). */\n reservedGeneration: number;\n /** Fraction of context_window kept as autocompact buffer (0..1). */\n autocompactBufferPct: number;\n /** Hard safety margin in tokens. */\n safetyMargin: number;\n /** Floor for history budget — never compact below this. */\n minHistoryBudget: number;\n /** Ratio of history budget allocated to the summary block. */\n summaryRatio: number;\n /** Hard cap for summary tokens. */\n summaryMaxTokens: number;\n /** Ratio of history budget allocated to the RAG-retrieved old turns. */\n ragHistRatio: number;\n /** Hard cap for ragHist tokens. */\n ragHistMaxTokens: number;\n /** Trigger summarization once history usage reaches this % of the budget. */\n triggerSummaryThresholdPct: number;\n /** Re-run summarization every N user turns. */\n summarizeEveryNTurns: number;\n /** Header prepended to the summary block injected into the model context. English\n * by default — override to localise (the compactor ships no locale system). */\n summaryLabel: string;\n /** Header prepended to the RAG-retrieved old turns injected into the context. */\n ragIntroLabel: string;\n}\n\nexport interface BudgetBreakdown {\n contextWindow: number;\n systemPrompt: number;\n tools: number;\n reservedThinking: number;\n reservedGeneration: number;\n autocompactBuffer: number;\n safetyMargin: number;\n historyAvailable: number;\n}\n\nexport interface BudgetResult {\n historyBudget: number;\n breakdown: BudgetBreakdown;\n config: CompactionConfig;\n}\n\nexport interface SplitBudget {\n summary: number;\n ragHist: number;\n window: number;\n}\n\nexport interface UsageBreakdown {\n system: number;\n summary: number;\n ragHist: number;\n window: number;\n}\n\nexport interface DroppedBreakdown {\n ragHits: number;\n oldTurns: number;\n summary: boolean;\n}\n\nexport interface FullBreakdown extends BudgetBreakdown {\n historyAllocated: SplitBudget;\n historyUsed: UsageBreakdown;\n totalUsed: number;\n /** Free tokens left in the context window after all reservations + actual use. */\n free: number;\n dropped: DroppedBreakdown;\n}\n\nexport interface CompactionInput {\n messages: CompactionMessage[];\n systemPrompt: string;\n /** Tools array passed to apply_chat_template (or null/undefined when no tools). */\n toolsArray?: unknown;\n /** Running summary text (regenerable). */\n summary?: string;\n /** RAG-retrieved old turns (older than the sliding window). */\n retrievedTurns?: RetrievedTurn[];\n /** Partial override of the default config. */\n config?: Partial<CompactionConfig>;\n}\n\nexport interface CompactionResult {\n compactedMessages: CompactionMessage[];\n breakdown: FullBreakdown;\n}\n\n// ─── Constants ──────────────────────────────────────────────────────────────\n\nexport const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {\n // Conservative model-agnostic defaults — the consuming app overrides\n // `contextWindow` / `reservedThinking` with its model's real values.\n contextWindow: 8192,\n reservedThinking: 0,\n reservedGeneration: 2000,\n autocompactBufferPct: 0.10, // ~3300 tok\n safetyMargin: 500,\n minHistoryBudget: 1000,\n\n summaryRatio: 0.10,\n summaryMaxTokens: 400,\n ragHistRatio: 0.25,\n ragHistMaxTokens: 1000,\n\n triggerSummaryThresholdPct: 0.75,\n summarizeEveryNTurns: 5,\n\n summaryLabel: 'Conversation summary:',\n ragIntroLabel: 'Relevant excerpts from earlier in the conversation:',\n};\n\n// ─── Token estimation ──────────────────────────────────────────────────────\n\n/**\n * Token heuristic (FR ~3.5 chars/tok, EN ~4 chars/tok).\n * Accurate to ±10% — enough for budgeting, and it avoids `tokenizer.encode()`,\n * which costs ~5ms per message × N (too slow to run on every turn).\n */\nexport function estimateTokens(text: string | null | undefined): number {\n if (!text) return 0;\n return Math.ceil(text.length / 3.8);\n}\n\n/**\n * Estimate tokens for a JSON-serializable structure (e.g. tools array).\n */\nexport function estimateTokensJson(obj: unknown): number {\n if (!obj) return 0;\n try {\n return estimateTokens(JSON.stringify(obj));\n } catch {\n return 0;\n }\n}\n\n// ─── Budget computation ────────────────────────────────────────────────────\n\n/**\n * Compute the available history budget after subtracting fixed costs.\n */\nexport function computeHistoryBudget(input: {\n systemPrompt: string;\n toolsArray?: unknown;\n config?: Partial<CompactionConfig>;\n}): BudgetResult {\n const cfg: CompactionConfig = { ...DEFAULT_COMPACTION_CONFIG, ...(input.config ?? {}) };\n\n const systemTokens = estimateTokens(input.systemPrompt);\n const toolsTokens = estimateTokensJson(input.toolsArray);\n const autocompactBuffer = Math.floor(cfg.contextWindow * cfg.autocompactBufferPct);\n\n const fixed = systemTokens + toolsTokens\n + cfg.reservedThinking + cfg.reservedGeneration\n + autocompactBuffer + cfg.safetyMargin;\n\n const historyBudget = Math.max(cfg.minHistoryBudget, cfg.contextWindow - fixed);\n\n return {\n historyBudget,\n breakdown: {\n contextWindow: cfg.contextWindow,\n systemPrompt: systemTokens,\n tools: toolsTokens,\n reservedThinking: cfg.reservedThinking,\n reservedGeneration: cfg.reservedGeneration,\n autocompactBuffer,\n safetyMargin: cfg.safetyMargin,\n historyAvailable: historyBudget,\n },\n config: cfg,\n };\n}\n\n/**\n * Split history budget into summary / ragHist / window slots.\n */\nexport function splitHistoryBudget(\n historyBudget: number,\n cfg: CompactionConfig = DEFAULT_COMPACTION_CONFIG,\n): SplitBudget {\n const summary = Math.min(cfg.summaryMaxTokens, Math.floor(historyBudget * cfg.summaryRatio));\n const ragHist = Math.min(cfg.ragHistMaxTokens, Math.floor(historyBudget * cfg.ragHistRatio));\n const window = historyBudget - summary - ragHist;\n return { summary, ragHist, window };\n}\n\n// ─── Message assembly ──────────────────────────────────────────────────────\n\ninterface AssembleParams {\n messages: CompactionMessage[];\n summary?: string;\n retrievedTurns?: RetrievedTurn[];\n windowBudget: number;\n summaryBudget: number;\n ragBudget: number;\n systemContent?: string;\n /** Context-injection headers; default to English (DEFAULT_COMPACTION_CONFIG). */\n summaryLabel?: string;\n ragIntroLabel?: string;\n}\n\ninterface AssembleResult {\n compactedMessages: CompactionMessage[];\n used: UsageBreakdown;\n dropped: DroppedBreakdown;\n}\n\n/**\n * Assemble compacted message list given a budget and conversation state.\n *\n * Output order:\n * [system?] → [summary system msg?] → [ragHist system msg?] → window verbatim\n *\n * Always keeps the last 2 non-system messages verbatim (working memory),\n * regardless of windowBudget — these are the floor that can never be dropped.\n */\nexport function assembleCompacted(params: AssembleParams): AssembleResult {\n const {\n messages,\n summary = '',\n retrievedTurns = [],\n windowBudget,\n summaryBudget,\n ragBudget,\n systemContent,\n summaryLabel = DEFAULT_COMPACTION_CONFIG.summaryLabel,\n ragIntroLabel = DEFAULT_COMPACTION_CONFIG.ragIntroLabel,\n } = params;\n\n const out: CompactionMessage[] = [];\n const used: UsageBreakdown = { system: 0, summary: 0, ragHist: 0, window: 0 };\n const dropped: DroppedBreakdown = { ragHits: 0, oldTurns: 0, summary: false };\n\n // System message first\n if (systemContent) {\n out.push({ role: 'system', content: systemContent });\n used.system = estimateTokens(systemContent);\n }\n\n // Summary\n if (summary) {\n const summaryWrapped = `${summaryLabel}\\n${summary}`;\n const summaryToks = estimateTokens(summaryWrapped);\n if (summaryToks <= summaryBudget) {\n out.push({ role: 'system', content: summaryWrapped });\n used.summary = summaryToks;\n } else {\n const ratio = (summaryBudget / summaryToks) * 0.95;\n const truncated = summary.slice(0, Math.max(0, Math.floor(summary.length * ratio)));\n const wrapped = `${summaryLabel}\\n${truncated}…`;\n out.push({ role: 'system', content: wrapped });\n used.summary = estimateTokens(wrapped);\n dropped.summary = true;\n }\n }\n\n // RAG hist — old turns retrieved by cosine similarity, sorted by score desc\n if (retrievedTurns.length > 0) {\n const sorted = [...retrievedTurns].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));\n const lines: string[] = [];\n const intro = `${ragIntroLabel}\\n`;\n let toks = estimateTokens(intro);\n for (const t of sorted) {\n const line = `[${t.role}] ${t.content}`;\n const lt = estimateTokens(line);\n if (toks + lt > ragBudget) {\n dropped.ragHits++;\n continue;\n }\n lines.push(line);\n toks += lt;\n }\n if (lines.length > 0) {\n const content = intro + lines.join('\\n');\n out.push({ role: 'system', content });\n used.ragHist = estimateTokens(content);\n }\n }\n\n // Sliding window verbatim — newest → oldest, until windowBudget exhausted.\n // Always keep the last 2 non-system messages (working memory floor).\n const nonSystem = messages.filter(m => m.role !== 'system');\n if (nonSystem.length === 0) {\n return { compactedMessages: out, used, dropped };\n }\n\n const reversedWindow: CompactionMessage[] = [];\n let windowToks = 0;\n const minKeep = Math.min(2, nonSystem.length);\n\n for (let i = nonSystem.length - 1; i >= 0; i--) {\n const m = nonSystem[i];\n if (!m) continue;\n const t = estimateTokens(m.content);\n const idxFromEnd = nonSystem.length - 1 - i;\n\n if (idxFromEnd < minKeep) {\n // Working memory floor — kept no matter the budget.\n reversedWindow.unshift(m);\n windowToks += t;\n continue;\n }\n\n if (windowToks + t > windowBudget) {\n dropped.oldTurns += i + 1;\n break;\n }\n reversedWindow.unshift(m);\n windowToks += t;\n }\n\n /*\n * A window must never BEGIN on a `tool` result.\n *\n * The walk is newest-to-oldest and keeps the last `minKeep` messages whatever\n * the budget, so when the second-to-last message is a tool result the compacted\n * history opens with `role: 'tool'` — an orphan whose requesting assistant turn\n * was dropped. Every OpenAI-compatible provider rejects that with a 400\n * (\"messages with role 'tool' must be a response to a preceding message with\n * tool_calls\"), so compaction turned a long conversation into an unusable one.\n *\n * Dropping the orphan rather than reaching further back for its assistant turn\n * is deliberate: `CompactionMessage` is `{ role, content }` and cannot represent\n * the pairing at all — no `toolCallId`, no `toolCalls` — so there is nothing to\n * reconstruct WITH. Its content already survives in the summary the window sits\n * under. Carrying the pairing properly means widening the type, which is a\n * design change and not a bug fix.\n */\n while (reversedWindow.length && reversedWindow[0]?.role === 'tool') {\n const orphan = reversedWindow.shift();\n if (orphan) {\n windowToks -= estimateTokens(orphan.content);\n dropped.oldTurns += 1;\n }\n }\n\n out.push(...reversedWindow);\n used.window = windowToks;\n\n return { compactedMessages: out, used, dropped };\n}\n\n// ─── High-level helper ─────────────────────────────────────────────────────\n\n/**\n * Compute budget + assemble in one call. The primary entry point for\n * consumers (a browser request interceptor, mobile, Node tests).\n */\nexport function compactConversation(input: CompactionInput): CompactionResult {\n const { messages, systemPrompt, toolsArray, summary, retrievedTurns, config } = input;\n\n const budget = computeHistoryBudget({ systemPrompt, toolsArray, config });\n const split = splitHistoryBudget(budget.historyBudget, budget.config);\n\n const { compactedMessages, used, dropped } = assembleCompacted({\n messages,\n summary,\n retrievedTurns,\n windowBudget: split.window,\n summaryBudget: split.summary,\n ragBudget: split.ragHist,\n systemContent: systemPrompt,\n summaryLabel: budget.config.summaryLabel,\n ragIntroLabel: budget.config.ragIntroLabel,\n });\n\n const totalUsed = budget.breakdown.systemPrompt\n + budget.breakdown.tools\n + used.summary\n + used.ragHist\n + used.window;\n\n const free = budget.config.contextWindow - totalUsed\n - budget.config.reservedThinking - budget.config.reservedGeneration\n - budget.breakdown.autocompactBuffer - budget.config.safetyMargin;\n\n const breakdown: FullBreakdown = {\n ...budget.breakdown,\n historyAllocated: split,\n historyUsed: used,\n totalUsed,\n free,\n dropped,\n };\n\n return { compactedMessages, breakdown };\n}\n","/**\n * conversation/selector.ts — the budget-aware `compactionSelector` for `AparteClient`.\n *\n * `AparteClient.compact()` decides what to summarise through one function:\n * `(messages) => { keep, drop }`. Its default drops everything — summarise all,\n * replace all. This is the other half the seam was written for: the newest turns\n * that still fit the history budget stay verbatim, the older ones are summarised.\n *\n * The budget is the compactor's (`computeHistoryBudget` + `splitHistoryBudget`), so\n * the gauge a page shows, the selector `compact()` uses and the window the model\n * declares all speak the same numbers. The budget is closed over here by the\n * consumer — the window is theirs to know, the client's to never guess.\n */\n\nimport { computeHistoryBudget, estimateTokens, splitHistoryBudget, type CompactionConfig } from './compactor.js';\n\n/**\n * The least a message must carry to be costed: its text, or segments with text.\n * Structural on purpose — this package imports nothing from core — and core's\n * `AparteMessage` satisfies it, which is what makes the selector below exactly the\n * `compactionSelector` `AparteClient` takes.\n */\nexport interface CompactableMessage {\n content?: string;\n /**\n * `unknown` elements, read defensively below: core's segment union includes a\n * segment with no `content` at all, and TypeScript refuses a type with no property\n * in common with the all-optional `{ content?: unknown }` (a \"weak type\"), so that\n * tighter shape made `AparteMessage` unassignable here.\n */\n segments?: ReadonlyArray<unknown>;\n}\n\n/** What `compact()` asks for: the messages kept verbatim and the ones to summarise. */\nexport interface CompactionSelection<M> {\n keep: M[];\n drop: M[];\n}\n\n/**\n * The selector itself — generic in the MESSAGE type, so it is assignable to core's\n * `AparteCompactionSelector` (messages of `AparteMessage`) without importing it. The\n * generic sits on the returned function, not on the factory: a factory-level `<M>`\n * has no inference site at `createCompactionSelector({...})` and falls back to the\n * bare `CompactableMessage`, which is not what the client's option type wants back.\n */\nexport type CompactionSelector = <M extends CompactableMessage>(messages: M[]) => CompactionSelection<M>;\n\nexport interface CompactionSelectorOptions {\n /**\n * The active model's context window, in tokens — a number, or a getter read at\n * each call so a model change is picked up (`() => aparteGlobalConfig.getCurrentModel()?.contextWindow`).\n * Unknown (`undefined`) means nothing is dropped: without a window there is no\n * budget to be over.\n */\n contextWindow: number | (() => number | undefined);\n /** The system prompt the request carries, for the budget. A string or a getter. Default none. */\n systemPrompt?: string | (() => string | null | undefined);\n /** The tools declared to the model, for the budget. A value or a getter. Default none. */\n tools?: unknown | (() => unknown);\n /** Partial override of the compactor's config — reserves, ratios, floors. */\n config?: Partial<Omit<CompactionConfig, 'contextWindow'>>;\n /** Never summarise fewer than this many of the newest messages. Default 2 — the last exchange. */\n minKeep?: number;\n}\n\nconst read = <T>(value: T | (() => T)): T => (typeof value === 'function' ? (value as () => T)() : value);\n\n/** What a message costs: its text, or the text of its segments when it has no `content`. */\nconst textOf = (message: CompactableMessage): string => {\n if (typeof message.content === 'string' && message.content.length > 0) return message.content;\n return (message.segments ?? [])\n .map((segment) => {\n const content = (segment as { content?: unknown }).content;\n return typeof content === 'string' ? content : '';\n })\n .join('\\n');\n};\n\n/**\n * Build the selector. Give it to the client:\n *\n * ```ts\n * new AparteClient({\n * compactionSelector: createCompactionSelector({\n * contextWindow: () => aparteGlobalConfig.getCurrentModel()?.contextWindow,\n * systemPrompt: () => aparteGlobalConfig.resolveSystemPrompt(),\n * }),\n * });\n * ```\n *\n * Walks the history from the newest message: what fits the sliding window's budget\n * is kept verbatim, the rest is dropped for summarising. When everything fits,\n * `drop` is empty and `compact()` reports `{ skipped: true }` — nothing to do.\n */\nexport function createCompactionSelector(options: CompactionSelectorOptions): CompactionSelector {\n const minKeep = Math.max(0, options.minKeep ?? 2);\n return <M extends CompactableMessage>(messages: M[]): CompactionSelection<M> => {\n const contextWindow = read(options.contextWindow);\n if (!contextWindow || contextWindow <= 0) return { keep: messages, drop: [] };\n\n const budget = computeHistoryBudget({\n systemPrompt: read(options.systemPrompt) ?? '',\n toolsArray: read(options.tools),\n config: { ...options.config, contextWindow },\n });\n const windowBudget = splitHistoryBudget(budget.historyBudget, budget.config).window;\n\n let used = 0;\n let cut = messages.length;\n for (let i = messages.length - 1; i >= 0; i--) {\n const cost = estimateTokens(textOf(messages[i]!));\n const kept = messages.length - 1 - i;\n if (used + cost > windowBudget && kept >= minKeep) break;\n used += cost;\n cut = i;\n }\n if (cut === 0) return { keep: messages, drop: [] };\n return { keep: messages.slice(cut), drop: messages.slice(0, cut) };\n };\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aparte/engine",
3
- "version": "0.13.1",
3
+ "version": "0.15.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,
@@ -23,9 +23,6 @@
23
23
  "engines": {
24
24
  "node": ">=18"
25
25
  },
26
- "peerDependencies": {
27
- "@aparte/core": ">=0.13.1 <1.0.0"
28
- },
29
26
  "peerDependenciesMeta": {
30
27
  "@aparte/core": {
31
28
  "optional": true
@@ -34,8 +31,7 @@
34
31
  "devDependencies": {
35
32
  "@types/node": "^22.0.0",
36
33
  "typescript": "^5.4.0",
37
- "vite": "^6.0.0",
38
- "@aparte/core": "0.13.1"
34
+ "vite": "^6.0.0"
39
35
  },
40
36
  "keywords": [
41
37
  "ai",
@@ -1,87 +0,0 @@
1
- /**
2
- * artifact-xml-state-machine.ts — streaming `<artifact>` XML parser (pure).
3
- *
4
- * The framework-free extraction of core's `client/xml-artifact-feed.ts` (the
5
- * scanning half) plus the `XML artifact finalize` block inside
6
- * `AparteClient._streamLoop`.
7
- * Small models emit artifacts as `…chat text… <artifact mimeType="…" title="…">
8
- * BODY</artifact> …more text…`, streamed in arbitrary chunks that can split the
9
- * opening tag, the body, or the closing tag across delta boundaries.
10
- *
11
- * This module owns only the PARSING (state + buffering); it emits DOM-free
12
- * micro-events. The `@aparte/core` adapter turns them into segment/lifecycle calls
13
- * (addSegment / updateSegment / _dispatchArtifactLifecycle) exactly as
14
- * `_streamLoop` does — so no DOM, no `@aparte/core` import here.
15
- *
16
- * Ported faithfully, including quirks: a `<artifact` split as `<arti`+`fact`
17
- * (so `indexOf('<artifact')` misses it) is treated as chat text, matching the
18
- * source. Providers emit the tag atomically in practice.
19
- */
20
- /** Where the streaming parser is between artifacts. */
21
- export type XmlArtifactState = 'normal' | 'scanning' | 'in-artifact';
22
- /** Fallback mime/kind for an artifact whose open tag omits the attribute. */
23
- export interface XmlArtifactHint {
24
- mimeType: string;
25
- kind: string;
26
- }
27
- /** DOM-free micro-events the machine emits; the adapter renders them. */
28
- export type XmlArtifactEvent = {
29
- type: 'chat-text';
30
- text: string;
31
- reduced?: boolean;
32
- } | {
33
- type: 'artifact-open';
34
- id: string;
35
- mimeType: string;
36
- kind: string;
37
- title: string;
38
- } | {
39
- type: 'artifact-chunk';
40
- id: string;
41
- content: string;
42
- } | {
43
- type: 'artifact-close';
44
- id: string;
45
- content: string;
46
- inline: boolean;
47
- };
48
- /**
49
- * Map an artifact mimeType to a renderer kind. Byte-identical copy of core's
50
- * canonical `deriveArtifactKind` (parsers/aparte-stream-parser.ts) — the
51
- * duplicate stays because `@aparte/core` is an OPTIONAL peer of the engine, so
52
- * no runtime import is possible. Kept in sync mechanically by
53
- * `__tests__/derive-artifact-kind-parity.test.ts`.
54
- */
55
- export declare function deriveArtifactKind(mimeType: string, fallback?: string): string;
56
- /**
57
- * A stateful streaming parser for one turn's `<artifact>` blocks. Feed it each
58
- * text delta; drain the returned events. Call {@link finalize} when the stream
59
- * ends to flush a truncated (unclosed) artifact.
60
- */
61
- export declare class ArtifactXmlStateMachine {
62
- private readonly hint;
63
- private state;
64
- /** Buffers the opening tag until its `>` arrives (may span deltas). */
65
- private scanBuf;
66
- /** Buffers the tail that might be the start of a split `</artifact>`. */
67
- private closeBuf;
68
- private segId;
69
- private content;
70
- private mime;
71
- private kind;
72
- private title;
73
- private seq;
74
- private readonly idGen;
75
- constructor(hint: XmlArtifactHint, idGen?: () => string);
76
- /** Feed one text delta; returns the ordered micro-events it produced. */
77
- feed(delta: string): XmlArtifactEvent[];
78
- /**
79
- * Flush a truncated artifact: if the stream ended mid-body (model cut off
80
- * before `</artifact>`), emit a close with whatever was buffered. Mirrors the
81
- * `XML artifact finalize` block in `AparteClient._streamLoop`.
82
- */
83
- finalize(): XmlArtifactEvent[];
84
- /** Current parser state (for the adapter to decide finalize routing). */
85
- get currentState(): XmlArtifactState;
86
- }
87
- //# sourceMappingURL=artifact-xml-state-machine.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"artifact-xml-state-machine.d.ts","sourceRoot":"","sources":["../../../src/agent/parsers/artifact-xml-state-machine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,uDAAuD;AACvD,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,UAAU,GAAG,aAAa,CAAC;AAErE,6EAA6E;AAC7E,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,yEAAyE;AACzE,MAAM,MAAM,gBAAgB,GAOtB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACtD;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACpF;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAoB/E;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,SAAY,GAAG,MAAM,CAqBjF;AAED;;;;GAIG;AACH,qBAAa,uBAAuB;IAcpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAbjC,OAAO,CAAC,KAAK,CAA8B;IAC3C,uEAAuE;IACvE,OAAO,CAAC,OAAO,CAAM;IACrB,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;gBAER,IAAI,EAAE,eAAe,EAAE,KAAK,CAAC,EAAE,MAAM,MAAM;IAIxE,yEAAyE;IACzE,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,EAAE;IA2FvC;;;;OAIG;IACH,QAAQ,IAAI,gBAAgB,EAAE;IAqB9B,yEAAyE;IACzE,IAAI,YAAY,IAAI,gBAAgB,CAEnC;CACJ"}
@@ -1,74 +0,0 @@
1
- /**
2
- * stream-events.contract.ts — COMPILE-TIME guard for the core↔engine run-event mirror.
3
- *
4
- * `StreamRunEvent` (this package) and `AparteStreamRunEvent` (@aparte/core's adapter)
5
- * are hand-mirrored across the zero-import boundary: core is the zero-dep leaf and must
6
- * never import engine, and engine keeps its stream contract standalone (see
7
- * `stream-events.ts`). That leaves the two unions synced BY HAND — the seam's one
8
- * unguarded soft spot. A silent drift here (a renamed/added variant, a changed payload
9
- * field) would corrupt streaming with no CI signal.
10
- *
11
- * This file makes that drift a TYPECHECK ERROR. It ships nothing: it only declares
12
- * type aliases (erased) and `import type`s core (erased → no runtime dep, so the
13
- * runtime zero-import rule still holds; core is present as engine's dev/peer dep at
14
- * typecheck time). It is not imported by the barrel, so it stays out of the bundle.
15
- *
16
- * The ONE remaining difference is `run-done.usage`: core carries the rich
17
- * `AparteUsage` (extra named provider-timing fields — ttft/decode/phases/…), engine
18
- * the five common ones. Those are mutually assignable — extra OPTIONAL fields never
19
- * block an assignment — but not equal, so that field is normalized away before the
20
- * exact-equality comparison of everything else.
21
- *
22
- * It used to differ for another reason: `StreamUsage` carried an
23
- * `[key: string]: unknown` "opaque passthrough" signature. That signature was the
24
- * last thing making `runStreamAgent` un-assignable to `AparteStreamRunner`, because
25
- * `transportCall` is contravariant and core's `AparteUsage` has no index signature.
26
- * Worth naming plainly: this guard normalized away the exact field that broke the
27
- * seam, so it had been written AROUND the defect — every shape it compared matched,
28
- * while the one composition both packages exist for did not compile. Assertion 4
29
- * below is what closes that gap, and it is the one that checks the composition
30
- * itself rather than a shape.
31
- */
32
- import type { AparteStreamRunEvent, AparteStreamRunEmitter, AparteStreamRunner } from '@aparte/core';
33
- import type { StreamRunEvent, StreamRunEmitter } from './stream-events.js';
34
- /** Invariant type-equality — distinguishes optional vs required and index signatures. */
35
- type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
36
- /** Compiles only when its argument is exactly `true`. */
37
- type Expect<T extends true> = T;
38
- /** Distributes over a union `A`: `true` iff every member is assignable to `B`. */
39
- type Assignable<A, B> = A extends B ? true : false;
40
- /** Erase the sole intentional difference (run-done.usage) before the equality check. */
41
- type NormalizeRunDone<E> = E extends {
42
- type: 'run-done';
43
- } ? {
44
- type: 'run-done';
45
- usage?: unknown;
46
- } : E;
47
- /**
48
- * Each element compiles only if its contract holds; a drift turns one into a
49
- * `false`, which fails `Expect<...>` and breaks the typecheck. Exported so it isn't
50
- * flagged as unused — this file is not re-exported by the barrel, so it never reaches
51
- * `@aparte/engine`'s public surface.
52
- *
53
- * 1. Every variant except `run-done.usage` is structurally identical (exact equality).
54
- * 2. The `run-done` usage stays forwardable engine→core (lets core's adapter treat it
55
- * as `AparteUsage`).
56
- * 3. The emitter core injects into `runStreamAgent` satisfies engine's emitter contract.
57
- * 4. **`runStreamAgent` is actually assignable to `AparteStreamRunner`.** This was the
58
- * hole: every mirror above compared event SHAPES, and all of them held, while the
59
- * one composition the two packages exist to make —
60
- * `new AparteClient({ streamRunner: runStreamAgent })` — did not compile.
61
- * `StreamAgentMessage.content` was written to mirror `AparteChatMessage` and never
62
- * followed it to `string | AparteContentPart[]`. Nothing in the repo assigned the
63
- * two together (the seam test builds its own runner), so it had zero compile
64
- * coverage anywhere, which is exactly why it shipped broken across five docs pages
65
- * and a README. One line here is the whole guard.
66
- */
67
- export type StreamEventContract = [
68
- Expect<Equal<NormalizeRunDone<StreamRunEvent>, NormalizeRunDone<AparteStreamRunEvent>>>,
69
- Expect<Assignable<StreamRunEvent, AparteStreamRunEvent>>,
70
- Expect<Assignable<AparteStreamRunEmitter, StreamRunEmitter>>,
71
- Expect<Assignable<typeof import('./stream-run.js')['runStreamAgent'], AparteStreamRunner>>
72
- ];
73
- export {};
74
- //# sourceMappingURL=stream-events.contract.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"stream-events.contract.d.ts","sourceRoot":"","sources":["../../src/agent/stream-events.contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACrG,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3E,yFAAyF;AACzF,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC,IACX,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC;AACzF,yDAAyD;AACzD,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAChC,kFAAkF;AAClF,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC;AAEnD,wFAAwF;AACxF,KAAK,gBAAgB,CAAC,CAAC,IAAI,CAAC,SAAS;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,CAAC,CAAC;AAEtG;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAC9B,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,cAAc,CAAC,EAAE,gBAAgB,CAAC,oBAAoB,CAAC,CAAC,CAAC;IACvF,MAAM,CAAC,UAAU,CAAC,cAAc,EAAE,oBAAoB,CAAC,CAAC;IACxD,MAAM,CAAC,UAAU,CAAC,sBAAsB,EAAE,gBAAgB,CAAC,CAAC;IAC5D,MAAM,CAAC,UAAU,CAAC,cAAc,iBAAiB,CAAC,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,CAAC;CAC7F,CAAC"}