@noetaris/harness-types 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/index.d.ts +13 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@ This package defines the common TypeScript interfaces that all LLM adapter packa
|
|
|
18
18
|
| `ContentBlock` | `TextBlock \| ImageBlock` |
|
|
19
19
|
| `Tool` | `{ name: string; description: string; inputSchema: Record<string, unknown> }` |
|
|
20
20
|
| `ToolCall` | `{ id: string; name: string; input: unknown }` |
|
|
21
|
-
| `LLMResponse` | `{ text: string; toolCalls: ToolCall[]; stopReason: 'end' \| 'tool_use' \| 'max_tokens'; usage: { inputTokens: number; outputTokens: number; cacheReadTokens?: number; cacheWriteTokens?: number; contextWindowSize?: number } }` — `usage` is always present; all adapters must populate it. `inputTokens` is the total, cached tokens included (see [Prompt caching](#prompt-caching)) |
|
|
21
|
+
| `LLMResponse` | `{ text: string; toolCalls: ToolCall[]; stopReason: 'end' \| 'tool_use' \| 'max_tokens'; usage: { inputTokens: number; outputTokens: number; cacheReadTokens?: number; cacheWriteTokens?: number; cacheWriteTokensByTtl?: Record<string, number>; contextWindowSize?: number } }` — `usage` is always present; all adapters must populate it. `inputTokens` is the total, cached tokens included (see [Prompt caching](#prompt-caching)) |
|
|
22
22
|
| `LLMRequestEvent` | `"llm.request"` payload: `{ callId: string; modelId: string; providerName: string; messages?: Message[]; tools?: Tool[]; system?: string \| TextBlock[] }` (`messages`/`tools`/`system` opt-in, the harness shapes passed to `invoke()`; `callId` pairs the event with its `"llm.response"` or `"llm.error"`) |
|
|
23
|
-
| `LLMUsageEvent` | `"llm.response"` payload: `{ callId: string; tokens: { input: number; output: number; cacheRead?: number; cacheWrite?: number }; modelId: string; stopReason: LLMResponse['stopReason']; providerName: string; output?: LLMResponse; contextWindowSize?: number }` (`output` opt-in; `tokens` carries the same numbers as `usage`, mock adapters included) |
|
|
23
|
+
| `LLMUsageEvent` | `"llm.response"` payload: `{ callId: string; tokens: { input: number; output: number; cacheRead?: number; cacheWrite?: number; cacheWriteByTtl?: Record<string, number> }; modelId: string; stopReason: LLMResponse['stopReason']; providerName: string; output?: LLMResponse; contextWindowSize?: number }` (`output` opt-in; `tokens` carries the same numbers as `usage`, mock adapters included) |
|
|
24
24
|
| `LLMErrorEvent` | `"llm.error"` payload: `{ callId: string; modelId: string; providerName: string; error: unknown }` — emitted when the call throws after `"llm.request"`; `error` is the thrown value, which the adapter rethrows (`signal.reason` for a cancelled call, see [Cancelling a call](#cancelling-a-call)) |
|
|
25
25
|
| `ToolCallEvent` | `"tool.call"` payload: `{ toolName: string; toolCallId: string; input?: unknown }` (`input` opt-in) |
|
|
26
26
|
| `ToolResultEvent` | `"tool.result"` payload: `{ toolName: string; toolCallId: string; durationMs: number; result?: unknown; error?: unknown }` (`result` opt-in; `error` is set whenever the tool threw) |
|
|
@@ -109,6 +109,7 @@ const messages: Message[] = [{
|
|
|
109
109
|
| `inputTokens` | `input` | Total input tokens of the call, **cached tokens included**. Compare it with `contextWindowSize`. |
|
|
110
110
|
| `cacheReadTokens?` | `cacheRead?` | Input tokens read from the cache. A part of `inputTokens`, not added to it. |
|
|
111
111
|
| `cacheWriteTokens?` | `cacheWrite?` | Input tokens written to the cache. A part of `inputTokens`, not added to it. |
|
|
112
|
+
| `cacheWriteTokensByTtl?` | `cacheWriteByTtl?` | `cacheWriteTokens` split by TTL. Keys are the provider's TTL labels; values add up to `cacheWriteTokens`. |
|
|
112
113
|
|
|
113
114
|
An absent cache field means the adapter cannot know. A present `0` is a real count (no cache hit, nothing written).
|
|
114
115
|
|
|
@@ -118,6 +119,19 @@ The uncached part, billed at the normal input price, is:
|
|
|
118
119
|
uncached = inputTokens − (cacheReadTokens ?? 0) − (cacheWriteTokens ?? 0)
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
**Writes by TTL.** A cache write's price depends on how long the entry lives. On Anthropic, a 5-minute write costs 1.25× the input price and a 1-hour write 2×. `cacheWriteTokensByTtl` splits `cacheWriteTokens` by TTL so each part can be priced:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// usage: { inputTokens: 2000, outputTokens: 50, cacheReadTokens: 0,
|
|
126
|
+
// cacheWriteTokens: 1500, cacheWriteTokensByTtl: { '5m': 500, '1h': 1000 } }
|
|
127
|
+
const writeCost = Object.entries(usage.cacheWriteTokensByTtl ?? {})
|
|
128
|
+
.reduce((sum, [ttl, tokens]) => sum + tokens * writePrice(ttl), 0)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- Keys are the provider's TTL labels (`'5m'`, `'1h'`, `'30m'`, ...). They are plain strings because the labels differ by provider.
|
|
132
|
+
- When present, the values add up to `cacheWriteTokens`.
|
|
133
|
+
- Absent means the adapter cannot tell the split, not that nothing was written. Each adapter's README lists the keys it reports; a reported key with `0` is a real count.
|
|
134
|
+
|
|
121
135
|
Which fields each adapter reports is in its README.
|
|
122
136
|
|
|
123
137
|
## System prompt
|
package/dist/index.d.ts
CHANGED
|
@@ -150,6 +150,14 @@ interface LLMResponse {
|
|
|
150
150
|
* it. Absent when the adapter cannot know; `0` is a real count.
|
|
151
151
|
*/
|
|
152
152
|
cacheWriteTokens?: number;
|
|
153
|
+
/**
|
|
154
|
+
* `cacheWriteTokens` split by cache lifetime (TTL). Each key is the provider's TTL label
|
|
155
|
+
* (for example `'5m'`, `'1h'`, `'30m'`); each value is the input tokens written with that
|
|
156
|
+
* TTL. The values add up to `cacheWriteTokens`, which is then present too. Absent when the
|
|
157
|
+
* adapter cannot tell the split. Which keys an adapter reports is in its README; a reported
|
|
158
|
+
* key with `0` is a real count. Keys are strings because TTL labels differ by provider.
|
|
159
|
+
*/
|
|
160
|
+
cacheWriteTokensByTtl?: Record<string, number>;
|
|
153
161
|
/**
|
|
154
162
|
* The model's total context window capacity in tokens.
|
|
155
163
|
* Present only when the adapter can determine it.
|
|
@@ -189,9 +197,10 @@ interface LLMRequestEvent {
|
|
|
189
197
|
* after a successful provider call.
|
|
190
198
|
*
|
|
191
199
|
* `tokens` carries the same numbers as the returned `LLMResponse.usage`: `input` = `inputTokens`,
|
|
192
|
-
* `output` = `outputTokens`, `cacheRead` = `cacheReadTokens`, `cacheWrite` = `cacheWriteTokens
|
|
193
|
-
* (present exactly when the usage field is). Mock
|
|
194
|
-
* they return.
|
|
200
|
+
* `output` = `outputTokens`, `cacheRead` = `cacheReadTokens`, `cacheWrite` = `cacheWriteTokens`,
|
|
201
|
+
* `cacheWriteByTtl` = `cacheWriteTokensByTtl` (present exactly when the usage field is). Mock
|
|
202
|
+
* adapters report the `usage` of the response they return. `cacheWriteByTtl` may be the same
|
|
203
|
+
* object as in `usage`; observers must not mutate it.
|
|
195
204
|
*
|
|
196
205
|
* `output` is opt-in — it may contain sensitive data; adapters default to omitting it. When
|
|
197
206
|
* present it is the normalized {@link LLMResponse} that `invoke()` returns, passed by reference.
|
|
@@ -206,6 +215,7 @@ interface LLMUsageEvent {
|
|
|
206
215
|
output: number;
|
|
207
216
|
cacheRead?: number;
|
|
208
217
|
cacheWrite?: number;
|
|
218
|
+
cacheWriteByTtl?: Record<string, number>;
|
|
209
219
|
};
|
|
210
220
|
modelId: string;
|
|
211
221
|
stopReason: LLMResponse['stopReason'];
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/run-tool.ts","../src/index.ts"],"sourcesContent":["import type { ToolCall, ToolCallEvent, ToolResultEvent } from './index.js'\n\n/**\n * The part of a step ctx {@link runTool} needs. The `@noetaris/harness` step ctx (0.7 and\n * later) satisfies it structurally.\n */\nexport interface ToolRunContext {\n emit(name: string, payload?: unknown): void\n within<T>(type: string, payload: unknown, fn: () => Promise<T>): Promise<T>\n}\n\nexport interface RunToolOptions {\n /**\n * Put the tool's `input` on `\"tool.call\"` and its `result` on `\"tool.result\"`. They may\n * contain sensitive data. Default: `false`.\n */\n captureIO?: boolean\n}\n\n/**\n * Run one tool call and report it: emits `\"tool.call\"`, runs `fn` inside the observer's scope\n * for that event (`ctx.within('tool.call', { toolCallId })`), then emits `\"tool.result\"` with\n * `durationMs` — and `error` when `fn` threw, in which case the same value is rethrown.\n * Returns `fn`'s value.\n *\n * @example\n * ```ts\n * const outputs = await Promise.all(\n * res.toolCalls.map(call => runTool(ctx, call, () => toolImpls[call.name](call.input))),\n * )\n * ```\n */\nexport async function runTool<T>(\n ctx: ToolRunContext,\n call: Pick<ToolCall, 'id' | 'name' | 'input'>,\n fn: () => Promise<T>,\n options?: RunToolOptions,\n): Promise<T> {\n const captureIO = options?.captureIO === true\n const base = { toolName: call.name, toolCallId: call.id }\n const callEvent: ToolCallEvent = captureIO ? { ...base, input: call.input } : base\n ctx.emit('tool.call', callEvent)\n\n const start = Date.now()\n let value: T\n try {\n value = await ctx.within('tool.call', { toolCallId: call.id }, fn)\n } catch (error) {\n const failed: ToolResultEvent = {\n ...base,\n durationMs: Date.now() - start,\n // Consumers read `error !== undefined` as \"the tool failed\", so a thrown undefined is named.\n error: error === undefined ? new Error('tool threw undefined') : error,\n }\n ctx.emit('tool.result', failed)\n throw error\n }\n const succeeded: ToolResultEvent = captureIO\n ? { ...base, durationMs: Date.now() - start, result: value }\n : { ...base, durationMs: Date.now() - start }\n ctx.emit('tool.result', succeeded)\n return value\n}\n","/**\n * Shared LLM protocol types used by all harness LLM adapters.\n *\n * @packageDocumentation\n */\n\n/** A run of plain text inside a user message or a system prompt. */\nexport type TextBlock = {\n type: 'text'\n text: string\n /**\n * Marks the end of a cacheable prefix: the provider may cache everything up to and including\n * this block (the system prompt first, then the messages in order). Adapters whose provider\n * caches only at marked points (Anthropic) send it; adapters whose provider caches\n * automatically ignore it. A hint, not a guarantee: the provider decides whether a cache\n * entry is written or read (e.g. a minimum prefix length). Anthropic allows at most 4 points\n * per request (its `autoCache` option takes one of them); its adapter throws before\n * `\"llm.request\"` when there are more. See the adapter's README.\n */\n cachePoint?: true\n}\n\n/**\n * An image inside a user message.\n *\n * `data` is raw base64 with no `data:` prefix. `mediaType` is the MIME type\n * (e.g. `'image/png'`); Anthropic, OpenAI and Ollama throw on types they do not accept; Gemini passes any value through.\n */\nexport type ImageBlock = {\n type: 'image'\n data: string\n mediaType: string\n /**\n * Marks the end of a cacheable prefix: the provider may cache everything up to and including\n * this block (the system prompt first, then the messages in order). Adapters whose provider\n * caches only at marked points (Anthropic) send it; adapters whose provider caches\n * automatically ignore it. A hint, not a guarantee: the provider decides whether a cache\n * entry is written or read (e.g. a minimum prefix length). Anthropic allows at most 4 points\n * per request (its `autoCache` option takes one of them); its adapter throws before\n * `\"llm.request\"` when there are more. See the adapter's README.\n */\n cachePoint?: true\n}\n\n/** One piece of user-message content. */\nexport type ContentBlock = TextBlock | ImageBlock\n\n/**\n * A single turn in a conversation.\n *\n * - `user` — input from the human side: a string, or blocks mixing text and images.\n * An empty block array is allowed by the type but may be rejected by providers; don't send one.\n * - `assistant` — model reply, optionally carrying tool-call requests.\n * - `tool` — result returned to the model after a tool call.\n */\nexport type Message =\n | { role: 'user'; content: string | ContentBlock[] }\n | { role: 'assistant'; content?: string; toolCalls?: ToolCall[] }\n | { role: 'tool'; toolCallId: string; content: string }\n\n/**\n * Normalize user-message content to blocks.\n *\n * A string becomes one text block (an empty string stays one empty text block).\n * An array is returned as-is, same reference, with no validation.\n */\nexport function toBlocks(content: string | ContentBlock[]): ContentBlock[] {\n return typeof content === 'string' ? [{ type: 'text', text: content }] : content\n}\n\n/**\n * A tool the LLM may invoke during a conversation turn.\n *\n * `inputSchema` must be a valid JSON Schema object describing the tool's\n * input parameters (LLM adapters pass it to the provider as-is).\n */\nexport interface Tool {\n name: string\n description: string\n inputSchema: Record<string, unknown>\n}\n\n/**\n * A single tool-call request emitted by the LLM in an assistant turn.\n *\n * `id` is provider-assigned and must be echoed back in the corresponding\n * `role: 'tool'` message so the model can correlate results.\n */\nexport interface ToolCall {\n id: string\n name: string\n input: unknown\n}\n\n/**\n * Normalized response returned by every {@link LLM} adapter.\n *\n * `text` may be empty when `stopReason` is `'tool_use'`.\n * `toolCalls` is empty when `stopReason` is `'end'` or `'max_tokens'`.\n * `usage` is always present — all adapters must populate it.\n */\nexport interface LLMResponse {\n text: string\n toolCalls: ToolCall[]\n stopReason: 'end' | 'tool_use' | 'max_tokens'\n usage: {\n /**\n * Total input tokens of this call, cached tokens included. Compare it with\n * `contextWindowSize`. The uncached part, billed at the normal input price, is\n * `inputTokens − (cacheReadTokens ?? 0) − (cacheWriteTokens ?? 0)`.\n */\n inputTokens: number\n /** Number of output tokens generated by this call. */\n outputTokens: number\n /**\n * Input tokens read from the provider's prompt cache. Part of `inputTokens`, not added to\n * it. Absent when the adapter cannot know; `0` is a real count (no cache hit).\n */\n cacheReadTokens?: number\n /**\n * Input tokens written to the provider's prompt cache. Part of `inputTokens`, not added to\n * it. Absent when the adapter cannot know; `0` is a real count.\n */\n cacheWriteTokens?: number\n /**\n * The model's total context window capacity in tokens.\n * Present only when the adapter can determine it.\n * Absent when the adapter cannot determine capacity.\n */\n contextWindowSize?: number\n }\n}\n\n/**\n * Payload emitted by LLM adapters as an `\"llm.request\"` observer event,\n * immediately before the provider call is made.\n *\n * Each `invoke()` call that reaches this event is followed by exactly one of\n * `\"llm.response\"` ({@link LLMUsageEvent}) or `\"llm.error\"` ({@link LLMErrorEvent}),\n * carrying the same `callId`. A throw before this event emits nothing.\n *\n * `messages`, `tools` and `system` are opt-in — they may contain sensitive data. When present they\n * are the normalized harness shapes passed to `invoke()`, not the provider-translated\n * payloads. They are passed by reference; observers must not mutate them.\n */\nexport interface LLMRequestEvent {\n /**\n * Correlation id for this single `invoke()` call, minted per call by the adapter and\n * carried on the `\"llm.request\"` event and on the matching `\"llm.response\"` or\n * `\"llm.error\"` event, so an observer can pair them reliably even when concurrent\n * fork branches interleave.\n */\n callId: string\n modelId: string\n providerName: string\n messages?: Message[] // opt-in; the messages passed to invoke()\n tools?: Tool[] // opt-in; the tools passed to invoke()\n system?: string | TextBlock[] // opt-in; the system option passed to invoke()\n}\n\n/**\n * Token-usage payload emitted by LLM adapters as an `\"llm.response\"` observer event,\n * after a successful provider call.\n *\n * `tokens` carries the same numbers as the returned `LLMResponse.usage`: `input` = `inputTokens`,\n * `output` = `outputTokens`, `cacheRead` = `cacheReadTokens`, `cacheWrite` = `cacheWriteTokens`\n * (present exactly when the usage field is). Mock adapters report the `usage` of the response\n * they return.\n *\n * `output` is opt-in — it may contain sensitive data; adapters default to omitting it. When\n * present it is the normalized {@link LLMResponse} that `invoke()` returns, passed by reference.\n * `contextWindowSize` mirrors `LLMResponse.usage.contextWindowSize` — present when the\n * adapter can determine it; absent otherwise.\n */\nexport interface LLMUsageEvent {\n /** Correlation id matching the `\"llm.request\"` event for the same `invoke()` call (see {@link LLMRequestEvent.callId}). */\n callId: string\n tokens: { input: number; output: number; cacheRead?: number; cacheWrite?: number }\n modelId: string\n stopReason: LLMResponse['stopReason']\n providerName: string\n output?: LLMResponse // opt-in; may be sensitive\n contextWindowSize?: number\n}\n\n/**\n * Payload emitted by LLM adapters as an `\"llm.error\"` observer event, when anything throws\n * after `\"llm.request\"` was emitted and before `\"llm.response\"` would be (the provider call,\n * response normalization). The adapter then rethrows the same value.\n *\n * When the call's `signal` aborted, `error` is `signal.reason`, unchanged. The caller can tell a\n * cancel from a failure reliably with `signal.aborted` (or `error === signal.reason`). An\n * observer, which does not see the signal, can use `error.name` only for the default reasons:\n * `AbortError` from `controller.abort()` with no argument, `TimeoutError` from\n * `AbortSignal.timeout()`; a custom reason (`controller.abort(new Error('x'))`,\n * `controller.abort('x')`) keeps its own name or has none.\n */\nexport interface LLMErrorEvent {\n /** Correlation id matching the `\"llm.request\"` event for the same `invoke()` call (see {@link LLMRequestEvent.callId}). */\n callId: string\n modelId: string\n providerName: string\n /** The thrown value, unchanged; any value, including `undefined`. */\n error: unknown\n}\n\n/**\n * Payload emitted on `\"tool.call\"` before a tool is dispatched.\n *\n * `input` is opt-in — it may contain sensitive data; omit by default.\n */\nexport interface ToolCallEvent {\n toolName: string\n toolCallId: string\n input?: unknown\n}\n\n/**\n * Payload emitted on `\"tool.result\"` after a tool completes or throws.\n *\n * `result` is opt-in — it may contain sensitive data; omit by default.\n * `error` is set whenever the tool threw (the thrown value) and absent on success; it is how\n * consumers tell a failed call from a successful one, so it is not gated.\n */\nexport interface ToolResultEvent {\n toolName: string\n toolCallId: string\n durationMs: number\n result?: unknown\n error?: unknown\n}\n\n/** Options of {@link LLM.invoke}. */\nexport interface InvokeOptions {\n /** Tools the model may call. */\n tools?: Tool[]\n /**\n * Cancels the call. When the signal aborts, the adapter closes the HTTP request and `invoke()`\n * rejects with `signal.reason` (an `AbortError` `DOMException` for `controller.abort()`, a\n * `TimeoutError` for `AbortSignal.timeout()`), not the provider SDK's own error. If the signal\n * is already aborted when `invoke()` is called, it rejects with no observer event. If it aborts\n * after `\"llm.request\"`, `\"llm.error\"` is emitted with `error: signal.reason` first.\n *\n * Aborting frees the caller and closes the connection. It does not guarantee that the provider\n * stops the work or stops billing for it.\n *\n * The harness does not pass the run's signal for you. To let `run.stop()` cut the call, pass it\n * from the step: `ctx.llm.invoke(messages, { signal: ctx.signal })`.\n */\n signal?: AbortSignal\n /**\n * The system prompt for this call. Each adapter sends it in its provider's native system slot.\n * A string, or text blocks (a block may carry `cachePoint`). Not stored in the conversation:\n * pass it on every call.\n */\n system?: string | TextBlock[]\n}\n\n/**\n * Core interface every LLM adapter must implement.\n *\n * @example\n * ```ts\n * const response = await llm.invoke(messages, { tools: [myTool], system: 'Be brief.', signal: ctx.signal })\n * ```\n */\nexport interface LLM {\n /**\n * Send a message list to the LLM and return a normalized response.\n *\n * @param messages - Conversation history, oldest first.\n * @param options - See {@link InvokeOptions}.\n * @param options.tools - Tools the model may call.\n * @param options.signal - Cancels the call; `invoke()` then rejects with `signal.reason`.\n * @param options.system - The system prompt, sent in the provider's native system slot.\n */\n invoke(messages: Message[], options?: InvokeOptions): Promise<LLMResponse>\n}\n\nexport { runTool, type ToolRunContext, type RunToolOptions } from './run-tool.js'\n"],"mappings":";AAgCA,eAAsB,QACpB,KACA,MACA,IACA,SACY;AACZ,QAAM,YAAY,SAAS,cAAc;AACzC,QAAM,OAAO,EAAE,UAAU,KAAK,MAAM,YAAY,KAAK,GAAG;AACxD,QAAM,YAA2B,YAAY,EAAE,GAAG,MAAM,OAAO,KAAK,MAAM,IAAI;AAC9E,MAAI,KAAK,aAAa,SAAS;AAE/B,QAAM,QAAQ,KAAK,IAAI;AACvB,MAAI;AACJ,MAAI;AACF,YAAQ,MAAM,IAAI,OAAO,aAAa,EAAE,YAAY,KAAK,GAAG,GAAG,EAAE;AAAA,EACnE,SAAS,OAAO;AACd,UAAM,SAA0B;AAAA,MAC9B,GAAG;AAAA,MACH,YAAY,KAAK,IAAI,IAAI;AAAA;AAAA,MAEzB,OAAO,UAAU,SAAY,IAAI,MAAM,sBAAsB,IAAI;AAAA,IACnE;AACA,QAAI,KAAK,eAAe,MAAM;AAC9B,UAAM;AAAA,EACR;AACA,QAAM,YAA6B,YAC/B,EAAE,GAAG,MAAM,YAAY,KAAK,IAAI,IAAI,OAAO,QAAQ,MAAM,IACzD,EAAE,GAAG,MAAM,YAAY,KAAK,IAAI,IAAI,MAAM;AAC9C,MAAI,KAAK,eAAe,SAAS;AACjC,SAAO;AACT;;;ACIO,SAAS,SAAS,SAAkD;AACzE,SAAO,OAAO,YAAY,WAAW,CAAC,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,IAAI;AAC3E;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/run-tool.ts","../src/index.ts"],"sourcesContent":["import type { ToolCall, ToolCallEvent, ToolResultEvent } from './index.js'\n\n/**\n * The part of a step ctx {@link runTool} needs. The `@noetaris/harness` step ctx (0.7 and\n * later) satisfies it structurally.\n */\nexport interface ToolRunContext {\n emit(name: string, payload?: unknown): void\n within<T>(type: string, payload: unknown, fn: () => Promise<T>): Promise<T>\n}\n\nexport interface RunToolOptions {\n /**\n * Put the tool's `input` on `\"tool.call\"` and its `result` on `\"tool.result\"`. They may\n * contain sensitive data. Default: `false`.\n */\n captureIO?: boolean\n}\n\n/**\n * Run one tool call and report it: emits `\"tool.call\"`, runs `fn` inside the observer's scope\n * for that event (`ctx.within('tool.call', { toolCallId })`), then emits `\"tool.result\"` with\n * `durationMs` — and `error` when `fn` threw, in which case the same value is rethrown.\n * Returns `fn`'s value.\n *\n * @example\n * ```ts\n * const outputs = await Promise.all(\n * res.toolCalls.map(call => runTool(ctx, call, () => toolImpls[call.name](call.input))),\n * )\n * ```\n */\nexport async function runTool<T>(\n ctx: ToolRunContext,\n call: Pick<ToolCall, 'id' | 'name' | 'input'>,\n fn: () => Promise<T>,\n options?: RunToolOptions,\n): Promise<T> {\n const captureIO = options?.captureIO === true\n const base = { toolName: call.name, toolCallId: call.id }\n const callEvent: ToolCallEvent = captureIO ? { ...base, input: call.input } : base\n ctx.emit('tool.call', callEvent)\n\n const start = Date.now()\n let value: T\n try {\n value = await ctx.within('tool.call', { toolCallId: call.id }, fn)\n } catch (error) {\n const failed: ToolResultEvent = {\n ...base,\n durationMs: Date.now() - start,\n // Consumers read `error !== undefined` as \"the tool failed\", so a thrown undefined is named.\n error: error === undefined ? new Error('tool threw undefined') : error,\n }\n ctx.emit('tool.result', failed)\n throw error\n }\n const succeeded: ToolResultEvent = captureIO\n ? { ...base, durationMs: Date.now() - start, result: value }\n : { ...base, durationMs: Date.now() - start }\n ctx.emit('tool.result', succeeded)\n return value\n}\n","/**\n * Shared LLM protocol types used by all harness LLM adapters.\n *\n * @packageDocumentation\n */\n\n/** A run of plain text inside a user message or a system prompt. */\nexport type TextBlock = {\n type: 'text'\n text: string\n /**\n * Marks the end of a cacheable prefix: the provider may cache everything up to and including\n * this block (the system prompt first, then the messages in order). Adapters whose provider\n * caches only at marked points (Anthropic) send it; adapters whose provider caches\n * automatically ignore it. A hint, not a guarantee: the provider decides whether a cache\n * entry is written or read (e.g. a minimum prefix length). Anthropic allows at most 4 points\n * per request (its `autoCache` option takes one of them); its adapter throws before\n * `\"llm.request\"` when there are more. See the adapter's README.\n */\n cachePoint?: true\n}\n\n/**\n * An image inside a user message.\n *\n * `data` is raw base64 with no `data:` prefix. `mediaType` is the MIME type\n * (e.g. `'image/png'`); Anthropic, OpenAI and Ollama throw on types they do not accept; Gemini passes any value through.\n */\nexport type ImageBlock = {\n type: 'image'\n data: string\n mediaType: string\n /**\n * Marks the end of a cacheable prefix: the provider may cache everything up to and including\n * this block (the system prompt first, then the messages in order). Adapters whose provider\n * caches only at marked points (Anthropic) send it; adapters whose provider caches\n * automatically ignore it. A hint, not a guarantee: the provider decides whether a cache\n * entry is written or read (e.g. a minimum prefix length). Anthropic allows at most 4 points\n * per request (its `autoCache` option takes one of them); its adapter throws before\n * `\"llm.request\"` when there are more. See the adapter's README.\n */\n cachePoint?: true\n}\n\n/** One piece of user-message content. */\nexport type ContentBlock = TextBlock | ImageBlock\n\n/**\n * A single turn in a conversation.\n *\n * - `user` — input from the human side: a string, or blocks mixing text and images.\n * An empty block array is allowed by the type but may be rejected by providers; don't send one.\n * - `assistant` — model reply, optionally carrying tool-call requests.\n * - `tool` — result returned to the model after a tool call.\n */\nexport type Message =\n | { role: 'user'; content: string | ContentBlock[] }\n | { role: 'assistant'; content?: string; toolCalls?: ToolCall[] }\n | { role: 'tool'; toolCallId: string; content: string }\n\n/**\n * Normalize user-message content to blocks.\n *\n * A string becomes one text block (an empty string stays one empty text block).\n * An array is returned as-is, same reference, with no validation.\n */\nexport function toBlocks(content: string | ContentBlock[]): ContentBlock[] {\n return typeof content === 'string' ? [{ type: 'text', text: content }] : content\n}\n\n/**\n * A tool the LLM may invoke during a conversation turn.\n *\n * `inputSchema` must be a valid JSON Schema object describing the tool's\n * input parameters (LLM adapters pass it to the provider as-is).\n */\nexport interface Tool {\n name: string\n description: string\n inputSchema: Record<string, unknown>\n}\n\n/**\n * A single tool-call request emitted by the LLM in an assistant turn.\n *\n * `id` is provider-assigned and must be echoed back in the corresponding\n * `role: 'tool'` message so the model can correlate results.\n */\nexport interface ToolCall {\n id: string\n name: string\n input: unknown\n}\n\n/**\n * Normalized response returned by every {@link LLM} adapter.\n *\n * `text` may be empty when `stopReason` is `'tool_use'`.\n * `toolCalls` is empty when `stopReason` is `'end'` or `'max_tokens'`.\n * `usage` is always present — all adapters must populate it.\n */\nexport interface LLMResponse {\n text: string\n toolCalls: ToolCall[]\n stopReason: 'end' | 'tool_use' | 'max_tokens'\n usage: {\n /**\n * Total input tokens of this call, cached tokens included. Compare it with\n * `contextWindowSize`. The uncached part, billed at the normal input price, is\n * `inputTokens − (cacheReadTokens ?? 0) − (cacheWriteTokens ?? 0)`.\n */\n inputTokens: number\n /** Number of output tokens generated by this call. */\n outputTokens: number\n /**\n * Input tokens read from the provider's prompt cache. Part of `inputTokens`, not added to\n * it. Absent when the adapter cannot know; `0` is a real count (no cache hit).\n */\n cacheReadTokens?: number\n /**\n * Input tokens written to the provider's prompt cache. Part of `inputTokens`, not added to\n * it. Absent when the adapter cannot know; `0` is a real count.\n */\n cacheWriteTokens?: number\n /**\n * `cacheWriteTokens` split by cache lifetime (TTL). Each key is the provider's TTL label\n * (for example `'5m'`, `'1h'`, `'30m'`); each value is the input tokens written with that\n * TTL. The values add up to `cacheWriteTokens`, which is then present too. Absent when the\n * adapter cannot tell the split. Which keys an adapter reports is in its README; a reported\n * key with `0` is a real count. Keys are strings because TTL labels differ by provider.\n */\n cacheWriteTokensByTtl?: Record<string, number>\n /**\n * The model's total context window capacity in tokens.\n * Present only when the adapter can determine it.\n * Absent when the adapter cannot determine capacity.\n */\n contextWindowSize?: number\n }\n}\n\n/**\n * Payload emitted by LLM adapters as an `\"llm.request\"` observer event,\n * immediately before the provider call is made.\n *\n * Each `invoke()` call that reaches this event is followed by exactly one of\n * `\"llm.response\"` ({@link LLMUsageEvent}) or `\"llm.error\"` ({@link LLMErrorEvent}),\n * carrying the same `callId`. A throw before this event emits nothing.\n *\n * `messages`, `tools` and `system` are opt-in — they may contain sensitive data. When present they\n * are the normalized harness shapes passed to `invoke()`, not the provider-translated\n * payloads. They are passed by reference; observers must not mutate them.\n */\nexport interface LLMRequestEvent {\n /**\n * Correlation id for this single `invoke()` call, minted per call by the adapter and\n * carried on the `\"llm.request\"` event and on the matching `\"llm.response\"` or\n * `\"llm.error\"` event, so an observer can pair them reliably even when concurrent\n * fork branches interleave.\n */\n callId: string\n modelId: string\n providerName: string\n messages?: Message[] // opt-in; the messages passed to invoke()\n tools?: Tool[] // opt-in; the tools passed to invoke()\n system?: string | TextBlock[] // opt-in; the system option passed to invoke()\n}\n\n/**\n * Token-usage payload emitted by LLM adapters as an `\"llm.response\"` observer event,\n * after a successful provider call.\n *\n * `tokens` carries the same numbers as the returned `LLMResponse.usage`: `input` = `inputTokens`,\n * `output` = `outputTokens`, `cacheRead` = `cacheReadTokens`, `cacheWrite` = `cacheWriteTokens`,\n * `cacheWriteByTtl` = `cacheWriteTokensByTtl` (present exactly when the usage field is). Mock\n * adapters report the `usage` of the response they return. `cacheWriteByTtl` may be the same\n * object as in `usage`; observers must not mutate it.\n *\n * `output` is opt-in — it may contain sensitive data; adapters default to omitting it. When\n * present it is the normalized {@link LLMResponse} that `invoke()` returns, passed by reference.\n * `contextWindowSize` mirrors `LLMResponse.usage.contextWindowSize` — present when the\n * adapter can determine it; absent otherwise.\n */\nexport interface LLMUsageEvent {\n /** Correlation id matching the `\"llm.request\"` event for the same `invoke()` call (see {@link LLMRequestEvent.callId}). */\n callId: string\n tokens: { input: number; output: number; cacheRead?: number; cacheWrite?: number; cacheWriteByTtl?: Record<string, number> }\n modelId: string\n stopReason: LLMResponse['stopReason']\n providerName: string\n output?: LLMResponse // opt-in; may be sensitive\n contextWindowSize?: number\n}\n\n/**\n * Payload emitted by LLM adapters as an `\"llm.error\"` observer event, when anything throws\n * after `\"llm.request\"` was emitted and before `\"llm.response\"` would be (the provider call,\n * response normalization). The adapter then rethrows the same value.\n *\n * When the call's `signal` aborted, `error` is `signal.reason`, unchanged. The caller can tell a\n * cancel from a failure reliably with `signal.aborted` (or `error === signal.reason`). An\n * observer, which does not see the signal, can use `error.name` only for the default reasons:\n * `AbortError` from `controller.abort()` with no argument, `TimeoutError` from\n * `AbortSignal.timeout()`; a custom reason (`controller.abort(new Error('x'))`,\n * `controller.abort('x')`) keeps its own name or has none.\n */\nexport interface LLMErrorEvent {\n /** Correlation id matching the `\"llm.request\"` event for the same `invoke()` call (see {@link LLMRequestEvent.callId}). */\n callId: string\n modelId: string\n providerName: string\n /** The thrown value, unchanged; any value, including `undefined`. */\n error: unknown\n}\n\n/**\n * Payload emitted on `\"tool.call\"` before a tool is dispatched.\n *\n * `input` is opt-in — it may contain sensitive data; omit by default.\n */\nexport interface ToolCallEvent {\n toolName: string\n toolCallId: string\n input?: unknown\n}\n\n/**\n * Payload emitted on `\"tool.result\"` after a tool completes or throws.\n *\n * `result` is opt-in — it may contain sensitive data; omit by default.\n * `error` is set whenever the tool threw (the thrown value) and absent on success; it is how\n * consumers tell a failed call from a successful one, so it is not gated.\n */\nexport interface ToolResultEvent {\n toolName: string\n toolCallId: string\n durationMs: number\n result?: unknown\n error?: unknown\n}\n\n/** Options of {@link LLM.invoke}. */\nexport interface InvokeOptions {\n /** Tools the model may call. */\n tools?: Tool[]\n /**\n * Cancels the call. When the signal aborts, the adapter closes the HTTP request and `invoke()`\n * rejects with `signal.reason` (an `AbortError` `DOMException` for `controller.abort()`, a\n * `TimeoutError` for `AbortSignal.timeout()`), not the provider SDK's own error. If the signal\n * is already aborted when `invoke()` is called, it rejects with no observer event. If it aborts\n * after `\"llm.request\"`, `\"llm.error\"` is emitted with `error: signal.reason` first.\n *\n * Aborting frees the caller and closes the connection. It does not guarantee that the provider\n * stops the work or stops billing for it.\n *\n * The harness does not pass the run's signal for you. To let `run.stop()` cut the call, pass it\n * from the step: `ctx.llm.invoke(messages, { signal: ctx.signal })`.\n */\n signal?: AbortSignal\n /**\n * The system prompt for this call. Each adapter sends it in its provider's native system slot.\n * A string, or text blocks (a block may carry `cachePoint`). Not stored in the conversation:\n * pass it on every call.\n */\n system?: string | TextBlock[]\n}\n\n/**\n * Core interface every LLM adapter must implement.\n *\n * @example\n * ```ts\n * const response = await llm.invoke(messages, { tools: [myTool], system: 'Be brief.', signal: ctx.signal })\n * ```\n */\nexport interface LLM {\n /**\n * Send a message list to the LLM and return a normalized response.\n *\n * @param messages - Conversation history, oldest first.\n * @param options - See {@link InvokeOptions}.\n * @param options.tools - Tools the model may call.\n * @param options.signal - Cancels the call; `invoke()` then rejects with `signal.reason`.\n * @param options.system - The system prompt, sent in the provider's native system slot.\n */\n invoke(messages: Message[], options?: InvokeOptions): Promise<LLMResponse>\n}\n\nexport { runTool, type ToolRunContext, type RunToolOptions } from './run-tool.js'\n"],"mappings":";AAgCA,eAAsB,QACpB,KACA,MACA,IACA,SACY;AACZ,QAAM,YAAY,SAAS,cAAc;AACzC,QAAM,OAAO,EAAE,UAAU,KAAK,MAAM,YAAY,KAAK,GAAG;AACxD,QAAM,YAA2B,YAAY,EAAE,GAAG,MAAM,OAAO,KAAK,MAAM,IAAI;AAC9E,MAAI,KAAK,aAAa,SAAS;AAE/B,QAAM,QAAQ,KAAK,IAAI;AACvB,MAAI;AACJ,MAAI;AACF,YAAQ,MAAM,IAAI,OAAO,aAAa,EAAE,YAAY,KAAK,GAAG,GAAG,EAAE;AAAA,EACnE,SAAS,OAAO;AACd,UAAM,SAA0B;AAAA,MAC9B,GAAG;AAAA,MACH,YAAY,KAAK,IAAI,IAAI;AAAA;AAAA,MAEzB,OAAO,UAAU,SAAY,IAAI,MAAM,sBAAsB,IAAI;AAAA,IACnE;AACA,QAAI,KAAK,eAAe,MAAM;AAC9B,UAAM;AAAA,EACR;AACA,QAAM,YAA6B,YAC/B,EAAE,GAAG,MAAM,YAAY,KAAK,IAAI,IAAI,OAAO,QAAQ,MAAM,IACzD,EAAE,GAAG,MAAM,YAAY,KAAK,IAAI,IAAI,MAAM;AAC9C,MAAI,KAAK,eAAe,SAAS;AACjC,SAAO;AACT;;;ACIO,SAAS,SAAS,SAAkD;AACzE,SAAO,OAAO,YAAY,WAAW,CAAC,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,IAAI;AAC3E;","names":[]}
|