@noetaris/harness-types 0.6.0 → 0.8.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 +36 -5
- package/dist/index.d.ts +69 -21
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ This package defines the common TypeScript interfaces that all LLM adapter packa
|
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
|
-
`@noetaris/harness-types` is a types package with zero dependencies and
|
|
9
|
+
`@noetaris/harness-types` is a types package with zero dependencies and two small runtime helpers, `toBlocks()` and `runTool()`. It provides the canonical definitions for messages, tools, and the `LLM` interface — the surface that all adapters (Anthropic, OpenAI, Gemini, etc.) must satisfy.
|
|
10
10
|
|
|
11
11
|
## Types
|
|
12
12
|
|
|
@@ -19,13 +19,16 @@ This package defines the common TypeScript interfaces that all LLM adapter packa
|
|
|
19
19
|
| `Tool` | `{ name: string; description: string; inputSchema: Record<string, unknown> }` |
|
|
20
20
|
| `ToolCall` | `{ id: string; name: string; input: unknown }` |
|
|
21
21
|
| `LLMResponse` | `{ text: string; toolCalls: ToolCall[]; stopReason: 'end' \| 'tool_use' \| 'max_tokens'; usage: { inputTokens: number; outputTokens: number; contextWindowSize?: number } }` — `usage` is always present; all adapters must populate it |
|
|
22
|
-
| `LLMRequestEvent` | `"llm.request"` payload: `{ modelId: string; providerName: string; messages?:
|
|
23
|
-
| `LLMUsageEvent` | `"llm.response"` payload: `{ tokens: { input: number; output: number }; modelId: string; stopReason: LLMResponse['stopReason']; providerName: string; output?:
|
|
22
|
+
| `LLMRequestEvent` | `"llm.request"` payload: `{ callId: string; modelId: string; providerName: string; messages?: Message[]; tools?: Tool[] }` (`messages`/`tools` 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 }; modelId: string; stopReason: LLMResponse['stopReason']; providerName: string; output?: LLMResponse; contextWindowSize?: number }` (`output` opt-in) |
|
|
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 |
|
|
24
25
|
| `ToolCallEvent` | `"tool.call"` payload: `{ toolName: string; toolCallId: string; input?: unknown }` (`input` opt-in) |
|
|
25
|
-
| `ToolResultEvent` | `"tool.result"` payload: `{ toolName: string; toolCallId: string; durationMs: number; result?: unknown; error?: unknown }` (`result
|
|
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) |
|
|
27
|
+
| `ToolRunContext` | `{ emit(name, payload?): void; within<T>(type, payload, fn): Promise<T> }` — the part of a step `ctx` that `runTool` needs |
|
|
28
|
+
| `RunToolOptions` | `{ captureIO?: boolean }` — options of `runTool` |
|
|
26
29
|
| `LLM` | `{ invoke(messages: Message[], options?: { tools?: Tool[] }): Promise<LLMResponse> }` |
|
|
27
30
|
|
|
28
|
-
The
|
|
31
|
+
The runtime exports are `toBlocks(content: string | ContentBlock[]): ContentBlock[]`, which turns a string into a single text block and returns an array as it is, and [`runTool`](#reporting-tool-calls).
|
|
29
32
|
|
|
30
33
|
## Image input
|
|
31
34
|
|
|
@@ -51,6 +54,34 @@ const message: Message = {
|
|
|
51
54
|
- The model must be able to read images. The adapters do not check this.
|
|
52
55
|
- Large images in session state are expensive to store. See [Large media in session state](https://github.com/noetaris-lab/harness/blob/main/LIMITATIONS.md).
|
|
53
56
|
|
|
57
|
+
## Reporting tool calls
|
|
58
|
+
|
|
59
|
+
`runTool(ctx, call, fn, options?)` runs one tool call and reports it. It emits `"tool.call"`,
|
|
60
|
+
runs `fn` inside `ctx.within('tool.call', { toolCallId })`, then emits `"tool.result"` with
|
|
61
|
+
`durationMs`. If `fn` throws, `"tool.result"` carries the thrown value as `error` and the same
|
|
62
|
+
value is rethrown. It returns `fn`'s value.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { runTool } from '@noetaris/harness-types'
|
|
66
|
+
|
|
67
|
+
// inside a step
|
|
68
|
+
const outputs = await Promise.all(
|
|
69
|
+
state.toolCalls.map(call => runTool(ctx, call, () => toolImpls[call.name](call.input))),
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `ctx` is the step's `ctx` from `@noetaris/harness` 0.7 or later, or any object with `emit` and
|
|
74
|
+
`within` (see `ToolRunContext`). With an OTel observer, the tool's span is the active span while
|
|
75
|
+
`fn` runs, so a sub agent or an instrumented HTTP call made by the tool nests under it. See the
|
|
76
|
+
[OTel bridge](https://github.com/noetaris-lab/harness-otel) and its
|
|
77
|
+
[GenAI extension](https://github.com/noetaris-lab/harness-otel-genai), which opens the `execute_tool` span.
|
|
78
|
+
- `options.captureIO` (default `false`) puts the call's `input` on `"tool.call"` and the tool's
|
|
79
|
+
result on `"tool.result"`. They may contain sensitive data. The GenAI extension records them
|
|
80
|
+
only when its `captureToolIO` option is on as well.
|
|
81
|
+
- `"tool.result"` always carries `error` when the tool threw, whatever `captureIO` says; a thrown
|
|
82
|
+
`undefined` is reported as `Error('tool threw undefined')`.
|
|
83
|
+
- Calls made at the same time keep their own events, told apart by `toolCallId`.
|
|
84
|
+
|
|
54
85
|
## Installation
|
|
55
86
|
|
|
56
87
|
```sh
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The part of a step ctx {@link runTool} needs. The `@noetaris/harness` step ctx (0.7 and
|
|
3
|
+
* later) satisfies it structurally.
|
|
4
|
+
*/
|
|
5
|
+
interface ToolRunContext {
|
|
6
|
+
emit(name: string, payload?: unknown): void;
|
|
7
|
+
within<T>(type: string, payload: unknown, fn: () => Promise<T>): Promise<T>;
|
|
8
|
+
}
|
|
9
|
+
interface RunToolOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Put the tool's `input` on `"tool.call"` and its `result` on `"tool.result"`. They may
|
|
12
|
+
* contain sensitive data. Default: `false`.
|
|
13
|
+
*/
|
|
14
|
+
captureIO?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Run one tool call and report it: emits `"tool.call"`, runs `fn` inside the observer's scope
|
|
18
|
+
* for that event (`ctx.within('tool.call', { toolCallId })`), then emits `"tool.result"` with
|
|
19
|
+
* `durationMs` — and `error` when `fn` threw, in which case the same value is rethrown.
|
|
20
|
+
* Returns `fn`'s value.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const outputs = await Promise.all(
|
|
25
|
+
* res.toolCalls.map(call => runTool(ctx, call, () => toolImpls[call.name](call.input))),
|
|
26
|
+
* )
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare function runTool<T>(ctx: ToolRunContext, call: Pick<ToolCall, 'id' | 'name' | 'input'>, fn: () => Promise<T>, options?: RunToolOptions): Promise<T>;
|
|
30
|
+
|
|
1
31
|
/**
|
|
2
32
|
* Shared LLM protocol types used by all harness LLM adapters.
|
|
3
33
|
*
|
|
@@ -98,32 +128,41 @@ interface LLMResponse {
|
|
|
98
128
|
* Payload emitted by LLM adapters as an `"llm.request"` observer event,
|
|
99
129
|
* immediately before the provider call is made.
|
|
100
130
|
*
|
|
101
|
-
* `
|
|
102
|
-
*
|
|
103
|
-
*
|
|
131
|
+
* Each `invoke()` call that reaches this event is followed by exactly one of
|
|
132
|
+
* `"llm.response"` ({@link LLMUsageEvent}) or `"llm.error"` ({@link LLMErrorEvent}),
|
|
133
|
+
* carrying the same `callId`. A throw before this event emits nothing.
|
|
134
|
+
*
|
|
135
|
+
* `messages` and `tools` are opt-in — they may contain sensitive data. When present they
|
|
136
|
+
* are the normalized harness shapes passed to `invoke()`, not the provider-translated
|
|
137
|
+
* payloads. They are passed by reference; observers must not mutate them.
|
|
104
138
|
*/
|
|
105
139
|
interface LLMRequestEvent {
|
|
106
|
-
modelId: string;
|
|
107
|
-
providerName: string;
|
|
108
|
-
messages?: unknown;
|
|
109
|
-
tools?: unknown;
|
|
110
140
|
/**
|
|
111
141
|
* Correlation id for this single `invoke()` call, minted per call by the adapter and
|
|
112
|
-
* carried on
|
|
113
|
-
* observer can pair them reliably even when concurrent
|
|
142
|
+
* carried on the `"llm.request"` event and on the matching `"llm.response"` or
|
|
143
|
+
* `"llm.error"` event, so an observer can pair them reliably even when concurrent
|
|
144
|
+
* fork branches interleave.
|
|
114
145
|
*/
|
|
115
|
-
callId
|
|
146
|
+
callId: string;
|
|
147
|
+
modelId: string;
|
|
148
|
+
providerName: string;
|
|
149
|
+
messages?: Message[];
|
|
150
|
+
tools?: Tool[];
|
|
116
151
|
}
|
|
117
152
|
/**
|
|
118
|
-
* Token-usage payload emitted by LLM adapters as an `"llm.response"` observer event
|
|
153
|
+
* Token-usage payload emitted by LLM adapters as an `"llm.response"` observer event,
|
|
154
|
+
* after a successful provider call.
|
|
119
155
|
*
|
|
120
156
|
* Adapters that cannot report real token counts (e.g. mock adapters) emit `{ input: 0, output: 0 }`.
|
|
121
157
|
*
|
|
122
|
-
* `output` is opt-in — it may contain sensitive data; adapters default to omitting it.
|
|
158
|
+
* `output` is opt-in — it may contain sensitive data; adapters default to omitting it. When
|
|
159
|
+
* present it is the normalized {@link LLMResponse} that `invoke()` returns, passed by reference.
|
|
123
160
|
* `contextWindowSize` mirrors `LLMResponse.usage.contextWindowSize` — present when the
|
|
124
161
|
* adapter can determine it; absent otherwise.
|
|
125
162
|
*/
|
|
126
163
|
interface LLMUsageEvent {
|
|
164
|
+
/** Correlation id matching the `"llm.request"` event for the same `invoke()` call (see {@link LLMRequestEvent.callId}). */
|
|
165
|
+
callId: string;
|
|
127
166
|
tokens: {
|
|
128
167
|
input: number;
|
|
129
168
|
output: number;
|
|
@@ -131,13 +170,21 @@ interface LLMUsageEvent {
|
|
|
131
170
|
modelId: string;
|
|
132
171
|
stopReason: LLMResponse['stopReason'];
|
|
133
172
|
providerName: string;
|
|
134
|
-
output?:
|
|
173
|
+
output?: LLMResponse;
|
|
135
174
|
contextWindowSize?: number;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Payload emitted by LLM adapters as an `"llm.error"` observer event, when anything throws
|
|
178
|
+
* after `"llm.request"` was emitted and before `"llm.response"` would be (the provider call,
|
|
179
|
+
* response normalization). The adapter then rethrows the same value.
|
|
180
|
+
*/
|
|
181
|
+
interface LLMErrorEvent {
|
|
182
|
+
/** Correlation id matching the `"llm.request"` event for the same `invoke()` call (see {@link LLMRequestEvent.callId}). */
|
|
183
|
+
callId: string;
|
|
184
|
+
modelId: string;
|
|
185
|
+
providerName: string;
|
|
186
|
+
/** The thrown value, unchanged; any value, including `undefined`. */
|
|
187
|
+
error: unknown;
|
|
141
188
|
}
|
|
142
189
|
/**
|
|
143
190
|
* Payload emitted on `"tool.call"` before a tool is dispatched.
|
|
@@ -152,8 +199,9 @@ interface ToolCallEvent {
|
|
|
152
199
|
/**
|
|
153
200
|
* Payload emitted on `"tool.result"` after a tool completes or throws.
|
|
154
201
|
*
|
|
155
|
-
* `result`
|
|
156
|
-
*
|
|
202
|
+
* `result` is opt-in — it may contain sensitive data; omit by default.
|
|
203
|
+
* `error` is set whenever the tool threw (the thrown value) and absent on success; it is how
|
|
204
|
+
* consumers tell a failed call from a successful one, so it is not gated.
|
|
157
205
|
*/
|
|
158
206
|
interface ToolResultEvent {
|
|
159
207
|
toolName: string;
|
|
@@ -182,4 +230,4 @@ interface LLM {
|
|
|
182
230
|
}): Promise<LLMResponse>;
|
|
183
231
|
}
|
|
184
232
|
|
|
185
|
-
export { type ContentBlock, type ImageBlock, type LLM, type LLMRequestEvent, type LLMResponse, type LLMUsageEvent, type Message, type TextBlock, type Tool, type ToolCall, type ToolCallEvent, type ToolResultEvent, toBlocks };
|
|
233
|
+
export { type ContentBlock, type ImageBlock, type LLM, type LLMErrorEvent, type LLMRequestEvent, type LLMResponse, type LLMUsageEvent, type Message, type RunToolOptions, type TextBlock, type Tool, type ToolCall, type ToolCallEvent, type ToolResultEvent, type ToolRunContext, runTool, toBlocks };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
|
+
// src/run-tool.ts
|
|
2
|
+
async function runTool(ctx, call, fn, options) {
|
|
3
|
+
const captureIO = options?.captureIO === true;
|
|
4
|
+
const base = { toolName: call.name, toolCallId: call.id };
|
|
5
|
+
const callEvent = captureIO ? { ...base, input: call.input } : base;
|
|
6
|
+
ctx.emit("tool.call", callEvent);
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
let value;
|
|
9
|
+
try {
|
|
10
|
+
value = await ctx.within("tool.call", { toolCallId: call.id }, fn);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
const failed = {
|
|
13
|
+
...base,
|
|
14
|
+
durationMs: Date.now() - start,
|
|
15
|
+
// Consumers read `error !== undefined` as "the tool failed", so a thrown undefined is named.
|
|
16
|
+
error: error === void 0 ? new Error("tool threw undefined") : error
|
|
17
|
+
};
|
|
18
|
+
ctx.emit("tool.result", failed);
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
const succeeded = captureIO ? { ...base, durationMs: Date.now() - start, result: value } : { ...base, durationMs: Date.now() - start };
|
|
22
|
+
ctx.emit("tool.result", succeeded);
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
1
26
|
// src/index.ts
|
|
2
27
|
function toBlocks(content) {
|
|
3
28
|
return typeof content === "string" ? [{ type: "text", text: content }] : content;
|
|
4
29
|
}
|
|
5
30
|
export {
|
|
31
|
+
runTool,
|
|
6
32
|
toBlocks
|
|
7
33
|
};
|
|
8
34
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\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. */\nexport type TextBlock = { type: 'text'; text: string }\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 = { type: 'image'; data: string; mediaType: string }\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 /** Number of input tokens consumed by this call. */\n inputTokens: number\n /** Number of output tokens generated by this call. */\n outputTokens: 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 * `messages` and `tools` are opt-in — they may contain sensitive data and are\n * typed as `unknown` because adapters pass their internal translated\n * representations; the framework never inspects this content.\n */\nexport interface LLMRequestEvent {\n modelId: string\n providerName: string\n messages?: unknown // opt-in; adapter's internal translated representation\n tools?: unknown // opt-in; adapter's internal translated representation\n /**\n * Correlation id for this single `invoke()` call, minted per call by the adapter and\n * carried on both the `\"llm.request\"` and the matching `\"llm.response\"` event, so an\n * observer can pair them reliably even when concurrent fork branches interleave.\n */\n callId?: string\n}\n\n/**\n * Token-usage payload emitted by LLM adapters as an `\"llm.response\"` observer event.\n *\n * Adapters that cannot report real token counts (e.g. mock adapters) emit `{ input: 0, output: 0 }`.\n *\n * `output` is opt-in — it may contain sensitive data; adapters default to omitting it.\n * `contextWindowSize` mirrors `LLMResponse.usage.contextWindowSize` — present when the\n * adapter can determine it; absent otherwise.\n */\nexport interface LLMUsageEvent {\n tokens: { input: number; output: number }\n modelId: string\n stopReason: LLMResponse['stopReason']\n providerName: string\n output?: unknown // opt-in; normalized LLMResponse or raw provider output; may be sensitive\n contextWindowSize?: number\n /**\n * Correlation id matching the `\"llm.request\"` event for the same `invoke()` call\n * (see {@link LLMRequestEvent.callId}). Lets an observer pair request and response.\n */\n callId?: string\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` and `error` are opt-in — they may contain sensitive data; omit by default.\n * Set `error` when the tool threw; leave it absent on success.\n */\nexport interface ToolResultEvent {\n toolName: string\n toolCallId: string\n durationMs: number\n result?: unknown\n error?: unknown\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] })\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.tools - Optional tools the model may invoke.\n */\n invoke(messages: Message[], options?: { tools?: Tool[] }): Promise<LLMResponse>\n}\n"],"mappings":";AAuCO,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. */\nexport type TextBlock = { type: 'text'; text: string }\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 = { type: 'image'; data: string; mediaType: string }\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 /** Number of input tokens consumed by this call. */\n inputTokens: number\n /** Number of output tokens generated by this call. */\n outputTokens: 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` and `tools` 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}\n\n/**\n * Token-usage payload emitted by LLM adapters as an `\"llm.response\"` observer event,\n * after a successful provider call.\n *\n * Adapters that cannot report real token counts (e.g. mock adapters) emit `{ input: 0, output: 0 }`.\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 }\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 */\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/**\n * Core interface every LLM adapter must implement.\n *\n * @example\n * ```ts\n * const response = await llm.invoke(messages, { tools: [myTool] })\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.tools - Optional tools the model may invoke.\n */\n invoke(messages: Message[], options?: { tools?: Tool[] }): 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;;;ACvBO,SAAS,SAAS,SAAkD;AACzE,SAAO,OAAO,YAAY,WAAW,CAAC,EAAE,MAAM,QAAQ,MAAM,QAAQ,CAAC,IAAI;AAC3E;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noetaris/harness-types",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Shared LLM type contract for the @noetaris/harness ecosystem",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"esbuild"
|
|
39
39
|
]
|
|
40
40
|
},
|
|
41
|
+
"dependencies": {},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@types/node": "^25.8.0",
|
|
43
44
|
"tsup": "^8.5.1",
|