@noetaris/harness-types 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,20 +13,21 @@ This package defines the common TypeScript interfaces that all LLM adapter packa
13
13
  | Type | Description |
14
14
  |------|-------------|
15
15
  | `Message` | Discriminated union: `{ role: 'user'; content: string \| ContentBlock[] }` \| `{ role: 'assistant'; content?: string; toolCalls?: ToolCall[] }` \| `{ role: 'tool'; toolCallId: string; content: string }` |
16
- | `TextBlock` | `{ type: 'text'; text: string }` — a run of text inside a user message |
17
- | `ImageBlock` | `{ type: 'image'; data: string; mediaType: string }` — an image inside a user message; `data` is raw base64 (see [Image input](#image-input)) |
16
+ | `TextBlock` | `{ type: 'text'; text: string; cachePoint?: true }` — a run of text inside a user message or a system prompt (`cachePoint`: see [Prompt caching](#prompt-caching)) |
17
+ | `ImageBlock` | `{ type: 'image'; data: string; mediaType: string; cachePoint?: true }` — an image inside a user message; `data` is raw base64 (see [Image input](#image-input)) |
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; contextWindowSize?: number } }` — `usage` is always present; all adapters must populate it |
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 |
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)) |
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) |
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) |
27
27
  | `ToolRunContext` | `{ emit(name, payload?): void; within<T>(type, payload, fn): Promise<T> }` — the part of a step `ctx` that `runTool` needs |
28
28
  | `RunToolOptions` | `{ captureIO?: boolean }` — options of `runTool` |
29
- | `LLM` | `{ invoke(messages: Message[], options?: { tools?: Tool[] }): Promise<LLMResponse> }` |
29
+ | `InvokeOptions` | `{ tools?: Tool[]; signal?: AbortSignal; system?: string \| TextBlock[] }` — options of `invoke()` |
30
+ | `LLM` | `{ invoke(messages: Message[], options?: InvokeOptions): Promise<LLMResponse> }` |
30
31
 
31
32
  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).
32
33
 
@@ -52,8 +53,94 @@ const message: Message = {
52
53
  - `content: []` is allowed by the type, but providers may reject it. Don't send an empty array.
53
54
  - `mediaType` is a plain string. Anthropic, OpenAI and Ollama check it and throw on types they do not accept. Gemini passes any value through and the provider decides. The accepted types differ per adapter. See the README of your adapter: [Anthropic](https://github.com/noetaris-lab/harness-anthropic), [OpenAI](https://github.com/noetaris-lab/harness-openai), [Gemini](https://github.com/noetaris-lab/harness-google), [Ollama](https://github.com/noetaris-lab/harness-ollama).
54
55
  - The model must be able to read images. The adapters do not check this.
56
+ - An image block can carry `cachePoint: true`, like a text block. See [Prompt caching](#prompt-caching).
55
57
  - 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).
56
58
 
59
+ ## Cancelling a call
60
+
61
+ Pass an `AbortSignal` as `signal` to stop waiting for a call:
62
+
63
+ ```ts
64
+ const response = await llm.invoke(messages, { signal: AbortSignal.timeout(30_000) })
65
+ ```
66
+
67
+ - When the signal aborts, the adapter closes the HTTP request and `invoke()` rejects with `signal.reason`, not the provider SDK's own error. That is an `AbortError` `DOMException` for `controller.abort()` and a `TimeoutError` for `AbortSignal.timeout()`.
68
+ - Already aborted when `invoke()` is called: it rejects with no observer event.
69
+ - Aborted after `"llm.request"`: `"llm.error"` is emitted with `error: signal.reason`, then `invoke()` rejects with the same value.
70
+ - Aborting frees the caller and closes the connection. It does **not** guarantee that the provider stops the work or stops billing for it.
71
+
72
+ To tell a cancel from a failure, the caller checks `signal.aborted` (or `error === signal.reason`). An observer of `"llm.error"` does not see the signal. It can use `error.name` only for the default reasons: `AbortError` from `controller.abort()` with no argument, `TimeoutError` from `AbortSignal.timeout()`. A custom reason (`controller.abort(new Error('x'))`, `controller.abort('x')`) keeps its own name or has none.
73
+
74
+ **Inside a step**, the harness does not pass the run's signal for you. To let `run.stop()` cut the call, pass it yourself:
75
+
76
+ ```ts
77
+ const response = await ctx.llm.invoke(state.messages, { signal: ctx.signal })
78
+ ```
79
+
80
+ Why it is not automatic: without it, `run.stop()` waits for the step to finish and pauses at the step boundary. With it, the step's call rejects, so the step fails with `$error` and may take its `onError` route. Each step chooses which behaviour it wants.
81
+
82
+ ## Prompt caching
83
+
84
+ Some providers can reuse the start of a prompt they have seen before (a *prefix*) and bill it at a lower price.
85
+
86
+ **Cache points.** Mark the end of a cacheable prefix with `cachePoint: true` on a text or image block:
87
+
88
+ ```ts
89
+ const messages: Message[] = [{
90
+ role: 'user',
91
+ content: [
92
+ { type: 'text', text: longDocument, cachePoint: true },
93
+ { type: 'text', text: 'Summarise the document.' },
94
+ ],
95
+ }]
96
+ ```
97
+
98
+ - The provider may cache everything up to and including the marked block: the system prompt first, then the messages in order.
99
+ - Adapters whose provider caches only at marked points (Anthropic) send it. Adapters whose provider caches automatically (OpenAI, Gemini) ignore it, and so does Ollama.
100
+ - It is a hint, not a guarantee. The provider decides whether a cache entry is written or read (for example, a prefix may be too short to cache).
101
+ - Anthropic allows at most 4 points per request. Its adapter throws before `"llm.request"` when there are more. With the adapter's `autoCache` option on, the automatic marker takes one of the 4.
102
+ - `cachePoint` takes only `true`. Omit the field for no point.
103
+ - Tool messages (`role: 'tool'`) cannot be marked.
104
+
105
+ **Usage fields.** `LLMResponse.usage` (and the `"llm.response"` event's `tokens`) report:
106
+
107
+ | Field | Event field | Meaning |
108
+ |---|---|---|
109
+ | `inputTokens` | `input` | Total input tokens of the call, **cached tokens included**. Compare it with `contextWindowSize`. |
110
+ | `cacheReadTokens?` | `cacheRead?` | Input tokens read from the cache. A part of `inputTokens`, not added to it. |
111
+ | `cacheWriteTokens?` | `cacheWrite?` | Input tokens written to the cache. A part of `inputTokens`, not added to it. |
112
+
113
+ An absent cache field means the adapter cannot know. A present `0` is a real count (no cache hit, nothing written).
114
+
115
+ The uncached part, billed at the normal input price, is:
116
+
117
+ ```
118
+ uncached = inputTokens − (cacheReadTokens ?? 0) − (cacheWriteTokens ?? 0)
119
+ ```
120
+
121
+ Which fields each adapter reports is in its README.
122
+
123
+ ## System prompt
124
+
125
+ Pass the system prompt as the `system` option, a string or text blocks:
126
+
127
+ ```ts
128
+ await llm.invoke(messages, { system: 'You are a careful reviewer. Answer in English.' })
129
+
130
+ await llm.invoke(messages, {
131
+ system: [
132
+ { type: 'text', text: styleGuide, cachePoint: true },
133
+ { type: 'text', text: 'Review the diff below.' },
134
+ ],
135
+ })
136
+ ```
137
+
138
+ - Each adapter sends it in its provider's native system slot. See your adapter's README for the mapping.
139
+ - Text only: image blocks are not accepted.
140
+ - A block of the system prompt can carry `cachePoint`. The system prompt is the most stable prefix, so it is a good place for one.
141
+ - It is an option, not a `Message` role. It is not stored in the conversation, so pass it on every call. A session resumed later uses the prompt your code passes then, not an old one saved in state. There is exactly one system prompt per call.
142
+ - On `"llm.request"`, `system` is opt-in like `messages`: adapters include it only when input capture is on.
143
+
57
144
  ## Reporting tool calls
58
145
 
59
146
  `runTool(ctx, call, fn, options?)` runs one tool call and reports it. It emits `"tool.call"`,
@@ -93,15 +180,26 @@ Requires Node.js ≥ 22.
93
180
  ## Usage
94
181
 
95
182
  ```ts
96
- import type { LLM, Message, LLMResponse, Tool } from '@noetaris/harness-types'
183
+ import type { LLM, Message, LLMResponse, InvokeOptions } from '@noetaris/harness-types'
97
184
 
98
185
  class MyAdapter implements LLM {
99
- async invoke(messages: Message[], options?: { tools?: Tool[] }): Promise<LLMResponse> {
186
+ async invoke(messages: Message[], options?: InvokeOptions): Promise<LLMResponse> {
187
+ // options.tools, options.signal, options.system
100
188
  // ...
101
189
  }
102
190
  }
103
191
  ```
104
192
 
193
+ A caller, inside a step:
194
+
195
+ ```ts
196
+ const response = await ctx.llm.invoke(state.messages, {
197
+ system: 'You are a helpful assistant.',
198
+ tools,
199
+ signal: ctx.signal,
200
+ })
201
+ ```
202
+
105
203
  An adapter that handles image input can use `toBlocks()` to treat a string and an array the same way:
106
204
 
107
205
  ```ts
@@ -124,6 +222,7 @@ for (const block of toBlocks(message.content)) {
124
222
  - [`@noetaris/harness-anthropic`](https://github.com/noetaris-lab/harness-anthropic) — Anthropic Claude adapter
125
223
  - [`@noetaris/harness-openai`](https://github.com/noetaris-lab/harness-openai) — OpenAI adapter
126
224
  - [`@noetaris/harness-google`](https://github.com/noetaris-lab/harness-google) — Google Gemini adapter
225
+ - [`@noetaris/harness-ollama`](https://github.com/noetaris-lab/harness-ollama) — Ollama adapter
127
226
 
128
227
  ## License
129
228
 
package/dist/index.d.ts CHANGED
@@ -33,10 +33,20 @@ declare function runTool<T>(ctx: ToolRunContext, call: Pick<ToolCall, 'id' | 'na
33
33
  *
34
34
  * @packageDocumentation
35
35
  */
36
- /** A run of plain text inside a user message. */
36
+ /** A run of plain text inside a user message or a system prompt. */
37
37
  type TextBlock = {
38
38
  type: 'text';
39
39
  text: string;
40
+ /**
41
+ * Marks the end of a cacheable prefix: the provider may cache everything up to and including
42
+ * this block (the system prompt first, then the messages in order). Adapters whose provider
43
+ * caches only at marked points (Anthropic) send it; adapters whose provider caches
44
+ * automatically ignore it. A hint, not a guarantee: the provider decides whether a cache
45
+ * entry is written or read (e.g. a minimum prefix length). Anthropic allows at most 4 points
46
+ * per request (its `autoCache` option takes one of them); its adapter throws before
47
+ * `"llm.request"` when there are more. See the adapter's README.
48
+ */
49
+ cachePoint?: true;
40
50
  };
41
51
  /**
42
52
  * An image inside a user message.
@@ -48,6 +58,16 @@ type ImageBlock = {
48
58
  type: 'image';
49
59
  data: string;
50
60
  mediaType: string;
61
+ /**
62
+ * Marks the end of a cacheable prefix: the provider may cache everything up to and including
63
+ * this block (the system prompt first, then the messages in order). Adapters whose provider
64
+ * caches only at marked points (Anthropic) send it; adapters whose provider caches
65
+ * automatically ignore it. A hint, not a guarantee: the provider decides whether a cache
66
+ * entry is written or read (e.g. a minimum prefix length). Anthropic allows at most 4 points
67
+ * per request (its `autoCache` option takes one of them); its adapter throws before
68
+ * `"llm.request"` when there are more. See the adapter's README.
69
+ */
70
+ cachePoint?: true;
51
71
  };
52
72
  /** One piece of user-message content. */
53
73
  type ContentBlock = TextBlock | ImageBlock;
@@ -112,10 +132,24 @@ interface LLMResponse {
112
132
  toolCalls: ToolCall[];
113
133
  stopReason: 'end' | 'tool_use' | 'max_tokens';
114
134
  usage: {
115
- /** Number of input tokens consumed by this call. */
135
+ /**
136
+ * Total input tokens of this call, cached tokens included. Compare it with
137
+ * `contextWindowSize`. The uncached part, billed at the normal input price, is
138
+ * `inputTokens − (cacheReadTokens ?? 0) − (cacheWriteTokens ?? 0)`.
139
+ */
116
140
  inputTokens: number;
117
141
  /** Number of output tokens generated by this call. */
118
142
  outputTokens: number;
143
+ /**
144
+ * Input tokens read from the provider's prompt cache. Part of `inputTokens`, not added to
145
+ * it. Absent when the adapter cannot know; `0` is a real count (no cache hit).
146
+ */
147
+ cacheReadTokens?: number;
148
+ /**
149
+ * Input tokens written to the provider's prompt cache. Part of `inputTokens`, not added to
150
+ * it. Absent when the adapter cannot know; `0` is a real count.
151
+ */
152
+ cacheWriteTokens?: number;
119
153
  /**
120
154
  * The model's total context window capacity in tokens.
121
155
  * Present only when the adapter can determine it.
@@ -132,7 +166,7 @@ interface LLMResponse {
132
166
  * `"llm.response"` ({@link LLMUsageEvent}) or `"llm.error"` ({@link LLMErrorEvent}),
133
167
  * carrying the same `callId`. A throw before this event emits nothing.
134
168
  *
135
- * `messages` and `tools` are opt-in — they may contain sensitive data. When present they
169
+ * `messages`, `tools` and `system` are opt-in — they may contain sensitive data. When present they
136
170
  * are the normalized harness shapes passed to `invoke()`, not the provider-translated
137
171
  * payloads. They are passed by reference; observers must not mutate them.
138
172
  */
@@ -148,12 +182,16 @@ interface LLMRequestEvent {
148
182
  providerName: string;
149
183
  messages?: Message[];
150
184
  tools?: Tool[];
185
+ system?: string | TextBlock[];
151
186
  }
152
187
  /**
153
188
  * Token-usage payload emitted by LLM adapters as an `"llm.response"` observer event,
154
189
  * after a successful provider call.
155
190
  *
156
- * Adapters that cannot report real token counts (e.g. mock adapters) emit `{ input: 0, output: 0 }`.
191
+ * `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 adapters report the `usage` of the response
194
+ * they return.
157
195
  *
158
196
  * `output` is opt-in — it may contain sensitive data; adapters default to omitting it. When
159
197
  * present it is the normalized {@link LLMResponse} that `invoke()` returns, passed by reference.
@@ -166,6 +204,8 @@ interface LLMUsageEvent {
166
204
  tokens: {
167
205
  input: number;
168
206
  output: number;
207
+ cacheRead?: number;
208
+ cacheWrite?: number;
169
209
  };
170
210
  modelId: string;
171
211
  stopReason: LLMResponse['stopReason'];
@@ -177,6 +217,13 @@ interface LLMUsageEvent {
177
217
  * Payload emitted by LLM adapters as an `"llm.error"` observer event, when anything throws
178
218
  * after `"llm.request"` was emitted and before `"llm.response"` would be (the provider call,
179
219
  * response normalization). The adapter then rethrows the same value.
220
+ *
221
+ * When the call's `signal` aborted, `error` is `signal.reason`, unchanged. The caller can tell a
222
+ * cancel from a failure reliably with `signal.aborted` (or `error === signal.reason`). An
223
+ * observer, which does not see the signal, can use `error.name` only for the default reasons:
224
+ * `AbortError` from `controller.abort()` with no argument, `TimeoutError` from
225
+ * `AbortSignal.timeout()`; a custom reason (`controller.abort(new Error('x'))`,
226
+ * `controller.abort('x')`) keeps its own name or has none.
180
227
  */
181
228
  interface LLMErrorEvent {
182
229
  /** Correlation id matching the `"llm.request"` event for the same `invoke()` call (see {@link LLMRequestEvent.callId}). */
@@ -210,12 +257,37 @@ interface ToolResultEvent {
210
257
  result?: unknown;
211
258
  error?: unknown;
212
259
  }
260
+ /** Options of {@link LLM.invoke}. */
261
+ interface InvokeOptions {
262
+ /** Tools the model may call. */
263
+ tools?: Tool[];
264
+ /**
265
+ * Cancels the call. When the signal aborts, the adapter closes the HTTP request and `invoke()`
266
+ * rejects with `signal.reason` (an `AbortError` `DOMException` for `controller.abort()`, a
267
+ * `TimeoutError` for `AbortSignal.timeout()`), not the provider SDK's own error. If the signal
268
+ * is already aborted when `invoke()` is called, it rejects with no observer event. If it aborts
269
+ * after `"llm.request"`, `"llm.error"` is emitted with `error: signal.reason` first.
270
+ *
271
+ * Aborting frees the caller and closes the connection. It does not guarantee that the provider
272
+ * stops the work or stops billing for it.
273
+ *
274
+ * The harness does not pass the run's signal for you. To let `run.stop()` cut the call, pass it
275
+ * from the step: `ctx.llm.invoke(messages, { signal: ctx.signal })`.
276
+ */
277
+ signal?: AbortSignal;
278
+ /**
279
+ * The system prompt for this call. Each adapter sends it in its provider's native system slot.
280
+ * A string, or text blocks (a block may carry `cachePoint`). Not stored in the conversation:
281
+ * pass it on every call.
282
+ */
283
+ system?: string | TextBlock[];
284
+ }
213
285
  /**
214
286
  * Core interface every LLM adapter must implement.
215
287
  *
216
288
  * @example
217
289
  * ```ts
218
- * const response = await llm.invoke(messages, { tools: [myTool] })
290
+ * const response = await llm.invoke(messages, { tools: [myTool], system: 'Be brief.', signal: ctx.signal })
219
291
  * ```
220
292
  */
221
293
  interface LLM {
@@ -223,11 +295,12 @@ interface LLM {
223
295
  * Send a message list to the LLM and return a normalized response.
224
296
  *
225
297
  * @param messages - Conversation history, oldest first.
226
- * @param options.tools - Optional tools the model may invoke.
298
+ * @param options - See {@link InvokeOptions}.
299
+ * @param options.tools - Tools the model may call.
300
+ * @param options.signal - Cancels the call; `invoke()` then rejects with `signal.reason`.
301
+ * @param options.system - The system prompt, sent in the provider's native system slot.
227
302
  */
228
- invoke(messages: Message[], options?: {
229
- tools?: Tool[];
230
- }): Promise<LLMResponse>;
303
+ invoke(messages: Message[], options?: InvokeOptions): Promise<LLMResponse>;
231
304
  }
232
305
 
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 };
306
+ export { type ContentBlock, type ImageBlock, type InvokeOptions, 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.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. */\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":[]}
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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetaris/harness-types",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Shared LLM type contract for the @noetaris/harness ecosystem",
5
5
  "type": "module",
6
6
  "repository": {