@orkestrel/ollama 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # @orkestrel/ollama
2
2
 
3
- A typed local-LLM provider for the `@orkestrel` line — a `ProviderInterface`
4
- implementation over a local Ollama daemon's `POST /api/chat`, with NDJSON
5
- streaming, tool calls, thinking, and usage accounting, built on pure
6
- web-standard `fetch` / `ReadableStream` (no Ollama SDK dependency).
3
+ > A typed local-LLM provider for the `@orkestrel` line: a `ProviderInterface` over a
4
+ > local Ollama daemon's `POST /api/chat`, with non-streaming `generate`, NDJSON
5
+ > `stream`, tool calls, thinking, and usage accounting, built on web-standard `fetch`
6
+ > and `ReadableStream` with no Ollama SDK dependency.
7
+
8
+ Create a provider with the `createOllama` function, hand it a conversation and a
9
+ bounding `AbortSignal`, and read the assembled `ProviderResult` the `generate`
10
+ method resolves — or drive the `stream` method for live deltas. Point `url` at your
11
+ own server and attach a short-lived token through `headers` where a browser runtime
12
+ must not hold the real key.
7
13
 
8
14
  ## Install
9
15
 
@@ -4,11 +4,15 @@ let _orkestrel_agent = require("@orkestrel/agent");
4
4
  let _orkestrel_ndjson = require("@orkestrel/ndjson");
5
5
  let _orkestrel_timeout = require("@orkestrel/timeout");
6
6
  //#region src/server/constants.ts
7
- /** Names the local Ollama daemon base URL assumed when `OllamaOptions.url` is omitted. */
7
+ /**
8
+ * Names the local Ollama daemon base URL, `'http://localhost:11434'`, assumed when
9
+ * `OllamaOptions.url` is omitted.
10
+ */
8
11
  var DEFAULT_OLLAMA_URL = "http://localhost:11434";
9
12
  /**
10
- * Names how long the model stays resident after a call when `OllamaOptions.keepAlive` is
11
- * omitted — Ollama's own `keep_alive` default, expressed as a duration string.
13
+ * Names how long the model stays resident after a call — `'5m'` when
14
+ * `OllamaOptions.keepAlive` is omitted, Ollama's own `keep_alive` default, expressed as a
15
+ * duration string.
12
16
  *
13
17
  * @remarks
14
18
  * The name mirrors the Ollama `/api/chat` `keep_alive` field this value is sent as, so
@@ -16,20 +20,19 @@ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
16
20
  */
17
21
  var DEFAULT_KEEP_ALIVE = "5m";
18
22
  /**
19
- * Names the per-call deadline in milliseconds when `OllamaOptions.timeout` is omitted —
20
- * generous enough that a cold model load does not trip it.
23
+ * Names the per-call deadline in milliseconds, `120_000`, when `OllamaOptions.timeout` is
24
+ * omitted — generous enough that a cold model load does not trip it.
21
25
  */
22
26
  var DEFAULT_PROVIDER_TIMEOUT = 12e4;
23
27
  /**
24
- * Names the cap, in characters, on how much of a non-OK response body is
28
+ * Names the character cap, `2048`, on how much of a non-OK response body is
25
29
  * incorporated into a thrown {@link OllamaHTTPError}'s message.
26
30
  *
27
31
  * @remarks
28
32
  * Bounds the excerpt so a defensive proxy or a misbehaving daemon handing
29
33
  * back an unbounded response body cannot inflate the thrown error's message
30
- * without limit. `2048` characters is generous enough to carry a
31
- * useful diagnostic snippet while staying well short of any practical size
32
- * concern.
34
+ * without limit, while the cap stays generous enough to carry a useful
35
+ * diagnostic snippet.
33
36
  */
34
37
  var MAX_ERROR_BODY_LENGTH = 2048;
35
38
  //#endregion
@@ -42,7 +45,8 @@ var MAX_ERROR_BODY_LENGTH = 2048;
42
45
  * HTTP response was received at all, for example a `null` body). Thrown by
43
46
  * {@link OllamaProvider} at its HTTP failure sites — the non-OK status branch and the
44
47
  * null-body branch — so a caller can branch on `error.code` and read `error.status`
45
- * for the HTTP number instead of parsing the message. Narrow a caught value with
48
+ * for the HTTP number instead of parsing the message. The message carries a body excerpt
49
+ * bounded to {@link MAX_ERROR_BODY_LENGTH} — `2048` characters. Narrow a caught value with
46
50
  * {@link isOllamaHTTPError}.
47
51
  *
48
52
  * @example
@@ -72,6 +76,10 @@ var OllamaHTTPError = class extends Error {
72
76
  /**
73
77
  * Checks whether a value is an {@link OllamaHTTPError}.
74
78
  *
79
+ * @remarks
80
+ * The check is an `instanceof` test, so it narrows a caught `unknown` to the error class
81
+ * without parsing the thrown message.
82
+ *
75
83
  * @param value - The value to test
76
84
  * @returns True if `value` is an `OllamaHTTPError`; false otherwise
77
85
  */
@@ -108,7 +116,7 @@ function mapMessages(messages) {
108
116
  }));
109
117
  }
110
118
  /**
111
- * Builds a provider result from a turn's content, reasoning, tool calls, and usage.
119
+ * Builds a `ProviderResult` from a turn's content, reasoning, tool calls, and usage.
112
120
  *
113
121
  * @remarks
114
122
  * Only the present optionals are set: no empty `thinking`, no empty `tools`, and no
@@ -171,11 +179,12 @@ function extractThinking(record) {
171
179
  return (0, _orkestrel_contract.isString)(thinking) ? thinking : "";
172
180
  }
173
181
  /**
174
- * Joins a call's two reasoning carriers into the result's `thinking`.
182
+ * Joins a call's reasoning carriers — the splitter's separated in-content spans and the
183
+ * accumulated wire-side `message.thinking` — into the result's `thinking`.
175
184
  *
176
185
  * @param splitter - The per-call splitter holding the separated in-content spans
177
186
  * @param wired - The accumulated wire-side `message.thinking` text
178
- * @returns The two carriers separated by a blank line, or whichever one is non-empty
187
+ * @returns The carriers separated by a blank line, or whichever one is non-empty
179
188
  *
180
189
  * @example
181
190
  * ```ts
@@ -303,16 +312,16 @@ async function parseBody(response) {
303
312
  * @remarks
304
313
  * - **Wire protocol.** Posts `{ model, messages, stream, keep_alive, think }` plus
305
314
  * passthrough sampling `options` and mapped function `tools`. The `think` flag is
306
- * CONFIGURABLE through {@link OllamaOptions.think} (default `false`). Non-stream parses
315
+ * configurable through {@link OllamaOptions.think} (default `false`). Non-stream parses
307
316
  * one JSON body; stream consumes NDJSON (one JSON object per `\n`-terminated line) —
308
317
  * deltas carry `message.content`, the final `done: true` line carries the token usage.
309
318
  * - **Think separation.** The wire `think` flag is configurable
310
319
  * ({@link OllamaOptions.think}, default `false`). With `think: true` a thinking model's
311
- * daemon separates reasoning NATIVELY — returning it on the distinct `message.thinking`
312
- * channel (read here through `extractThinking`) instead of inline in `message.content`. EITHER
320
+ * daemon separates reasoning natively — returning it on the distinct `message.thinking`
321
+ * channel (read here through `extractThinking`) instead of inline in `message.content`. Either
313
322
  * way the per-call {@link ThinkSplitterInterface} is the defensive guarantee: a daemon
314
323
  * may ignore `think: false` for a thinking model and inline `<think>` tags, so every
315
- * content delta routes through the splitter, only CLEAN content is yielded / assembled,
324
+ * content delta routes through the splitter, only clean content is yielded / assembled,
316
325
  * and the separated reasoning (plus any daemon-side `message.thinking` deltas) lands on
317
326
  * `ProviderResult.thinking`, never in the conversation.
318
327
  * - **Boundary narrowing.** Every wire value arrives as `unknown` and is
@@ -321,11 +330,11 @@ async function parseBody(response) {
321
330
  * usage, `{}` arguments), never a throw.
322
331
  * - **Bounded.** Each call arms a {@link Timeout} for `OllamaOptions.timeout` and
323
332
  * passes `AbortSignal.any([timeout.signal, signal])` to `fetch`, so the caller's
324
- * signal AND the deadline both cancel the request. The timeout is always cleared —
333
+ * signal and the deadline both cancel the request. The timeout is always cleared —
325
334
  * in `#fetch` if the request fails/aborts, otherwise in the consuming call's `finally`.
326
335
  * - **Abort recovers partial.** A `stream` cancelled mid-flight throws a
327
336
  * `ProviderAbortError` carrying the partial result assembled so far; pairing the
328
- * `TextDecoder({ stream: true })` with the {@link NDJSONParser} parser keeps multi-byte
337
+ * `TextDecoder({ stream: true })` with the `createNDJSONParser` parser keeps multi-byte
329
338
  * UTF-8 splits and partial lines honest.
330
339
  * - **Event-free.** A pure functional boundary — no Emitter, no events.
331
340
  * - **Transport seam.** {@link OllamaOptions.fetch} swaps the transport (default
@@ -378,18 +387,18 @@ var OllamaProvider = class {
378
387
  return this.#id;
379
388
  }
380
389
  /**
381
- * Exposes the provider's context-framing default — the PROVIDER-DEFAULT level of
382
- * {@link import('@orkestrel/agent').AgentContextInterface.build}'s format cascade (it BEATS
383
- * the managers' built-in framing, is BEATEN by a manager-options or per-item override).
384
- * Satisfies the OPTIONAL {@link ProviderInterface.format} contract member: `undefined`
390
+ * Exposes the provider's context-framing default — the provider-default level of
391
+ * {@link import('@orkestrel/agent').AgentContextInterface.build}'s format cascade (it beats
392
+ * the managers' built-in framing, is beaten by a manager-options or per-item override).
393
+ * Satisfies the optional {@link ProviderInterface.format} contract member: `undefined`
385
394
  * when {@link OllamaOptions.format} was omitted (the framing-agnostic default ⇒ core's
386
395
  * built-in framing applies unchanged), else the exact configured framing the Agent
387
396
  * threads into `build()`.
388
397
  *
389
398
  * @remarks
390
- * EXPOSE-ONLY — read by the Agent loop and consumed by core's cascade; it is NEVER sent
391
- * on the `/api/chat` wire (it is absent from `#body` / the request). This is NOT Ollama's
392
- * structured-output `format` wire parameter — that one IS sent in `#body`, but only when
399
+ * Expose-only — read by the Agent loop and consumed by core's cascade; it is never sent
400
+ * on the `/api/chat` wire (it is absent from `#body` / the request). This is not Ollama's
401
+ * structured-output `format` wire parameter — that one is sent in `#body`, but only when
393
402
  * a per-call `ProviderStreamOptions.schema` is supplied; only the word collides.
394
403
  *
395
404
  * @returns The configured {@link ContextFormat}, or `undefined` when none
@@ -397,6 +406,24 @@ var OllamaProvider = class {
397
406
  get format() {
398
407
  return this.#format;
399
408
  }
409
+ /**
410
+ * Generates one complete turn and resolves the assembled result — the clean content,
411
+ * any separated reasoning, any tool calls, and any usage the wire reported.
412
+ *
413
+ * @remarks
414
+ * Sends `stream: false` and parses one JSON body. Content routes through a per-call
415
+ * think splitter, so the assembled content stays clean even where the daemon renders a
416
+ * thinking model's reasoning inline; the separated spans and any daemon-side
417
+ * `message.thinking` land on `thinking`. The caller's signal and the armed deadline
418
+ * both cancel the request, and the deadline is cleared once the body is read.
419
+ *
420
+ * @param messages - The conversation turns to send
421
+ * @param signal - The caller's bounding signal, folded with the armed deadline
422
+ * @param tools - The callable tools to advertise for this turn, when the caller passes any
423
+ * @param options - The per-call overrides, `think` and `schema` among them
424
+ * @returns The assembled result of the turn
425
+ * @throws {@link OllamaHTTPError} When the daemon answers a non-OK status.
426
+ */
400
427
  async generate(messages, signal, tools, options) {
401
428
  const { response, timeout } = await this.#fetch(messages, false, signal, tools, options);
402
429
  try {
@@ -410,6 +437,25 @@ var OllamaProvider = class {
410
437
  timeout.clear();
411
438
  }
412
439
  }
440
+ /**
441
+ * Streams one turn, yielding a channel-tagged delta per non-empty content or reasoning
442
+ * span and returning the assembled result when the stream completes.
443
+ *
444
+ * @remarks
445
+ * Sends `stream: true` and consumes NDJSON — one JSON object per newline-terminated
446
+ * line — pairing a streaming `TextDecoder` with the `NDJSONParser` so a record split
447
+ * across byte reads is reassembled. The returned result's content is the splitter's
448
+ * clean accumulation, beside any tool calls collected across lines and the usage the
449
+ * `done` line carries. A cancel mid-flight throws a `ProviderAbortError` carrying the
450
+ * partial assembled so far.
451
+ *
452
+ * @param messages - The conversation turns to send
453
+ * @param signal - The caller's bounding signal, folded with the armed deadline
454
+ * @param tools - The callable tools to advertise for this turn, when the caller passes any
455
+ * @param options - The per-call overrides, `think` and `schema` among them
456
+ * @returns The assembled result of the turn, after the last delta
457
+ * @throws {@link OllamaHTTPError} When the daemon answers a non-OK status or a `null` body.
458
+ */
413
459
  async *stream(messages, signal, tools, options) {
414
460
  const { response, timeout, combined } = await this.#fetch(messages, true, signal, tools, options);
415
461
  const body = response.body;
@@ -545,36 +591,43 @@ var OllamaProvider = class {
545
591
  * @remarks
546
592
  * Only `model` is required; `url` defaults to the local daemon, `keepAlive` to `'5m'`,
547
593
  * `timeout` to `120_000`ms, and `options` is forwarded verbatim as sampling
548
- * parameters (`temperature`, `seed`, and `num_predict`). Both calls take an
594
+ * parameters (`temperature`, `seed`, and `num_predict`). Each call takes an
549
595
  * `AbortSignal` to bound the request; a `stream` cancelled mid-flight throws a
550
596
  * `ProviderAbortError` carrying the partial result.
551
597
  *
552
598
  * The optional `fetch` + `headers` form a transport seam (see {@link OllamaOptions}):
553
599
  * point `url` at your own server, inject a custom `fetch`, and have `headers` attach a
554
600
  * generated/obfuscated bearer token your server validates — so a browser runtime
555
- * reaches the LLM through your middleware WITHOUT this library ever handling the real API
601
+ * reaches the LLM through your middleware without this library ever handling the real API
556
602
  * key. Both omitted ⇒ the global `fetch` and only a JSON content type.
557
603
  *
558
- * The optional `format` is the provider's context-framing default — the PROVIDER-DEFAULT
604
+ * The optional `format` is the provider's context-framing default — the provider-default
559
605
  * level of `AgentContext`'s format cascade (beaten by a manager-options or per-item
560
606
  * override, beating the managers' built-in framing), declaring how this
561
607
  * provider's models prefer context sections framed (for example XML group wrappers vs. Markdown
562
- * headers). It is EXPOSED on the provider for the Agent's `build()` and is NOT Ollama's
563
- * `/api/chat` `format` wire parameter (structured output) — the two are unrelated despite
564
- * the shared word. Omitted ⇒ the provider is framing-agnostic (core's built-in defaults).
608
+ * headers). It is exposed on the provider for the Agent's `build()` and is not Ollama's
609
+ * `/api/chat` `format` wire parameter (structured output) — the framing default and that
610
+ * wire parameter are unrelated despite the shared word. Omitted ⇒ the provider is
611
+ * framing-agnostic (core's built-in defaults).
565
612
  *
566
613
  * @param options - `model` (required), and optional `url` / `keepAlive` / `timeout` /
567
614
  * `options` / `fetch` / `headers` / `format` (see {@link OllamaOptions})
568
615
  * @returns A working {@link ProviderInterface} backed by Ollama
569
616
  *
570
- * @example
617
+ * @example createOllama + generate
571
618
  * ```ts
572
619
  * import { createAbort } from '@orkestrel/abort'
573
620
  * import { createOllama } from '@orkestrel/ollama'
574
621
  *
575
- * const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M' })
622
+ * const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M', options: { temperature: 0 } })
576
623
  * const abort = createAbort()
624
+ * const messages = [
625
+ * { id: '1', role: 'user', content: 'Summarize the release notes for version 2.0.' },
626
+ * ] as const
627
+ *
577
628
  * const result = await provider.generate(messages, abort.signal)
629
+ * console.log(result.content)
630
+ * if (result.usage) charge(result.usage) // fold into a token budget
578
631
  * ```
579
632
  *
580
633
  * @example
@@ -590,7 +643,7 @@ var OllamaProvider = class {
590
643
  *
591
644
  * @example
592
645
  * Declare a context-framing default — wrap the instructions section in an XML group (the
593
- * provider-default level of `AgentContext`'s cascade; NOT the wire `format`):
646
+ * provider-default level of `AgentContext`'s cascade; not the wire `format`):
594
647
  * ```ts
595
648
  * const provider = createOllama({
596
649
  * model: 'qwen3.5:2b-q4_K_M',
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["#id","#model","#url","#keepAlive","#timeout","#think","#options","#transport","#headers","#format","#fetch","#deltas","#requestHeaders","#body"],"sources":["../../../src/server/constants.ts","../../../src/server/errors.ts","../../../src/server/helpers.ts","../../../src/server/parsers.ts","../../../src/server/OllamaProvider.ts","../../../src/server/factories.ts"],"sourcesContent":["// Ollama constants — the provider's defaults.\n\n/** Names the local Ollama daemon base URL assumed when `OllamaOptions.url` is omitted. */\nexport const DEFAULT_OLLAMA_URL = 'http://localhost:11434'\n\n/**\n * Names how long the model stays resident after a call when `OllamaOptions.keepAlive` is\n * omitted — Ollama's own `keep_alive` default, expressed as a duration string.\n *\n * @remarks\n * The name mirrors the Ollama `/api/chat` `keep_alive` field this value is sent as, so\n * the constant, the `OllamaOptions.keepAlive` key, and the wire member read as one term.\n */\nexport const DEFAULT_KEEP_ALIVE = '5m'\n\n/**\n * Names the per-call deadline in milliseconds when `OllamaOptions.timeout` is omitted —\n * generous enough that a cold model load does not trip it.\n */\nexport const DEFAULT_PROVIDER_TIMEOUT = 120_000\n\n/**\n * Names the cap, in characters, on how much of a non-OK response body is\n * incorporated into a thrown {@link OllamaHTTPError}'s message.\n *\n * @remarks\n * Bounds the excerpt so a defensive proxy or a misbehaving daemon handing\n * back an unbounded response body cannot inflate the thrown error's message\n * without limit. `2048` characters is generous enough to carry a\n * useful diagnostic snippet while staying well short of any practical size\n * concern.\n */\nexport const MAX_ERROR_BODY_LENGTH = 2048\n","// Errors for the Ollama provider. A single `OllamaHTTPError` carries the\n// `/api/chat` HTTP status at the boundary — non-OK responses and a missing\n// response body both throw it — so a `catch` can branch on `error.status`\n// rather than parsing a message.\n\nimport type { OllamaHTTPErrorOptions } from './types.js'\n\n/**\n * Represents an error thrown when the Ollama `/api/chat` HTTP transport fails.\n *\n * @remarks\n * Carries the machine-readable `code` `'HTTP'` and the response `status` (0 when no\n * HTTP response was received at all, for example a `null` body). Thrown by\n * {@link OllamaProvider} at its HTTP failure sites — the non-OK status branch and the\n * null-body branch — so a caller can branch on `error.code` and read `error.status`\n * for the HTTP number instead of parsing the message. Narrow a caught value with\n * {@link isOllamaHTTPError}.\n *\n * @example\n * ```ts\n * try {\n * \tawait provider.generate(messages, signal)\n * } catch (error) {\n * \tif (isOllamaHTTPError(error) && error.status === 404) {\n * \t\t// the configured model isn't pulled\n * \t}\n * }\n * ```\n */\nexport class OllamaHTTPError extends Error {\n\t/**\n\t * Names the machine-readable condition this error reports — `'HTTP'`: an `/api/chat`\n\t * transport, status, or body failure.\n\t */\n\treadonly code = 'HTTP' as const\n\treadonly status: number\n\n\tconstructor(message: string, status: number, options?: OllamaHTTPErrorOptions) {\n\t\tsuper(message, options)\n\t\tthis.name = 'OllamaHTTPError'\n\t\tthis.status = status\n\t}\n}\n\n/**\n * Checks whether a value is an {@link OllamaHTTPError}.\n *\n * @param value - The value to test\n * @returns True if `value` is an `OllamaHTTPError`; false otherwise\n */\nexport function isOllamaHTTPError(value: unknown): value is OllamaHTTPError {\n\treturn value instanceof OllamaHTTPError\n}\n","// The Ollama wire leaves — the request projections and the response extractions\n// `OllamaProvider` composes. Each is a pure, total function of its parameters: a missing\n// or malformed wire field degrades to a sensible default (empty content, no usage, `{}`\n// arguments), never a throw, and no value is reached through `as`.\n\nimport type { Message, ProviderResult, ThinkSplitterInterface } from '@orkestrel/agent'\nimport type { TokenUsage } from '@orkestrel/budget'\nimport type { ToolCall } from '@orkestrel/tool'\nimport type { WireChatRequest } from './types.js'\nimport { isNumber, isRecord, isString, parseJSONAs } from '@orkestrel/contract'\n\n/**\n * Maps conversation turns onto the `/api/chat` wire's minimal message shape.\n *\n * @remarks\n * `tool_calls` is emitted only on a turn that replays them and `images` only on a\n * multimodal turn, so an empty optional never reaches the wire.\n *\n * @param messages - The conversation turns to send\n * @returns The wire `messages` array, one entry per turn, in order\n *\n * @example\n * ```ts\n * mapMessages([{ id: '1', role: 'user', content: 'Say hello.' }])\n * // [{ role: 'user', content: 'Say hello.' }]\n * ```\n */\nexport function mapMessages(messages: readonly Message[]): WireChatRequest['messages'] {\n\treturn messages.map((message) => ({\n\t\trole: message.role,\n\t\tcontent: message.content,\n\t\t...(message.calls !== undefined && message.calls.length > 0\n\t\t\t? {\n\t\t\t\t\ttool_calls: message.calls.map((call) => ({\n\t\t\t\t\t\tfunction: { name: call.name, arguments: call.arguments },\n\t\t\t\t\t})),\n\t\t\t\t}\n\t\t\t: {}),\n\t\t// Forward multimodal image data — Ollama accepts a base64 `images` array on a\n\t\t// message, which a vision-capable model receives alongside the text content.\n\t\t...(message.images !== undefined && message.images.length > 0\n\t\t\t? { images: [...message.images] }\n\t\t\t: {}),\n\t}))\n}\n\n/**\n * Builds a provider result from a turn's content, reasoning, tool calls, and usage.\n *\n * @remarks\n * Only the present optionals are set: no empty `thinking`, no empty `tools`, and no\n * `usage` unless the wire reported one.\n *\n * @param content - The clean assistant content the splitter accumulated\n * @param thinking - The joined reasoning, empty when the turn produced none\n * @param tools - The tool calls collected across the turn\n * @param usage - The token usage, or `undefined` when the wire reported none\n * @returns The result carrying only its populated fields\n *\n * @example\n * ```ts\n * buildResult('ok', '', [], undefined) // { content: 'ok' }\n * ```\n */\nexport function buildResult(\n\tcontent: string,\n\tthinking: string,\n\ttools: readonly ToolCall[],\n\tusage: TokenUsage | undefined,\n): ProviderResult {\n\tconst result: {\n\t\tcontent: string\n\t\tthinking?: string\n\t\ttools?: readonly ToolCall[]\n\t\tusage?: TokenUsage\n\t} = { content }\n\tif (thinking.length > 0) result.thinking = thinking\n\tif (tools.length > 0) result.tools = tools\n\tif (usage !== undefined) result.usage = usage\n\treturn result\n}\n\n/**\n * Extracts the assistant text of one wire record.\n *\n * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line\n * @returns The record's `message.content` when it is a string, else `''`\n *\n * @example\n * ```ts\n * extractContent({ message: { content: 'ok' } }) // 'ok'\n * ```\n */\nexport function extractContent(record: Readonly<Record<string, unknown>>): string {\n\tconst message = Reflect.get(record, 'message')\n\tif (!isRecord(message)) return ''\n\tconst content = Reflect.get(message, 'content')\n\treturn isString(content) ? content : ''\n}\n\n/**\n * Extracts the daemon-side reasoning of one wire record.\n *\n * @remarks\n * `message.thinking` is the `think: true` wire shape. It is read whatever the configured\n * flag says, because a daemon may separate reasoning on its own.\n *\n * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line\n * @returns The record's `message.thinking` when it is a string, else `''`\n *\n * @example\n * ```ts\n * extractThinking({ message: { thinking: 'weighing it' } }) // 'weighing it'\n * ```\n */\nexport function extractThinking(record: Readonly<Record<string, unknown>>): string {\n\tconst message = Reflect.get(record, 'message')\n\tif (!isRecord(message)) return ''\n\tconst thinking = Reflect.get(message, 'thinking')\n\treturn isString(thinking) ? thinking : ''\n}\n\n/**\n * Joins a call's two reasoning carriers into the result's `thinking`.\n *\n * @param splitter - The per-call splitter holding the separated in-content spans\n * @param wired - The accumulated wire-side `message.thinking` text\n * @returns The two carriers separated by a blank line, or whichever one is non-empty\n *\n * @example\n * ```ts\n * joinThinking(createThinkSplitter(), 'from the wire') // 'from the wire'\n * ```\n */\nexport function joinThinking(splitter: ThinkSplitterInterface, wired: string): string {\n\tif (splitter.thinking.length === 0) return wired\n\tif (wired.length === 0) return splitter.thinking\n\treturn `${splitter.thinking}\\n\\n${wired}`\n}\n\n/**\n * Extracts the token usage of one wire record.\n *\n * @remarks\n * Both counts must be numbers, which is true of the non-stream body and the stream's\n * `done: true` line. A delta line carries neither, so it yields `undefined`.\n *\n * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line\n * @returns The `TokenUsage` shape, or `undefined` when either count is absent\n *\n * @example\n * ```ts\n * extractUsage({ prompt_eval_count: 3, eval_count: 4 })\n * // { prompt: 3, completion: 4, total: 7 }\n * ```\n */\nexport function extractUsage(record: Readonly<Record<string, unknown>>): TokenUsage | undefined {\n\tconst prompt = Reflect.get(record, 'prompt_eval_count')\n\tconst completion = Reflect.get(record, 'eval_count')\n\tif (!isNumber(prompt) || !isNumber(completion)) return undefined\n\treturn { prompt, completion, total: prompt + completion }\n}\n\n/**\n * Extracts the tool calls of one wire record's `message.tool_calls`.\n *\n * @remarks\n * Each entry narrows to `{ id, name, arguments }`: the entry and its `function` must be\n * records and `name` a string, else the entry is dropped. An id is minted when the wire\n * omits one.\n *\n * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line\n * @returns The narrowed tool calls, empty when the record carries none\n *\n * @example\n * ```ts\n * extractTools({ message: { tool_calls: [{ function: { name: 'weather' } }] } })\n * // [{ id: '…', name: 'weather', arguments: {} }]\n * ```\n */\nexport function extractTools(record: Readonly<Record<string, unknown>>): readonly ToolCall[] {\n\tconst message = Reflect.get(record, 'message')\n\tif (!isRecord(message)) return []\n\tconst calls = Reflect.get(message, 'tool_calls')\n\tif (!Array.isArray(calls)) return []\n\tconst out: ToolCall[] = []\n\tfor (const entry of calls) {\n\t\tif (!isRecord(entry)) continue\n\t\tconst callable = Reflect.get(entry, 'function')\n\t\tif (!isRecord(callable)) continue\n\t\tconst name = Reflect.get(callable, 'name')\n\t\tif (!isString(name)) continue\n\t\tconst id = Reflect.get(entry, 'id')\n\t\tout.push({\n\t\t\tid: isString(id) ? id : crypto.randomUUID(),\n\t\t\tname,\n\t\t\targuments: extractArguments(Reflect.get(callable, 'arguments')),\n\t\t})\n\t}\n\treturn out\n}\n\n/**\n * Extracts a wire `arguments` value as a record.\n *\n * @remarks\n * Total: an object passes through, a JSON string is parsed when it yields a record, and\n * a malformed string yields `{}` rather than throwing.\n *\n * @param value - The wire's `function.arguments` value, of unknown shape\n * @returns The argument record, or `{}` when the value carries none\n *\n * @example\n * ```ts\n * extractArguments('{\"city\":\"Oslo\"}') // { city: 'Oslo' }\n * ```\n */\nexport function extractArguments(value: unknown): Readonly<Record<string, unknown>> {\n\tif (isRecord(value)) return value\n\tif (isString(value)) return parseJSONAs(value, isRecord) ?? {}\n\treturn {}\n}\n","// The Ollama response coercer — the non-stream `/api/chat` body read off the wire and\n// coerced to a record inside a total guard, never a raw `SyntaxError`.\n\nimport { isRecord, parseJSONAs } from '@orkestrel/contract'\n\n/**\n * Parses a non-stream `/api/chat` response body into a wire record.\n *\n * @remarks\n * Total by construction: an empty body, a body that is not JSON, and a body whose JSON is\n * not an object all yield `undefined`, so a malformed daemon response never escapes as a\n * `SyntaxError`. The call site supplies the empty-record default that reads as empty\n * content and no usage.\n *\n * @param response - The 200-OK `/api/chat` response whose body is read as text\n * @returns The parsed record, or `undefined` when the body is empty or malformed\n *\n * @example\n * ```ts\n * await parseBody(new Response('{\"message\":{\"content\":\"ok\"}}'))\n * // { message: { content: 'ok' } }\n * ```\n */\nexport async function parseBody(\n\tresponse: Response,\n): Promise<Readonly<Record<string, unknown>> | undefined> {\n\treturn parseJSONAs(await response.text(), isRecord)\n}\n","import type {\n\tContextFormat,\n\tMessage,\n\tProviderDelta,\n\tProviderInterface,\n\tProviderResult,\n\tProviderStreamOptions,\n\tThinkSplitterInterface,\n} from '@orkestrel/agent'\nimport type { TokenUsage } from '@orkestrel/budget'\nimport type { ToolCall, ToolDefinition } from '@orkestrel/tool'\nimport type { OllamaOptions, OllamaResponse, WireChatRequest } from './types.js'\nimport { createThinkSplitter, ProviderAbortError } from '@orkestrel/agent'\nimport { createNDJSONParser } from '@orkestrel/ndjson'\nimport { Timeout } from '@orkestrel/timeout'\nimport {\n\tDEFAULT_KEEP_ALIVE,\n\tDEFAULT_OLLAMA_URL,\n\tDEFAULT_PROVIDER_TIMEOUT,\n\tMAX_ERROR_BODY_LENGTH,\n} from './constants.js'\nimport { OllamaHTTPError } from './errors.js'\nimport {\n\tbuildResult,\n\textractContent,\n\textractThinking,\n\textractTools,\n\textractUsage,\n\tjoinThinking,\n\tmapMessages,\n} from './helpers.js'\nimport { parseBody } from './parsers.js'\n\n/**\n * Implements the local Ollama inference boundary — a {@link ProviderInterface} over Ollama's\n * `POST /api/chat`, both non-streaming (`generate`) and streaming NDJSON (`stream`).\n *\n * @remarks\n * - **Wire protocol.** Posts `{ model, messages, stream, keep_alive, think }` plus\n * passthrough sampling `options` and mapped function `tools`. The `think` flag is\n * CONFIGURABLE through {@link OllamaOptions.think} (default `false`). Non-stream parses\n * one JSON body; stream consumes NDJSON (one JSON object per `\\n`-terminated line) —\n * deltas carry `message.content`, the final `done: true` line carries the token usage.\n * - **Think separation.** The wire `think` flag is configurable\n * ({@link OllamaOptions.think}, default `false`). With `think: true` a thinking model's\n * daemon separates reasoning NATIVELY — returning it on the distinct `message.thinking`\n * channel (read here through `extractThinking`) instead of inline in `message.content`. EITHER\n * way the per-call {@link ThinkSplitterInterface} is the defensive guarantee: a daemon\n * may ignore `think: false` for a thinking model and inline `<think>` tags, so every\n * content delta routes through the splitter, only CLEAN content is yielded / assembled,\n * and the separated reasoning (plus any daemon-side `message.thinking` deltas) lands on\n * `ProviderResult.thinking`, never in the conversation.\n * - **Boundary narrowing.** Every wire value arrives as `unknown` and is\n * narrowed through guards (`isRecord` / `isString` / `isNumber`) — never `as`. A\n * missing / malformed field degrades to a sensible default (empty content, no\n * usage, `{}` arguments), never a throw.\n * - **Bounded.** Each call arms a {@link Timeout} for `OllamaOptions.timeout` and\n * passes `AbortSignal.any([timeout.signal, signal])` to `fetch`, so the caller's\n * signal AND the deadline both cancel the request. The timeout is always cleared —\n * in `#fetch` if the request fails/aborts, otherwise in the consuming call's `finally`.\n * - **Abort recovers partial.** A `stream` cancelled mid-flight throws a\n * `ProviderAbortError` carrying the partial result assembled so far; pairing the\n * `TextDecoder({ stream: true })` with the {@link NDJSONParser} parser keeps multi-byte\n * UTF-8 splits and partial lines honest.\n * - **Event-free.** A pure functional boundary — no Emitter, no events.\n * - **Transport seam.** {@link OllamaOptions.fetch} swaps the transport (default\n * `globalThis.fetch`) and {@link OllamaOptions.headers} is a per-request, possibly\n * async header injector merged over the base `Content-Type` — so a browser runtime\n * can route through the developer's own server with an obfuscated bearer token,\n * without this library ever handling a real API key. Both omitted ⇒ the global `fetch`\n * and only a JSON content type.\n * Orthogonal to the deadline: the hook is awaited inside `#fetch`'s try, so a hook\n * rejection clears the armed timer like any other request failure.\n *\n * @example\n * ```ts\n * const provider = new OllamaProvider({ model: 'qwen3.5:2b-q4_K_M' })\n * const result = await provider.generate(messages, abort.signal)\n * ```\n */\nexport class OllamaProvider implements ProviderInterface {\n\treadonly name = 'ollama'\n\treadonly #id: string\n\treadonly #model: string\n\treadonly #url: string\n\treadonly #keepAlive: string | number\n\treadonly #timeout: number\n\treadonly #think: boolean\n\treadonly #options: Readonly<Record<string, unknown>> | undefined\n\treadonly #transport: typeof globalThis.fetch\n\treadonly #headers:\n\t\t| (() => Readonly<Record<string, string>> | Promise<Readonly<Record<string, string>>>)\n\t\t| undefined\n\treadonly #format: ContextFormat | undefined\n\n\tconstructor(options: OllamaOptions) {\n\t\tthis.#id = crypto.randomUUID()\n\t\tthis.#model = options.model\n\t\tthis.#url = options.url ?? DEFAULT_OLLAMA_URL\n\t\tthis.#keepAlive = options.keepAlive ?? DEFAULT_KEEP_ALIVE\n\t\tthis.#timeout = options.timeout ?? DEFAULT_PROVIDER_TIMEOUT\n\t\t// The `/api/chat` `think` wire flag — DEFAULT `false`, so a non-thinking model needs no\n\t\t// configuration and answers immediately. A thinking model whose reasoning is DISPLAYED\n\t\t// separately sets `think: true`, and the daemon then returns it on the\n\t\t// `message.thinking` channel (`extractThinking`) rather than inline in `message.content`.\n\t\tthis.#think = options.think ?? false\n\t\tthis.#options = options.options\n\t\t// The transport seam: a custom fetch (defaulting to the global, BOUND to its\n\t\t// `globalThis` receiver — invoking a bare reference through a field loses the `window`\n\t\t// receiver and browsers throw `Illegal invocation`; node's fetch is receiver-agnostic,\n\t\t// so only a browser runtime ever saw it) and a dynamic header injector — both omitted\n\t\t// by default, so the request goes out over the global fetch carrying only the JSON\n\t\t// content type. The injected transport is `#transport` (the request METHOD already\n\t\t// owns the `#fetch` name).\n\t\tthis.#transport = options.fetch ?? globalThis.fetch.bind(globalThis)\n\t\tthis.#headers = options.headers\n\t\t// The context-framing default (the provider-DEFAULT level of AgentContext's format\n\t\t// cascade) — EXPOSE-ONLY: read by the Agent through `build(this.#provider.format)` and\n\t\t// consumed by core's cascade, it NEVER enters `#body` / the `/api/chat` wire. It is\n\t\t// NOT Ollama's structured-output `format` wire param — that one IS sent in `#body`,\n\t\t// but only when a per-call `ProviderStreamOptions.schema` is supplied; the two\n\t\t// merely share a word. Omitted ⇒ undefined ⇒ core's built-in framing.\n\t\tthis.#format = options.format\n\t}\n\n\t/**\n\t * Exposes this instance's identity — a fresh `crypto.randomUUID()` minted at\n\t * construction, satisfying the {@link ProviderInterface.id} contract member. A second\n\t * provider built from identical options carries a distinct id.\n\t *\n\t * @returns The instance's minted identifier\n\t */\n\tget id(): string {\n\t\treturn this.#id\n\t}\n\n\t/**\n\t * Exposes the provider's context-framing default — the PROVIDER-DEFAULT level of\n\t * {@link import('@orkestrel/agent').AgentContextInterface.build}'s format cascade (it BEATS\n\t * the managers' built-in framing, is BEATEN by a manager-options or per-item override).\n\t * Satisfies the OPTIONAL {@link ProviderInterface.format} contract member: `undefined`\n\t * when {@link OllamaOptions.format} was omitted (the framing-agnostic default ⇒ core's\n\t * built-in framing applies unchanged), else the exact configured framing the Agent\n\t * threads into `build()`.\n\t *\n\t * @remarks\n\t * EXPOSE-ONLY — read by the Agent loop and consumed by core's cascade; it is NEVER sent\n\t * on the `/api/chat` wire (it is absent from `#body` / the request). This is NOT Ollama's\n\t * structured-output `format` wire parameter — that one IS sent in `#body`, but only when\n\t * a per-call `ProviderStreamOptions.schema` is supplied; only the word collides.\n\t *\n\t * @returns The configured {@link ContextFormat}, or `undefined` when none\n\t */\n\tget format(): ContextFormat | undefined {\n\t\treturn this.#format\n\t}\n\n\tasync generate(\n\t\tmessages: readonly Message[],\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): Promise<ProviderResult> {\n\t\tconst { response, timeout } = await this.#fetch(messages, false, signal, tools, options)\n\t\ttry {\n\t\t\tconst record = (await parseBody(response)) ?? {}\n\t\t\t// The one-body call routes through the SAME splitter as the stream (the daemon may\n\t\t\t// ignore `think: false` — the splitter is the guarantee): the assembled content is\n\t\t\t// CLEAN (the splitter's authoritative `content`, which also covers the qwen3\n\t\t\t// template's IMPLICIT leading open), the separated spans + any wire-side\n\t\t\t// `message.thinking` land on `thinking`.\n\t\t\tconst splitter = createThinkSplitter()\n\t\t\tsplitter.split(extractContent(record))\n\t\t\tsplitter.flush()\n\t\t\tconst thinking = joinThinking(splitter, extractThinking(record))\n\t\t\treturn buildResult(splitter.content, thinking, extractTools(record), extractUsage(record))\n\t\t} finally {\n\t\t\ttimeout.clear()\n\t\t}\n\t}\n\n\tasync *stream(\n\t\tmessages: readonly Message[],\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): AsyncGenerator<ProviderDelta, ProviderResult> {\n\t\tconst { response, timeout, combined } = await this.#fetch(\n\t\t\tmessages,\n\t\t\ttrue,\n\t\t\tsignal,\n\t\t\ttools,\n\t\t\toptions,\n\t\t)\n\t\tconst body = response.body\n\t\tif (body === null) {\n\t\t\ttimeout.clear()\n\t\t\tthrow new OllamaHTTPError('Ollama API error: no response body', 0)\n\t\t}\n\t\tconst reader = body.getReader()\n\t\tconst decoder = new TextDecoder()\n\t\tconst parser = createNDJSONParser()\n\t\t// The per-call think separator: every wire content delta routes through it, so\n\t\t// only CLEAN content is yielded / assembled even when the daemon ignores `think: false`\n\t\t// for a thinking model; daemon-side `message.thinking` deltas accumulate beside it.\n\t\t// The ASSEMBLED content is the splitter's authoritative `content` — across the qwen3\n\t\t// template's IMPLICIT leading open (a bare `</think>` with the open pre-seeded into the\n\t\t// prompt scaffold) the splitter RECLASSIFIES the already-yielded prefix into `thinking`,\n\t\t// so the result stays clean even though those deltas could not be recalled.\n\t\tconst splitter = createThinkSplitter()\n\t\t// The per-stream accumulators, folded from every `#deltas` return across the live\n\t\t// loop and the post-loop NDJSON tail flush following.\n\t\tlet wired = ''\n\t\tconst calls: ToolCall[] = []\n\t\tlet usage: TokenUsage | undefined\n\t\ttry {\n\t\t\tfor (;;) {\n\t\t\t\tconst { value, done } = await reader.read()\n\t\t\t\tif (done) break\n\t\t\t\t// Pair the streaming decoder with the line parser: the decoder handles\n\t\t\t\t// partial multi-byte CHARS, the parser handles partial LINES.\n\t\t\t\tfor (const record of parser.parse(decoder.decode(value, { stream: true }))) {\n\t\t\t\t\tconst increment = yield* this.#deltas(record, splitter, usage)\n\t\t\t\t\twired += increment.thinking\n\t\t\t\t\tcalls.push(...increment.calls)\n\t\t\t\t\tusage = increment.usage\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Flush the decoder's held partial multi-byte tail and feed it (plus a\n\t\t\t// terminating `\\n`) through the parser, so a non-conformant proxy's final\n\t\t\t// unterminated `done` line is recovered instead of silently dropped.\n\t\t\tconst decoderTail = decoder.decode()\n\t\t\tfor (const record of parser.parse(decoderTail.length > 0 ? `${decoderTail}\\n` : '\\n')) {\n\t\t\t\tconst increment = yield* this.#deltas(record, splitter, usage)\n\t\t\t\twired += increment.thinking\n\t\t\t\tcalls.push(...increment.calls)\n\t\t\t\tusage = increment.usage\n\t\t\t}\n\t\t\t// Stream end: a held partial tag that never completed was real content — it is the\n\t\t\t// final delta (the splitter folds it into its `content` too).\n\t\t\tconst tail = splitter.flush()\n\t\t\tif (tail.length > 0) yield { channel: 'content', text: tail }\n\t\t} catch (error) {\n\t\t\t// A mid-stream cancel (the caller's signal or the deadline) surfaces the\n\t\t\t// partial so the loop can recover what streamed; anything else propagates.\n\t\t\tif (combined.aborted) {\n\t\t\t\t// Flush the splitter's held partial tail first (mirrors the\n\t\t\t\t// normal-completion assembly preceding) so the recovered partial includes\n\t\t\t\t// any clean content that never crossed a tag boundary.\n\t\t\t\tsplitter.flush()\n\t\t\t\tthrow new ProviderAbortError(\n\t\t\t\t\tbuildResult(splitter.content, joinThinking(splitter, wired), calls, usage),\n\t\t\t\t)\n\t\t\t}\n\t\t\tthrow error\n\t\t} finally {\n\t\t\t// Cancel (not merely release) the reader on early return so the\n\t\t\t// underlying HTTP connection is freed; a normal-done or already-errored\n\t\t\t// reader tolerates the redundant cancel as a no-op. `cancel()` also\n\t\t\t// releases the lock — never call `releaseLock()` afterward.\n\t\t\ttry {\n\t\t\t\tawait reader.cancel()\n\t\t\t} catch {\n\t\t\t\t// Never mask the primary error/result with a cancel failure.\n\t\t\t}\n\t\t\tparser.clear()\n\t\t\ttimeout.clear()\n\t\t}\n\t\treturn buildResult(splitter.content, joinThinking(splitter, wired), calls, usage)\n\t}\n\n\t// Per-record streaming step shared between the live NDJSON loop and the post-loop\n\t// tail flush in `stream()` — a `#` private method (not a free helper) because it is\n\t// the streaming spine that composes the wire leaves and drives the splitter, and\n\t// because its yields are the stream's own. It mutates nothing: it RETURNS the record's\n\t// increments (`thinking` / `calls` / `usage`) and `stream()` folds them, so the\n\t// accumulator's shape is written once, here.\n\t*#deltas(\n\t\trecord: Readonly<Record<string, unknown>>,\n\t\tsplitter: ThinkSplitterInterface,\n\t\tusage: TokenUsage | undefined,\n\t): Generator<\n\t\tProviderDelta,\n\t\t{\n\t\t\treadonly thinking: string\n\t\t\treadonly calls: readonly ToolCall[]\n\t\t\treadonly usage: TokenUsage | undefined\n\t\t}\n\t> {\n\t\tconst delta = splitter.split(extractContent(record))\n\t\tif (delta.length > 0) yield { channel: 'content', text: delta }\n\t\t// The PRIMARY live reasoning channel: each native `message.thinking` wire delta is\n\t\t// surfaced as a tagged `thinking` delta AND returned for the caller's `wired`\n\t\t// accumulation (the two stay in lockstep). The ThinkSplitter's in-content\n\t\t// reclassified spans have no per-delta hook — the final `ProviderResult.thinking`\n\t\t// reconciles them; the native channel (think: true) is what streams live.\n\t\tconst thinking = extractThinking(record)\n\t\tif (thinking.length > 0) yield { channel: 'thinking', text: thinking }\n\t\t// Only the `done` line carries usage, so every other record hands the caller's\n\t\t// current value straight back rather than clearing it.\n\t\treturn {\n\t\t\tthinking,\n\t\t\tcalls: extractTools(record),\n\t\t\tusage: Reflect.get(record, 'done') === true ? extractUsage(record) : usage,\n\t\t}\n\t}\n\n\t// Arm the deadline, POST `/api/chat`, and hand back the response + the handles\n\t// that bound it. On a non-OK status, clear the deadline and throw with the body.\n\tasync #fetch(\n\t\tmessages: readonly Message[],\n\t\tstream: boolean,\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): Promise<OllamaResponse> {\n\t\tconst timeout = new Timeout({ ms: this.#timeout })\n\t\ttimeout.start()\n\t\tconst combined = AbortSignal.any([timeout.signal, signal])\n\t\ttry {\n\t\t\tconst response = await this.#transport(`${this.#url}/api/chat`, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: await this.#requestHeaders(),\n\t\t\t\tbody: JSON.stringify(this.#body(messages, stream, tools, options)),\n\t\t\t\tsignal: combined,\n\t\t\t})\n\t\t\tif (!response.ok) {\n\t\t\t\t// Bound the incorporated body: a defensive proxy or daemon could hand\n\t\t\t\t// back an unbounded response — read defensively so a body-read\n\t\t\t\t// failure still throws with the status, never a masked/unbounded read.\n\t\t\t\tlet detail: string\n\t\t\t\ttry {\n\t\t\t\t\tconst text = await response.text()\n\t\t\t\t\tdetail = text.length > MAX_ERROR_BODY_LENGTH ? text.slice(0, MAX_ERROR_BODY_LENGTH) : text\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tthrow new OllamaHTTPError(\n\t\t\t\t\t\t`Ollama API error: ${response.status} - (error body unavailable)`,\n\t\t\t\t\t\tresponse.status,\n\t\t\t\t\t\t{ cause },\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthrow new OllamaHTTPError(\n\t\t\t\t\t`Ollama API error: ${response.status} - ${detail}`,\n\t\t\t\t\tresponse.status,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn { response, timeout, combined }\n\t\t} catch (error) {\n\t\t\t// `fetch` rejected (pre-aborted signal / unreachable / network) or the status\n\t\t\t// was non-OK — clear the deadline so the armed timer can't outlive the failed\n\t\t\t// call. The caller's `finally` only takes ownership once `#fetch` returns a response.\n\t\t\ttimeout.clear()\n\t\t\tthrow error\n\t\t}\n\t}\n\n\t// The request headers — the base JSON content type, plus the dynamic `headers`\n\t// hook's result merged ON TOP when configured (so a dev can attach an obfuscated\n\t// bearer the server validates). Merge order: `Content-Type` is seeded first, then\n\t// the hook's entries overlay it — so the hook ADDS auth headers but only clobbers\n\t// `Content-Type` if the dev explicitly returns one. Awaited (the hook may be async,\n\t// for example refreshing a token); called inside `#fetch`'s try so a hook rejection\n\t// clears the armed deadline like any other request failure. The hook's result is a\n\t// `Readonly<Record<string, string>>` already — merged through `Object.entries`, no `as`.\n\tasync #requestHeaders(): Promise<Record<string, string>> {\n\t\tconst headers: Record<string, string> = { 'Content-Type': 'application/json' }\n\t\tif (this.#headers !== undefined) {\n\t\t\tfor (const [key, value] of Object.entries(await this.#headers())) headers[key] = value\n\t\t}\n\t\treturn headers\n\t}\n\n\t// The `/api/chat` request body — conditional `options` / `tools` / `format` only when set. The\n\t// wire `think` flag honours a PER-CALL override (`options.think`) over the constructor default\n\t// (`#think`), so a caller can flip reasoning on / off for one turn without reconfiguring the\n\t// provider; no per-call option ⇒ the constructed default.\n\t// `format` is the wire's structured-output constraint, forwarded verbatim from the per-call\n\t// `ProviderStreamOptions.schema` — unrelated to `OllamaOptions.format` (prompt-context framing).\n\t#body(\n\t\tmessages: readonly Message[],\n\t\tstream: boolean,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): WireChatRequest {\n\t\treturn {\n\t\t\tmodel: this.#model,\n\t\t\tmessages: mapMessages(messages),\n\t\t\tstream,\n\t\t\tkeep_alive: this.#keepAlive,\n\t\t\tthink: options?.think ?? this.#think,\n\t\t\t...(this.#options !== undefined ? { options: this.#options } : {}),\n\t\t\t...(options?.schema !== undefined ? { format: options.schema } : {}),\n\t\t\t...(tools !== undefined && tools.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\ttools: tools.map((tool): NonNullable<WireChatRequest['tools']>[number] => ({\n\t\t\t\t\t\t\ttype: 'function',\n\t\t\t\t\t\t\tfunction: {\n\t\t\t\t\t\t\t\tname: tool.name,\n\t\t\t\t\t\t\t\t...(tool.description === undefined ? {} : { description: tool.description }),\n\t\t\t\t\t\t\t\t...(tool.parameters === undefined ? {} : { parameters: tool.parameters }),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t}\n\t}\n}\n","import type { ProviderInterface } from '@orkestrel/agent'\nimport type { OllamaOptions } from './types.js'\nimport { OllamaProvider } from './OllamaProvider.js'\n\n/**\n * Creates a local Ollama inference provider — a {@link ProviderInterface} over the\n * daemon's `POST /api/chat`, supporting non-streaming `generate` and streaming\n * `stream`.\n *\n * @remarks\n * Only `model` is required; `url` defaults to the local daemon, `keepAlive` to `'5m'`,\n * `timeout` to `120_000`ms, and `options` is forwarded verbatim as sampling\n * parameters (`temperature`, `seed`, and `num_predict`). Both calls take an\n * `AbortSignal` to bound the request; a `stream` cancelled mid-flight throws a\n * `ProviderAbortError` carrying the partial result.\n *\n * The optional `fetch` + `headers` form a transport seam (see {@link OllamaOptions}):\n * point `url` at your own server, inject a custom `fetch`, and have `headers` attach a\n * generated/obfuscated bearer token your server validates — so a browser runtime\n * reaches the LLM through your middleware WITHOUT this library ever handling the real API\n * key. Both omitted ⇒ the global `fetch` and only a JSON content type.\n *\n * The optional `format` is the provider's context-framing default — the PROVIDER-DEFAULT\n * level of `AgentContext`'s format cascade (beaten by a manager-options or per-item\n * override, beating the managers' built-in framing), declaring how this\n * provider's models prefer context sections framed (for example XML group wrappers vs. Markdown\n * headers). It is EXPOSED on the provider for the Agent's `build()` and is NOT Ollama's\n * `/api/chat` `format` wire parameter (structured output) — the two are unrelated despite\n * the shared word. Omitted ⇒ the provider is framing-agnostic (core's built-in defaults).\n *\n * @param options - `model` (required), and optional `url` / `keepAlive` / `timeout` /\n * `options` / `fetch` / `headers` / `format` (see {@link OllamaOptions})\n * @returns A working {@link ProviderInterface} backed by Ollama\n *\n * @example\n * ```ts\n * import { createAbort } from '@orkestrel/abort'\n * import { createOllama } from '@orkestrel/ollama'\n *\n * const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M' })\n * const abort = createAbort()\n * const result = await provider.generate(messages, abort.signal)\n * ```\n *\n * @example\n * Route through your own server with an obfuscated token:\n * ```ts\n * const provider = createOllama({\n * model: 'qwen3.5:2b-q4_K_M',\n * url: 'https://my-app.example.com/llm', // your server, not the daemon\n * fetch: myFetch, // optional custom transport\n * headers: () => ({ authorization: `Bearer ${myToken}` }), // your server validates this\n * })\n * ```\n *\n * @example\n * Declare a context-framing default — wrap the instructions section in an XML group (the\n * provider-default level of `AgentContext`'s cascade; NOT the wire `format`):\n * ```ts\n * const provider = createOllama({\n * model: 'qwen3.5:2b-q4_K_M',\n * format: {\n * instructions: {\n * open: '<instructions>',\n * render: (i) => `<instruction>${i.content}</instruction>`,\n * close: '</instructions>',\n * },\n * },\n * })\n * ```\n */\nexport function createOllama(options: OllamaOptions): ProviderInterface {\n\treturn new OllamaProvider(options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["#id","#model","#url","#keepAlive","#timeout","#think","#options","#transport","#headers","#format","#fetch","#deltas","#requestHeaders","#body"],"sources":["../../../src/server/constants.ts","../../../src/server/errors.ts","../../../src/server/helpers.ts","../../../src/server/parsers.ts","../../../src/server/OllamaProvider.ts","../../../src/server/factories.ts"],"sourcesContent":["// Ollama constants — the provider's defaults.\n\n/**\n * Names the local Ollama daemon base URL, `'http://localhost:11434'`, assumed when\n * `OllamaOptions.url` is omitted.\n */\nexport const DEFAULT_OLLAMA_URL = 'http://localhost:11434'\n\n/**\n * Names how long the model stays resident after a call — `'5m'` when\n * `OllamaOptions.keepAlive` is omitted, Ollama's own `keep_alive` default, expressed as a\n * duration string.\n *\n * @remarks\n * The name mirrors the Ollama `/api/chat` `keep_alive` field this value is sent as, so\n * the constant, the `OllamaOptions.keepAlive` key, and the wire member read as one term.\n */\nexport const DEFAULT_KEEP_ALIVE = '5m'\n\n/**\n * Names the per-call deadline in milliseconds, `120_000`, when `OllamaOptions.timeout` is\n * omitted — generous enough that a cold model load does not trip it.\n */\nexport const DEFAULT_PROVIDER_TIMEOUT = 120_000\n\n/**\n * Names the character cap, `2048`, on how much of a non-OK response body is\n * incorporated into a thrown {@link OllamaHTTPError}'s message.\n *\n * @remarks\n * Bounds the excerpt so a defensive proxy or a misbehaving daemon handing\n * back an unbounded response body cannot inflate the thrown error's message\n * without limit, while the cap stays generous enough to carry a useful\n * diagnostic snippet.\n */\nexport const MAX_ERROR_BODY_LENGTH = 2048\n","// Errors for the Ollama provider. A single `OllamaHTTPError` carries the\n// `/api/chat` HTTP status at the boundary — non-OK responses and a missing\n// response body both throw it — so a `catch` can branch on `error.status`\n// rather than parsing a message.\n\nimport type { OllamaHTTPErrorOptions } from './types.js'\n\n/**\n * Represents an error thrown when the Ollama `/api/chat` HTTP transport fails.\n *\n * @remarks\n * Carries the machine-readable `code` `'HTTP'` and the response `status` (0 when no\n * HTTP response was received at all, for example a `null` body). Thrown by\n * {@link OllamaProvider} at its HTTP failure sites — the non-OK status branch and the\n * null-body branch — so a caller can branch on `error.code` and read `error.status`\n * for the HTTP number instead of parsing the message. The message carries a body excerpt\n * bounded to {@link MAX_ERROR_BODY_LENGTH} — `2048` characters. Narrow a caught value with\n * {@link isOllamaHTTPError}.\n *\n * @example\n * ```ts\n * try {\n * \tawait provider.generate(messages, signal)\n * } catch (error) {\n * \tif (isOllamaHTTPError(error) && error.status === 404) {\n * \t\t// the configured model isn't pulled\n * \t}\n * }\n * ```\n */\nexport class OllamaHTTPError extends Error {\n\t/**\n\t * Names the machine-readable condition this error reports — `'HTTP'`: an `/api/chat`\n\t * transport, status, or body failure.\n\t */\n\treadonly code = 'HTTP' as const\n\treadonly status: number\n\n\tconstructor(message: string, status: number, options?: OllamaHTTPErrorOptions) {\n\t\tsuper(message, options)\n\t\tthis.name = 'OllamaHTTPError'\n\t\tthis.status = status\n\t}\n}\n\n/**\n * Checks whether a value is an {@link OllamaHTTPError}.\n *\n * @remarks\n * The check is an `instanceof` test, so it narrows a caught `unknown` to the error class\n * without parsing the thrown message.\n *\n * @param value - The value to test\n * @returns True if `value` is an `OllamaHTTPError`; false otherwise\n */\nexport function isOllamaHTTPError(value: unknown): value is OllamaHTTPError {\n\treturn value instanceof OllamaHTTPError\n}\n","// The Ollama wire leaves — the request projections and the response extractions\n// `OllamaProvider` composes. Each is a pure, total function of its parameters: a missing\n// or malformed wire field degrades to a sensible default (empty content, no usage, `{}`\n// arguments), never a throw, and no value is reached through `as`.\n\nimport type { Message, ProviderResult, ThinkSplitterInterface } from '@orkestrel/agent'\nimport type { TokenUsage } from '@orkestrel/budget'\nimport type { ToolCall } from '@orkestrel/tool'\nimport type { WireChatRequest } from './types.js'\nimport { isNumber, isRecord, isString, parseJSONAs } from '@orkestrel/contract'\n\n/**\n * Maps conversation turns onto the `/api/chat` wire's minimal message shape.\n *\n * @remarks\n * `tool_calls` is emitted only on a turn that replays them and `images` only on a\n * multimodal turn, so an empty optional never reaches the wire.\n *\n * @param messages - The conversation turns to send\n * @returns The wire `messages` array, one entry per turn, in order\n *\n * @example\n * ```ts\n * mapMessages([{ id: '1', role: 'user', content: 'Say hello.' }])\n * // [{ role: 'user', content: 'Say hello.' }]\n * ```\n */\nexport function mapMessages(messages: readonly Message[]): WireChatRequest['messages'] {\n\treturn messages.map((message) => ({\n\t\trole: message.role,\n\t\tcontent: message.content,\n\t\t...(message.calls !== undefined && message.calls.length > 0\n\t\t\t? {\n\t\t\t\t\ttool_calls: message.calls.map((call) => ({\n\t\t\t\t\t\tfunction: { name: call.name, arguments: call.arguments },\n\t\t\t\t\t})),\n\t\t\t\t}\n\t\t\t: {}),\n\t\t// Forward multimodal image data — Ollama accepts a base64 `images` array on a\n\t\t// message, which a vision-capable model receives alongside the text content.\n\t\t...(message.images !== undefined && message.images.length > 0\n\t\t\t? { images: [...message.images] }\n\t\t\t: {}),\n\t}))\n}\n\n/**\n * Builds a `ProviderResult` from a turn's content, reasoning, tool calls, and usage.\n *\n * @remarks\n * Only the present optionals are set: no empty `thinking`, no empty `tools`, and no\n * `usage` unless the wire reported one.\n *\n * @param content - The clean assistant content the splitter accumulated\n * @param thinking - The joined reasoning, empty when the turn produced none\n * @param tools - The tool calls collected across the turn\n * @param usage - The token usage, or `undefined` when the wire reported none\n * @returns The result carrying only its populated fields\n *\n * @example\n * ```ts\n * buildResult('ok', '', [], undefined) // { content: 'ok' }\n * ```\n */\nexport function buildResult(\n\tcontent: string,\n\tthinking: string,\n\ttools: readonly ToolCall[],\n\tusage: TokenUsage | undefined,\n): ProviderResult {\n\tconst result: {\n\t\tcontent: string\n\t\tthinking?: string\n\t\ttools?: readonly ToolCall[]\n\t\tusage?: TokenUsage\n\t} = { content }\n\tif (thinking.length > 0) result.thinking = thinking\n\tif (tools.length > 0) result.tools = tools\n\tif (usage !== undefined) result.usage = usage\n\treturn result\n}\n\n/**\n * Extracts the assistant text of one wire record.\n *\n * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line\n * @returns The record's `message.content` when it is a string, else `''`\n *\n * @example\n * ```ts\n * extractContent({ message: { content: 'ok' } }) // 'ok'\n * ```\n */\nexport function extractContent(record: Readonly<Record<string, unknown>>): string {\n\tconst message = Reflect.get(record, 'message')\n\tif (!isRecord(message)) return ''\n\tconst content = Reflect.get(message, 'content')\n\treturn isString(content) ? content : ''\n}\n\n/**\n * Extracts the daemon-side reasoning of one wire record.\n *\n * @remarks\n * `message.thinking` is the `think: true` wire shape. It is read whatever the configured\n * flag says, because a daemon may separate reasoning on its own.\n *\n * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line\n * @returns The record's `message.thinking` when it is a string, else `''`\n *\n * @example\n * ```ts\n * extractThinking({ message: { thinking: 'weighing it' } }) // 'weighing it'\n * ```\n */\nexport function extractThinking(record: Readonly<Record<string, unknown>>): string {\n\tconst message = Reflect.get(record, 'message')\n\tif (!isRecord(message)) return ''\n\tconst thinking = Reflect.get(message, 'thinking')\n\treturn isString(thinking) ? thinking : ''\n}\n\n/**\n * Joins a call's reasoning carriers — the splitter's separated in-content spans and the\n * accumulated wire-side `message.thinking` — into the result's `thinking`.\n *\n * @param splitter - The per-call splitter holding the separated in-content spans\n * @param wired - The accumulated wire-side `message.thinking` text\n * @returns The carriers separated by a blank line, or whichever one is non-empty\n *\n * @example\n * ```ts\n * joinThinking(createThinkSplitter(), 'from the wire') // 'from the wire'\n * ```\n */\nexport function joinThinking(splitter: ThinkSplitterInterface, wired: string): string {\n\tif (splitter.thinking.length === 0) return wired\n\tif (wired.length === 0) return splitter.thinking\n\treturn `${splitter.thinking}\\n\\n${wired}`\n}\n\n/**\n * Extracts the token usage of one wire record.\n *\n * @remarks\n * Both counts must be numbers, which is true of the non-stream body and the stream's\n * `done: true` line. A delta line carries neither, so it yields `undefined`.\n *\n * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line\n * @returns The `TokenUsage` shape, or `undefined` when either count is absent\n *\n * @example\n * ```ts\n * extractUsage({ prompt_eval_count: 3, eval_count: 4 })\n * // { prompt: 3, completion: 4, total: 7 }\n * ```\n */\nexport function extractUsage(record: Readonly<Record<string, unknown>>): TokenUsage | undefined {\n\tconst prompt = Reflect.get(record, 'prompt_eval_count')\n\tconst completion = Reflect.get(record, 'eval_count')\n\tif (!isNumber(prompt) || !isNumber(completion)) return undefined\n\treturn { prompt, completion, total: prompt + completion }\n}\n\n/**\n * Extracts the tool calls of one wire record's `message.tool_calls`.\n *\n * @remarks\n * Each entry narrows to `{ id, name, arguments }`: the entry and its `function` must be\n * records and `name` a string, else the entry is dropped. An id is minted when the wire\n * omits one.\n *\n * @param record - One parsed `/api/chat` record — a non-stream body or an NDJSON line\n * @returns The narrowed tool calls, empty when the record carries none\n *\n * @example\n * ```ts\n * extractTools({ message: { tool_calls: [{ function: { name: 'weather' } }] } })\n * // [{ id: '…', name: 'weather', arguments: {} }]\n * ```\n */\nexport function extractTools(record: Readonly<Record<string, unknown>>): readonly ToolCall[] {\n\tconst message = Reflect.get(record, 'message')\n\tif (!isRecord(message)) return []\n\tconst calls = Reflect.get(message, 'tool_calls')\n\tif (!Array.isArray(calls)) return []\n\tconst out: ToolCall[] = []\n\tfor (const entry of calls) {\n\t\tif (!isRecord(entry)) continue\n\t\tconst callable = Reflect.get(entry, 'function')\n\t\tif (!isRecord(callable)) continue\n\t\tconst name = Reflect.get(callable, 'name')\n\t\tif (!isString(name)) continue\n\t\tconst id = Reflect.get(entry, 'id')\n\t\tout.push({\n\t\t\tid: isString(id) ? id : crypto.randomUUID(),\n\t\t\tname,\n\t\t\targuments: extractArguments(Reflect.get(callable, 'arguments')),\n\t\t})\n\t}\n\treturn out\n}\n\n/**\n * Extracts a wire `arguments` value as a record.\n *\n * @remarks\n * Total: an object passes through, a JSON string is parsed when it yields a record, and\n * a malformed string yields `{}` rather than throwing.\n *\n * @param value - The wire's `function.arguments` value, of unknown shape\n * @returns The argument record, or `{}` when the value carries none\n *\n * @example\n * ```ts\n * extractArguments('{\"city\":\"Oslo\"}') // { city: 'Oslo' }\n * ```\n */\nexport function extractArguments(value: unknown): Readonly<Record<string, unknown>> {\n\tif (isRecord(value)) return value\n\tif (isString(value)) return parseJSONAs(value, isRecord) ?? {}\n\treturn {}\n}\n","// The Ollama response coercer — the non-stream `/api/chat` body read off the wire and\n// coerced to a record inside a total guard, never a raw `SyntaxError`.\n\nimport { isRecord, parseJSONAs } from '@orkestrel/contract'\n\n/**\n * Parses a non-stream `/api/chat` response body into a wire record.\n *\n * @remarks\n * Total by construction: an empty body, a body that is not JSON, and a body whose JSON is\n * not an object all yield `undefined`, so a malformed daemon response never escapes as a\n * `SyntaxError`. The call site supplies the empty-record default that reads as empty\n * content and no usage.\n *\n * @param response - The 200-OK `/api/chat` response whose body is read as text\n * @returns The parsed record, or `undefined` when the body is empty or malformed\n *\n * @example\n * ```ts\n * await parseBody(new Response('{\"message\":{\"content\":\"ok\"}}'))\n * // { message: { content: 'ok' } }\n * ```\n */\nexport async function parseBody(\n\tresponse: Response,\n): Promise<Readonly<Record<string, unknown>> | undefined> {\n\treturn parseJSONAs(await response.text(), isRecord)\n}\n","import type {\n\tContextFormat,\n\tMessage,\n\tProviderDelta,\n\tProviderInterface,\n\tProviderResult,\n\tProviderStreamOptions,\n\tThinkSplitterInterface,\n} from '@orkestrel/agent'\nimport type { TokenUsage } from '@orkestrel/budget'\nimport type { ToolCall, ToolDefinition } from '@orkestrel/tool'\nimport type { OllamaOptions, OllamaResponse, WireChatRequest } from './types.js'\nimport { createThinkSplitter, ProviderAbortError } from '@orkestrel/agent'\nimport { createNDJSONParser } from '@orkestrel/ndjson'\nimport { Timeout } from '@orkestrel/timeout'\nimport {\n\tDEFAULT_KEEP_ALIVE,\n\tDEFAULT_OLLAMA_URL,\n\tDEFAULT_PROVIDER_TIMEOUT,\n\tMAX_ERROR_BODY_LENGTH,\n} from './constants.js'\nimport { OllamaHTTPError } from './errors.js'\nimport {\n\tbuildResult,\n\textractContent,\n\textractThinking,\n\textractTools,\n\textractUsage,\n\tjoinThinking,\n\tmapMessages,\n} from './helpers.js'\nimport { parseBody } from './parsers.js'\n\n/**\n * Implements the local Ollama inference boundary — a {@link ProviderInterface} over Ollama's\n * `POST /api/chat`, both non-streaming (`generate`) and streaming NDJSON (`stream`).\n *\n * @remarks\n * - **Wire protocol.** Posts `{ model, messages, stream, keep_alive, think }` plus\n * passthrough sampling `options` and mapped function `tools`. The `think` flag is\n * configurable through {@link OllamaOptions.think} (default `false`). Non-stream parses\n * one JSON body; stream consumes NDJSON (one JSON object per `\\n`-terminated line) —\n * deltas carry `message.content`, the final `done: true` line carries the token usage.\n * - **Think separation.** The wire `think` flag is configurable\n * ({@link OllamaOptions.think}, default `false`). With `think: true` a thinking model's\n * daemon separates reasoning natively — returning it on the distinct `message.thinking`\n * channel (read here through `extractThinking`) instead of inline in `message.content`. Either\n * way the per-call {@link ThinkSplitterInterface} is the defensive guarantee: a daemon\n * may ignore `think: false` for a thinking model and inline `<think>` tags, so every\n * content delta routes through the splitter, only clean content is yielded / assembled,\n * and the separated reasoning (plus any daemon-side `message.thinking` deltas) lands on\n * `ProviderResult.thinking`, never in the conversation.\n * - **Boundary narrowing.** Every wire value arrives as `unknown` and is\n * narrowed through guards (`isRecord` / `isString` / `isNumber`) — never `as`. A\n * missing / malformed field degrades to a sensible default (empty content, no\n * usage, `{}` arguments), never a throw.\n * - **Bounded.** Each call arms a {@link Timeout} for `OllamaOptions.timeout` and\n * passes `AbortSignal.any([timeout.signal, signal])` to `fetch`, so the caller's\n * signal and the deadline both cancel the request. The timeout is always cleared —\n * in `#fetch` if the request fails/aborts, otherwise in the consuming call's `finally`.\n * - **Abort recovers partial.** A `stream` cancelled mid-flight throws a\n * `ProviderAbortError` carrying the partial result assembled so far; pairing the\n * `TextDecoder({ stream: true })` with the `createNDJSONParser` parser keeps multi-byte\n * UTF-8 splits and partial lines honest.\n * - **Event-free.** A pure functional boundary — no Emitter, no events.\n * - **Transport seam.** {@link OllamaOptions.fetch} swaps the transport (default\n * `globalThis.fetch`) and {@link OllamaOptions.headers} is a per-request, possibly\n * async header injector merged over the base `Content-Type` — so a browser runtime\n * can route through the developer's own server with an obfuscated bearer token,\n * without this library ever handling a real API key. Both omitted ⇒ the global `fetch`\n * and only a JSON content type.\n * Orthogonal to the deadline: the hook is awaited inside `#fetch`'s try, so a hook\n * rejection clears the armed timer like any other request failure.\n *\n * @example\n * ```ts\n * const provider = new OllamaProvider({ model: 'qwen3.5:2b-q4_K_M' })\n * const result = await provider.generate(messages, abort.signal)\n * ```\n */\nexport class OllamaProvider implements ProviderInterface {\n\treadonly name = 'ollama'\n\treadonly #id: string\n\treadonly #model: string\n\treadonly #url: string\n\treadonly #keepAlive: string | number\n\treadonly #timeout: number\n\treadonly #think: boolean\n\treadonly #options: Readonly<Record<string, unknown>> | undefined\n\treadonly #transport: typeof globalThis.fetch\n\treadonly #headers:\n\t\t| (() => Readonly<Record<string, string>> | Promise<Readonly<Record<string, string>>>)\n\t\t| undefined\n\treadonly #format: ContextFormat | undefined\n\n\tconstructor(options: OllamaOptions) {\n\t\tthis.#id = crypto.randomUUID()\n\t\tthis.#model = options.model\n\t\tthis.#url = options.url ?? DEFAULT_OLLAMA_URL\n\t\tthis.#keepAlive = options.keepAlive ?? DEFAULT_KEEP_ALIVE\n\t\tthis.#timeout = options.timeout ?? DEFAULT_PROVIDER_TIMEOUT\n\t\t// The `/api/chat` `think` wire flag — default `false`, so a non-thinking model needs no\n\t\t// configuration and answers immediately. A thinking model whose reasoning is displayed\n\t\t// separately sets `think: true`, and the daemon then returns it on the\n\t\t// `message.thinking` channel (`extractThinking`) rather than inline in `message.content`.\n\t\tthis.#think = options.think ?? false\n\t\tthis.#options = options.options\n\t\t// The transport seam: a custom fetch (defaulting to the global, bound to its\n\t\t// `globalThis` receiver — invoking a bare reference through a field loses the `window`\n\t\t// receiver and browsers throw `Illegal invocation`; node's fetch is receiver-agnostic,\n\t\t// so only a browser runtime ever saw it) and a dynamic header injector — both omitted\n\t\t// by default, so the request goes out over the global fetch carrying only the JSON\n\t\t// content type. The injected transport is `#transport` (the request method already\n\t\t// owns the `#fetch` name).\n\t\tthis.#transport = options.fetch ?? globalThis.fetch.bind(globalThis)\n\t\tthis.#headers = options.headers\n\t\t// The context-framing default (the provider-default level of AgentContext's format\n\t\t// cascade) — expose-only: read by the Agent through `build(this.#provider.format)` and\n\t\t// consumed by core's cascade, it never enters `#body` / the `/api/chat` wire. It is\n\t\t// not Ollama's structured-output `format` wire param — that one is sent in `#body`,\n\t\t// but only when a per-call `ProviderStreamOptions.schema` is supplied; the two\n\t\t// merely share a word. Omitted ⇒ undefined ⇒ core's built-in framing.\n\t\tthis.#format = options.format\n\t}\n\n\t/**\n\t * Exposes this instance's identity — a fresh `crypto.randomUUID()` minted at\n\t * construction, satisfying the {@link ProviderInterface.id} contract member. A second\n\t * provider built from identical options carries a distinct id.\n\t *\n\t * @returns The instance's minted identifier\n\t */\n\tget id(): string {\n\t\treturn this.#id\n\t}\n\n\t/**\n\t * Exposes the provider's context-framing default — the provider-default level of\n\t * {@link import('@orkestrel/agent').AgentContextInterface.build}'s format cascade (it beats\n\t * the managers' built-in framing, is beaten by a manager-options or per-item override).\n\t * Satisfies the optional {@link ProviderInterface.format} contract member: `undefined`\n\t * when {@link OllamaOptions.format} was omitted (the framing-agnostic default ⇒ core's\n\t * built-in framing applies unchanged), else the exact configured framing the Agent\n\t * threads into `build()`.\n\t *\n\t * @remarks\n\t * Expose-only — read by the Agent loop and consumed by core's cascade; it is never sent\n\t * on the `/api/chat` wire (it is absent from `#body` / the request). This is not Ollama's\n\t * structured-output `format` wire parameter — that one is sent in `#body`, but only when\n\t * a per-call `ProviderStreamOptions.schema` is supplied; only the word collides.\n\t *\n\t * @returns The configured {@link ContextFormat}, or `undefined` when none\n\t */\n\tget format(): ContextFormat | undefined {\n\t\treturn this.#format\n\t}\n\n\t/**\n\t * Generates one complete turn and resolves the assembled result — the clean content,\n\t * any separated reasoning, any tool calls, and any usage the wire reported.\n\t *\n\t * @remarks\n\t * Sends `stream: false` and parses one JSON body. Content routes through a per-call\n\t * think splitter, so the assembled content stays clean even where the daemon renders a\n\t * thinking model's reasoning inline; the separated spans and any daemon-side\n\t * `message.thinking` land on `thinking`. The caller's signal and the armed deadline\n\t * both cancel the request, and the deadline is cleared once the body is read.\n\t *\n\t * @param messages - The conversation turns to send\n\t * @param signal - The caller's bounding signal, folded with the armed deadline\n\t * @param tools - The callable tools to advertise for this turn, when the caller passes any\n\t * @param options - The per-call overrides, `think` and `schema` among them\n\t * @returns The assembled result of the turn\n\t * @throws {@link OllamaHTTPError} When the daemon answers a non-OK status.\n\t */\n\tasync generate(\n\t\tmessages: readonly Message[],\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): Promise<ProviderResult> {\n\t\tconst { response, timeout } = await this.#fetch(messages, false, signal, tools, options)\n\t\ttry {\n\t\t\tconst record = (await parseBody(response)) ?? {}\n\t\t\t// The one-body call routes through the same splitter as the stream (the daemon may\n\t\t\t// ignore `think: false` — the splitter is the guarantee): the assembled content is\n\t\t\t// clean (the splitter's authoritative `content`, which also covers the qwen3\n\t\t\t// template's implicit leading open), the separated spans + any wire-side\n\t\t\t// `message.thinking` land on `thinking`.\n\t\t\tconst splitter = createThinkSplitter()\n\t\t\tsplitter.split(extractContent(record))\n\t\t\tsplitter.flush()\n\t\t\tconst thinking = joinThinking(splitter, extractThinking(record))\n\t\t\treturn buildResult(splitter.content, thinking, extractTools(record), extractUsage(record))\n\t\t} finally {\n\t\t\ttimeout.clear()\n\t\t}\n\t}\n\n\t/**\n\t * Streams one turn, yielding a channel-tagged delta per non-empty content or reasoning\n\t * span and returning the assembled result when the stream completes.\n\t *\n\t * @remarks\n\t * Sends `stream: true` and consumes NDJSON — one JSON object per newline-terminated\n\t * line — pairing a streaming `TextDecoder` with the `NDJSONParser` so a record split\n\t * across byte reads is reassembled. The returned result's content is the splitter's\n\t * clean accumulation, beside any tool calls collected across lines and the usage the\n\t * `done` line carries. A cancel mid-flight throws a `ProviderAbortError` carrying the\n\t * partial assembled so far.\n\t *\n\t * @param messages - The conversation turns to send\n\t * @param signal - The caller's bounding signal, folded with the armed deadline\n\t * @param tools - The callable tools to advertise for this turn, when the caller passes any\n\t * @param options - The per-call overrides, `think` and `schema` among them\n\t * @returns The assembled result of the turn, after the last delta\n\t * @throws {@link OllamaHTTPError} When the daemon answers a non-OK status or a `null` body.\n\t */\n\tasync *stream(\n\t\tmessages: readonly Message[],\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): AsyncGenerator<ProviderDelta, ProviderResult> {\n\t\tconst { response, timeout, combined } = await this.#fetch(\n\t\t\tmessages,\n\t\t\ttrue,\n\t\t\tsignal,\n\t\t\ttools,\n\t\t\toptions,\n\t\t)\n\t\tconst body = response.body\n\t\tif (body === null) {\n\t\t\ttimeout.clear()\n\t\t\tthrow new OllamaHTTPError('Ollama API error: no response body', 0)\n\t\t}\n\t\tconst reader = body.getReader()\n\t\tconst decoder = new TextDecoder()\n\t\tconst parser = createNDJSONParser()\n\t\t// The per-call think separator: every wire content delta routes through it, so\n\t\t// only clean content is yielded / assembled even when the daemon ignores `think: false`\n\t\t// for a thinking model; daemon-side `message.thinking` deltas accumulate beside it.\n\t\t// The assembled content is the splitter's authoritative `content` — across the qwen3\n\t\t// template's implicit leading open (a bare `</think>` with the open pre-seeded into the\n\t\t// prompt scaffold) the splitter reclassifies the already-yielded prefix into `thinking`,\n\t\t// so the result stays clean even though those deltas could not be recalled.\n\t\tconst splitter = createThinkSplitter()\n\t\t// The per-stream accumulators, folded from every `#deltas` return across the live\n\t\t// loop and the post-loop NDJSON tail flush following.\n\t\tlet wired = ''\n\t\tconst calls: ToolCall[] = []\n\t\tlet usage: TokenUsage | undefined\n\t\ttry {\n\t\t\tfor (;;) {\n\t\t\t\tconst { value, done } = await reader.read()\n\t\t\t\tif (done) break\n\t\t\t\t// Pair the streaming decoder with the line parser: the decoder handles\n\t\t\t\t// partial multi-byte characters, the parser handles partial lines.\n\t\t\t\tfor (const record of parser.parse(decoder.decode(value, { stream: true }))) {\n\t\t\t\t\tconst increment = yield* this.#deltas(record, splitter, usage)\n\t\t\t\t\twired += increment.thinking\n\t\t\t\t\tcalls.push(...increment.calls)\n\t\t\t\t\tusage = increment.usage\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Flush the decoder's held partial multi-byte tail and feed it (plus a\n\t\t\t// terminating `\\n`) through the parser, so a non-conformant proxy's final\n\t\t\t// unterminated `done` line is recovered instead of silently dropped.\n\t\t\tconst decoderTail = decoder.decode()\n\t\t\tfor (const record of parser.parse(decoderTail.length > 0 ? `${decoderTail}\\n` : '\\n')) {\n\t\t\t\tconst increment = yield* this.#deltas(record, splitter, usage)\n\t\t\t\twired += increment.thinking\n\t\t\t\tcalls.push(...increment.calls)\n\t\t\t\tusage = increment.usage\n\t\t\t}\n\t\t\t// Stream end: a held partial tag that never completed was real content — it is the\n\t\t\t// final delta (the splitter folds it into its `content` too).\n\t\t\tconst tail = splitter.flush()\n\t\t\tif (tail.length > 0) yield { channel: 'content', text: tail }\n\t\t} catch (error) {\n\t\t\t// A mid-stream cancel (the caller's signal or the deadline) surfaces the\n\t\t\t// partial so the loop can recover what streamed; anything else propagates.\n\t\t\tif (combined.aborted) {\n\t\t\t\t// Flush the splitter's held partial tail first (mirrors the\n\t\t\t\t// normal-completion assembly preceding) so the recovered partial includes\n\t\t\t\t// any clean content that never crossed a tag boundary.\n\t\t\t\tsplitter.flush()\n\t\t\t\tthrow new ProviderAbortError(\n\t\t\t\t\tbuildResult(splitter.content, joinThinking(splitter, wired), calls, usage),\n\t\t\t\t)\n\t\t\t}\n\t\t\tthrow error\n\t\t} finally {\n\t\t\t// Cancel (not merely release) the reader on early return so the\n\t\t\t// underlying HTTP connection is freed; a normal-done or already-errored\n\t\t\t// reader tolerates the redundant cancel as a no-op. `cancel()` also\n\t\t\t// releases the lock — never call `releaseLock()` afterward.\n\t\t\ttry {\n\t\t\t\tawait reader.cancel()\n\t\t\t} catch {\n\t\t\t\t// Never mask the primary error/result with a cancel failure.\n\t\t\t}\n\t\t\tparser.clear()\n\t\t\ttimeout.clear()\n\t\t}\n\t\treturn buildResult(splitter.content, joinThinking(splitter, wired), calls, usage)\n\t}\n\n\t// Per-record streaming step shared between the live NDJSON loop and the post-loop\n\t// tail flush in `stream()` — a `#` private method (not a free helper) because it is\n\t// the streaming spine that composes the wire leaves and drives the splitter, and\n\t// because its yields are the stream's own. It mutates nothing: it returns the record's\n\t// increments (`thinking` / `calls` / `usage`) and `stream()` folds them, so the\n\t// accumulator's shape is written once, here.\n\t*#deltas(\n\t\trecord: Readonly<Record<string, unknown>>,\n\t\tsplitter: ThinkSplitterInterface,\n\t\tusage: TokenUsage | undefined,\n\t): Generator<\n\t\tProviderDelta,\n\t\t{\n\t\t\treadonly thinking: string\n\t\t\treadonly calls: readonly ToolCall[]\n\t\t\treadonly usage: TokenUsage | undefined\n\t\t}\n\t> {\n\t\tconst delta = splitter.split(extractContent(record))\n\t\tif (delta.length > 0) yield { channel: 'content', text: delta }\n\t\t// The primary live reasoning channel: each native `message.thinking` wire delta is\n\t\t// surfaced as a tagged `thinking` delta and returned for the caller's `wired`\n\t\t// accumulation (the two stay in lockstep). The ThinkSplitter's in-content\n\t\t// reclassified spans have no per-delta hook — the final `ProviderResult.thinking`\n\t\t// reconciles them; the native channel (think: true) is what streams live.\n\t\tconst thinking = extractThinking(record)\n\t\tif (thinking.length > 0) yield { channel: 'thinking', text: thinking }\n\t\t// Only the `done` line carries usage, so every other record hands the caller's\n\t\t// current value straight back rather than clearing it.\n\t\treturn {\n\t\t\tthinking,\n\t\t\tcalls: extractTools(record),\n\t\t\tusage: Reflect.get(record, 'done') === true ? extractUsage(record) : usage,\n\t\t}\n\t}\n\n\t// Arm the deadline, POST `/api/chat`, and hand back the response + the handles\n\t// that bound it. On a non-OK status, clear the deadline and throw with the body.\n\tasync #fetch(\n\t\tmessages: readonly Message[],\n\t\tstream: boolean,\n\t\tsignal: AbortSignal,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): Promise<OllamaResponse> {\n\t\tconst timeout = new Timeout({ ms: this.#timeout })\n\t\ttimeout.start()\n\t\tconst combined = AbortSignal.any([timeout.signal, signal])\n\t\ttry {\n\t\t\tconst response = await this.#transport(`${this.#url}/api/chat`, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: await this.#requestHeaders(),\n\t\t\t\tbody: JSON.stringify(this.#body(messages, stream, tools, options)),\n\t\t\t\tsignal: combined,\n\t\t\t})\n\t\t\tif (!response.ok) {\n\t\t\t\t// Bound the incorporated body: a defensive proxy or daemon could hand\n\t\t\t\t// back an unbounded response — read defensively so a body-read\n\t\t\t\t// failure still throws with the status, never a masked/unbounded read.\n\t\t\t\tlet detail: string\n\t\t\t\ttry {\n\t\t\t\t\tconst text = await response.text()\n\t\t\t\t\tdetail = text.length > MAX_ERROR_BODY_LENGTH ? text.slice(0, MAX_ERROR_BODY_LENGTH) : text\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tthrow new OllamaHTTPError(\n\t\t\t\t\t\t`Ollama API error: ${response.status} - (error body unavailable)`,\n\t\t\t\t\t\tresponse.status,\n\t\t\t\t\t\t{ cause },\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthrow new OllamaHTTPError(\n\t\t\t\t\t`Ollama API error: ${response.status} - ${detail}`,\n\t\t\t\t\tresponse.status,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn { response, timeout, combined }\n\t\t} catch (error) {\n\t\t\t// `fetch` rejected (pre-aborted signal / unreachable / network) or the status\n\t\t\t// was non-OK — clear the deadline so the armed timer can't outlive the failed\n\t\t\t// call. The caller's `finally` only takes ownership once `#fetch` returns a response.\n\t\t\ttimeout.clear()\n\t\t\tthrow error\n\t\t}\n\t}\n\n\t// The request headers — the base JSON content type, plus the dynamic `headers`\n\t// hook's result merged on top when configured (so a dev can attach an obfuscated\n\t// bearer the server validates). Merge order: `Content-Type` is seeded first, then\n\t// the hook's entries overlay it — so the hook adds auth headers but only clobbers\n\t// `Content-Type` if the dev explicitly returns one. Awaited (the hook may be async,\n\t// for example refreshing a token); called inside `#fetch`'s try so a hook rejection\n\t// clears the armed deadline like any other request failure. The hook's result is a\n\t// `Readonly<Record<string, string>>` already — merged through `Object.entries`, no `as`.\n\tasync #requestHeaders(): Promise<Record<string, string>> {\n\t\tconst headers: Record<string, string> = { 'Content-Type': 'application/json' }\n\t\tif (this.#headers !== undefined) {\n\t\t\tfor (const [key, value] of Object.entries(await this.#headers())) headers[key] = value\n\t\t}\n\t\treturn headers\n\t}\n\n\t// The `/api/chat` request body — conditional `options` / `tools` / `format` only when set. The\n\t// wire `think` flag honours a per-call override (`options.think`) over the constructor default\n\t// (`#think`), so a caller can flip reasoning on / off for one turn without reconfiguring the\n\t// provider; no per-call option ⇒ the constructed default.\n\t// `format` is the wire's structured-output constraint, forwarded verbatim from the per-call\n\t// `ProviderStreamOptions.schema` — unrelated to `OllamaOptions.format` (prompt-context framing).\n\t#body(\n\t\tmessages: readonly Message[],\n\t\tstream: boolean,\n\t\ttools?: readonly ToolDefinition[],\n\t\toptions?: ProviderStreamOptions,\n\t): WireChatRequest {\n\t\treturn {\n\t\t\tmodel: this.#model,\n\t\t\tmessages: mapMessages(messages),\n\t\t\tstream,\n\t\t\tkeep_alive: this.#keepAlive,\n\t\t\tthink: options?.think ?? this.#think,\n\t\t\t...(this.#options !== undefined ? { options: this.#options } : {}),\n\t\t\t...(options?.schema !== undefined ? { format: options.schema } : {}),\n\t\t\t...(tools !== undefined && tools.length > 0\n\t\t\t\t? {\n\t\t\t\t\t\ttools: tools.map((tool): NonNullable<WireChatRequest['tools']>[number] => ({\n\t\t\t\t\t\t\ttype: 'function',\n\t\t\t\t\t\t\tfunction: {\n\t\t\t\t\t\t\t\tname: tool.name,\n\t\t\t\t\t\t\t\t...(tool.description === undefined ? {} : { description: tool.description }),\n\t\t\t\t\t\t\t\t...(tool.parameters === undefined ? {} : { parameters: tool.parameters }),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})),\n\t\t\t\t\t}\n\t\t\t\t: {}),\n\t\t}\n\t}\n}\n","import type { ProviderInterface } from '@orkestrel/agent'\nimport type { OllamaOptions } from './types.js'\nimport { OllamaProvider } from './OllamaProvider.js'\n\n/**\n * Creates a local Ollama inference provider — a {@link ProviderInterface} over the\n * daemon's `POST /api/chat`, supporting non-streaming `generate` and streaming\n * `stream`.\n *\n * @remarks\n * Only `model` is required; `url` defaults to the local daemon, `keepAlive` to `'5m'`,\n * `timeout` to `120_000`ms, and `options` is forwarded verbatim as sampling\n * parameters (`temperature`, `seed`, and `num_predict`). Each call takes an\n * `AbortSignal` to bound the request; a `stream` cancelled mid-flight throws a\n * `ProviderAbortError` carrying the partial result.\n *\n * The optional `fetch` + `headers` form a transport seam (see {@link OllamaOptions}):\n * point `url` at your own server, inject a custom `fetch`, and have `headers` attach a\n * generated/obfuscated bearer token your server validates — so a browser runtime\n * reaches the LLM through your middleware without this library ever handling the real API\n * key. Both omitted ⇒ the global `fetch` and only a JSON content type.\n *\n * The optional `format` is the provider's context-framing default — the provider-default\n * level of `AgentContext`'s format cascade (beaten by a manager-options or per-item\n * override, beating the managers' built-in framing), declaring how this\n * provider's models prefer context sections framed (for example XML group wrappers vs. Markdown\n * headers). It is exposed on the provider for the Agent's `build()` and is not Ollama's\n * `/api/chat` `format` wire parameter (structured output) — the framing default and that\n * wire parameter are unrelated despite the shared word. Omitted ⇒ the provider is\n * framing-agnostic (core's built-in defaults).\n *\n * @param options - `model` (required), and optional `url` / `keepAlive` / `timeout` /\n * `options` / `fetch` / `headers` / `format` (see {@link OllamaOptions})\n * @returns A working {@link ProviderInterface} backed by Ollama\n *\n * @example createOllama + generate\n * ```ts\n * import { createAbort } from '@orkestrel/abort'\n * import { createOllama } from '@orkestrel/ollama'\n *\n * const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M', options: { temperature: 0 } })\n * const abort = createAbort()\n * const messages = [\n * \t{ id: '1', role: 'user', content: 'Summarize the release notes for version 2.0.' },\n * ] as const\n *\n * const result = await provider.generate(messages, abort.signal)\n * console.log(result.content)\n * if (result.usage) charge(result.usage) // fold into a token budget\n * ```\n *\n * @example\n * Route through your own server with an obfuscated token:\n * ```ts\n * const provider = createOllama({\n * model: 'qwen3.5:2b-q4_K_M',\n * url: 'https://my-app.example.com/llm', // your server, not the daemon\n * fetch: myFetch, // optional custom transport\n * headers: () => ({ authorization: `Bearer ${myToken}` }), // your server validates this\n * })\n * ```\n *\n * @example\n * Declare a context-framing default — wrap the instructions section in an XML group (the\n * provider-default level of `AgentContext`'s cascade; not the wire `format`):\n * ```ts\n * const provider = createOllama({\n * model: 'qwen3.5:2b-q4_K_M',\n * format: {\n * instructions: {\n * open: '<instructions>',\n * render: (i) => `<instruction>${i.content}</instruction>`,\n * close: '</instructions>',\n * },\n * },\n * })\n * ```\n */\nexport function createOllama(options: OllamaOptions): ProviderInterface {\n\treturn new OllamaProvider(options)\n}\n"],"mappings":"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"}