@nylorun/harness 0.8.0-beta.1 → 0.9.0-beta.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@ All notable changes to `@nylorun/harness` are documented in this file.
4
4
 
5
5
  The project follows [Semantic Versioning](https://semver.org/). Before 1.0, the public API is experimental: breaking changes may occur in minor releases, while patch releases are reserved for compatible fixes.
6
6
 
7
+ ## [0.9.0-beta.1] - 2026-09-02
8
+
9
+ ### Added
10
+
11
+ - Public provider translator helpers under `@nylorun/harness/model/adapters` for OpenAI-compatible
12
+ Chat Completions, OpenAI Responses, and Anthropic Messages model loops.
13
+ - `MiddlewareManifest` and declared middleware contributions in `AgentManifest`, including static
14
+ instructions, tool metadata, and model controls for host tooling such as Studio.
15
+
7
16
  ## [0.8.0-beta.1] - 2026-08-31
8
17
 
9
18
  ### Breaking changes
@@ -93,3 +102,4 @@ The project follows [Semantic Versioning](https://semver.org/). Before 1.0, the
93
102
  [0.6.0-beta.1]: https://github.com/nylorun/harness/tree/main/harness
94
103
  [0.7.0-beta.1]: https://github.com/nylorun/harness/tree/main/harness
95
104
  [0.8.0-beta.1]: https://github.com/nylorun/harness/tree/main/harness
105
+ [0.9.0-beta.1]: https://github.com/nylorun/harness/tree/main/harness
package/README.md CHANGED
@@ -41,3 +41,44 @@ const result = await agent.run().input("Echo hello").completed;
41
41
 
42
42
  See [Examples](../examples/README.md) for complete agents and [CHANGELOG.md](./CHANGELOG.md) for
43
43
  release notes.
44
+
45
+ ## Model adapter translators
46
+
47
+ For OpenAI-compatible endpoints, keep transport and credentials in host code while Harness maps its
48
+ canonical model call and candidate:
49
+
50
+ ```ts
51
+ import { chatCompletionsAdapter } from "@nylorun/harness/model/adapters";
52
+
53
+ const adapter = chatCompletionsAdapter(async (body, call, { signal }) => {
54
+ const response = await fetch("https://example.com/v1/chat/completions", {
55
+ method: "POST",
56
+ headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
57
+ body: JSON.stringify({ model: "my-model", ...call.model?.config, ...body }),
58
+ signal,
59
+ });
60
+ if (!response.ok) throw new Error(await response.text());
61
+ return response.json();
62
+ });
63
+ ```
64
+
65
+ `toResponses` / `fromResponses` / `responsesAdapter` support direct OpenAI Responses transport.
66
+ `toMessages` / `fromMessages` / `anthropicAdapter` support direct Anthropic Messages transport;
67
+ `anthropicAdapter` requires an explicit `defaultMaxOutputTokens`. These translators support text and
68
+ JSON tool loops only. Provider-native continuation state, streaming, images, and cache controls stay
69
+ in application integrations.
70
+
71
+ ```ts
72
+ const responses = responsesAdapter((body, call, { signal }) =>
73
+ openai.responses.create({ model: "gpt-5.6", ...call.model?.config, ...body }, { signal }),
74
+ );
75
+
76
+ const messages = anthropicAdapter({
77
+ defaultMaxOutputTokens: 1_024,
78
+ send: (body, call, { signal }) =>
79
+ anthropic.messages.create(
80
+ { model: "claude-sonnet-4-5", ...call.model?.config, ...body },
81
+ { signal },
82
+ ),
83
+ });
84
+ ```
@@ -27,6 +27,7 @@ export function assembleAgent(middleware, invoke, identity) {
27
27
  id: item.id,
28
28
  handle: item.handle,
29
29
  ...(item.state === undefined ? {} : { state: item.state }),
30
+ ...(item.contributions === undefined ? {} : { contributions: item.contributions }),
30
31
  }));
31
32
  }
32
33
  }
@@ -99,6 +99,7 @@ function compileDeclaration(declaration) {
99
99
  const tools = copyItems(declaration.tools, declaration.id);
100
100
  const instructions = copyItems(declaration.instructions, declaration.id);
101
101
  const model = declaration.model;
102
+ const contributions = snapshotContributions(instructions?.items, tools?.items, model);
102
103
  const handle = async (request, next) => {
103
104
  if (tools)
104
105
  request.configuration.tools.set(tools.slot, tools.items);
@@ -114,8 +115,33 @@ function compileDeclaration(declaration) {
114
115
  ...(declaration.state === undefined
115
116
  ? {}
116
117
  : { state: declaration.state }),
118
+ ...(contributions === undefined ? {} : { contributions }),
117
119
  };
118
120
  }
121
+ function snapshotContributions(instructions, tools, model) {
122
+ const snapInstructions = instructions === undefined ? undefined : Object.freeze([...instructions]);
123
+ const snapTools = tools === undefined
124
+ ? undefined
125
+ : Object.freeze(tools.map((tool) => Object.freeze({
126
+ name: tool.name,
127
+ ...(tool.description === undefined ? {} : { description: tool.description }),
128
+ })));
129
+ const snapModel = model === undefined ? undefined : snapshotModel(model);
130
+ if (snapInstructions === undefined && snapTools === undefined && snapModel === undefined) {
131
+ return undefined;
132
+ }
133
+ return Object.freeze({
134
+ ...(snapInstructions === undefined ? {} : { instructions: snapInstructions }),
135
+ ...(snapTools === undefined ? {} : { tools: snapTools }),
136
+ ...(snapModel === undefined ? {} : { model: snapModel }),
137
+ });
138
+ }
139
+ function snapshotModel(model) {
140
+ return Object.freeze({
141
+ ...(model.id === undefined ? {} : { id: model.id }),
142
+ ...(model.controls === undefined ? {} : { controls: Object.freeze({ ...model.controls }) }),
143
+ });
144
+ }
119
145
  function copyItems(value, defaultSlot) {
120
146
  if (value === undefined)
121
147
  return undefined;
@@ -1,9 +1,20 @@
1
1
  import { deepFreeze } from "../utils/immutable.js";
2
2
  export function createManifest(input) {
3
- const middleware = input.middleware.map(({ id }) => ({ id }));
3
+ const middleware = input.middleware.map((item) => projectMiddleware(item));
4
4
  return deepFreeze({
5
5
  id: input.id,
6
6
  name: input.name,
7
7
  middleware,
8
8
  });
9
9
  }
10
+ function projectMiddleware(item) {
11
+ const contributions = item.contributions;
12
+ return {
13
+ id: item.id,
14
+ ...(contributions?.instructions === undefined
15
+ ? {}
16
+ : { instructions: contributions.instructions }),
17
+ ...(contributions?.tools === undefined ? {} : { tools: contributions.tools }),
18
+ ...(contributions?.model === undefined ? {} : { model: contributions.model }),
19
+ };
20
+ }
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Machine-readable error raised by Harness-owned code. */
2
- export type HarnessErrorCode = "agent.build-failed" | "agent.lifecycle-sealed" | "capability.state.create-failed" | "capability.state.undeclared" | "context.invalid-item" | "context.invalid-item-type" | "context.invalid-order" | "context.invalid-reason" | "context.invalid-slot" | "interaction.invalid" | "interaction.missing-resume" | "interaction.uncorrelated-resume" | "json.invalid-data" | "json.invalid-object" | "middleware.next-after-return" | "middleware.next-called-twice" | "middleware.request-mutators-revoked" | "model.candidate-missing" | "model.invalid-candidate" | "model.invalid-directive" | "configuration.duplicate-tool-name" | "configuration.invalid" | "configuration.invalid-instructions" | "configuration.invalid-order" | "configuration.invalid-reason" | "configuration.invalid-slot" | "configuration.invalid-tools" | "configuration.model-selection-conflict" | "response.invalid-replacement" | "session.invalid-seed" | "session.record-failed" | "session.stale-result" | "tool.invalid" | "tool.invalid-arguments" | "tool.invalid-name" | "tool.invalid-schema" | "tool.invalid-tool-result";
2
+ export type HarnessErrorCode = "agent.build-failed" | "agent.lifecycle-sealed" | "capability.state.create-failed" | "capability.state.undeclared" | "context.invalid-item" | "context.invalid-item-type" | "context.invalid-order" | "context.invalid-reason" | "context.invalid-slot" | "interaction.invalid" | "interaction.missing-resume" | "interaction.uncorrelated-resume" | "json.invalid-data" | "json.invalid-object" | "middleware.next-after-return" | "middleware.next-called-twice" | "middleware.request-mutators-revoked" | "model.candidate-missing" | "model.adapter-invalid-options" | "model.adapter-invalid-response" | "model.invalid-candidate" | "model.invalid-directive" | "configuration.duplicate-tool-name" | "configuration.invalid" | "configuration.invalid-instructions" | "configuration.invalid-order" | "configuration.invalid-reason" | "configuration.invalid-slot" | "configuration.invalid-tools" | "configuration.model-selection-conflict" | "response.invalid-replacement" | "session.invalid-seed" | "session.record-failed" | "session.stale-result" | "tool.invalid" | "tool.invalid-arguments" | "tool.invalid-name" | "tool.invalid-schema" | "tool.invalid-tool-result";
3
3
  export type HarnessErrorDetails = Readonly<Record<string, string | number | boolean>>;
4
4
  export interface HarnessErrorOptions {
5
5
  readonly cause?: unknown;
package/dist/index.d.ts CHANGED
@@ -4,9 +4,9 @@ export { HarnessError, isHarnessError } from "./errors.js";
4
4
  export type { HarnessErrorCode, HarnessErrorDetails, HarnessErrorOptions } from "./errors.js";
5
5
  export { BuiltAgent } from "./build/agent.js";
6
6
  export { middleware, model, tool } from "./build/helpers.js";
7
- export type { AgentManifest } from "./types/manifest.js";
7
+ export type { AgentManifest, MiddlewareManifest } from "./types/manifest.js";
8
+ export type { BoundMiddleware, CapabilityDeclaration, CapabilityItems, CapabilityState, MiddlewareContributions, StepInput, StepMiddleware, StepRequest, StepResponse, } from "./types/middleware.js";
8
9
  export type { ModelCandidate, ModelControls, ModelDirective, ModelEvidence, ModelFinishReason, ModelAdapter, ModelAdapterContext, ContextContributor, ContextMutationOptions, ContextSnapshot, ModelCall, ModelCallTool, ModelOutputBlock, PromptContentPart, PromptItem, ModelConfigurationContributor, ModelConfigurationInstruction, ModelConfigurationMutationOptions, ModelConfigurationSnapshot, ModelConfigurationTool, ModelRequest, ModelToolCall, ModelUsage, } from "./types/model.js";
9
- export type { BoundMiddleware, CapabilityDeclaration, CapabilityItems, CapabilityState, StepInput, StepMiddleware, StepRequest, StepResponse, } from "./types/middleware.js";
10
10
  export type { BuildDiagnostic, ContextItem, DeferredOutcome, JsonObject, JsonPrimitive, JsonValue, ObserveEvent, ObserveModelConfigurationSnapshot, ObserveModelRequested, ObserveSealedCall, ObserveToolSnapshot, Observer, Tripwire, } from "./types/shared.js";
11
11
  export type { ActiveExecutionRecord, ActiveInteractionExecutionRecord, ActiveModelExecutionRecord, ActiveToolCallRecord, ActiveToolsExecutionRecord, InteractionReply, InputCompletion, InputEvent, InputHandle, InputOptions, MessageInput, Session, SessionInput, SessionIdentity, SessionEvent, SessionOptions, SessionRecord, SessionRecorder, SessionRunOptions, SessionSeed, SeededSessionOptions, SessionSnapshot, TranscriptEntry, } from "./types/session.js";
12
12
  export type { BoundToolSchema, BoundToolDefinition, Interaction, RequiredInteraction, SealedToolCall, ToolContent, ToolDefinition, ToolExecutionContext, ToolExecutionResume, ToolObjectSchema, ToolOwner, ToolOutcome, ToolResult, } from "./types/tool.js";
@@ -0,0 +1,117 @@
1
+ import type { ModelAdapter, ModelAdapterContext, ModelCall, ModelCandidate } from "../types/model.js";
2
+ import type { JsonObject } from "../types/shared.js";
3
+ export type ChatCompletionsMessage = {
4
+ readonly role: "system" | "user";
5
+ readonly content: string;
6
+ } | {
7
+ readonly role: "assistant";
8
+ readonly content: string | null;
9
+ readonly tool_calls?: readonly ChatCompletionsToolCall[];
10
+ } | {
11
+ readonly role: "tool";
12
+ readonly tool_call_id: string;
13
+ readonly content: string;
14
+ };
15
+ export interface ChatCompletionsToolCall {
16
+ readonly id: string;
17
+ readonly type: "function";
18
+ readonly function: Readonly<{
19
+ name: string;
20
+ arguments: string;
21
+ }>;
22
+ }
23
+ export interface ChatCompletionsRequest {
24
+ readonly messages: readonly ChatCompletionsMessage[];
25
+ readonly tools?: readonly Readonly<{
26
+ type: "function";
27
+ function: Readonly<{
28
+ name: string;
29
+ description?: string;
30
+ parameters: JsonObject;
31
+ }>;
32
+ }>[];
33
+ readonly temperature?: number;
34
+ readonly max_completion_tokens?: number;
35
+ }
36
+ export interface ResponsesRequest {
37
+ readonly instructions?: string;
38
+ readonly input: readonly ResponsesInputItem[];
39
+ readonly tools?: readonly Readonly<{
40
+ type: "function";
41
+ name: string;
42
+ description?: string;
43
+ parameters: JsonObject;
44
+ }>[];
45
+ readonly temperature?: number;
46
+ readonly max_output_tokens?: number;
47
+ }
48
+ export type ResponsesInputItem = {
49
+ readonly type: "message";
50
+ readonly role: "user" | "assistant";
51
+ readonly content: string;
52
+ } | {
53
+ readonly type: "function_call";
54
+ readonly call_id: string;
55
+ readonly name: string;
56
+ readonly arguments: string;
57
+ } | {
58
+ readonly type: "function_call_output";
59
+ readonly call_id: string;
60
+ readonly output: string;
61
+ };
62
+ export interface MessagesRequest {
63
+ readonly system?: string;
64
+ readonly messages: readonly MessagesMessage[];
65
+ readonly tools?: readonly Readonly<{
66
+ name: string;
67
+ description?: string;
68
+ input_schema: JsonObject;
69
+ }>[];
70
+ readonly temperature?: number;
71
+ readonly max_tokens: number;
72
+ }
73
+ export type MessagesMessage = {
74
+ readonly role: "user";
75
+ readonly content: string | readonly MessagesToolResult[];
76
+ } | {
77
+ readonly role: "assistant";
78
+ readonly content: readonly MessagesAssistantPart[];
79
+ };
80
+ export type MessagesAssistantPart = {
81
+ readonly type: "text";
82
+ readonly text: string;
83
+ } | {
84
+ readonly type: "tool_use";
85
+ readonly id: string;
86
+ readonly name: string;
87
+ readonly input: JsonObject;
88
+ };
89
+ export interface MessagesToolResult {
90
+ readonly type: "tool_result";
91
+ readonly tool_use_id: string;
92
+ readonly content: string;
93
+ readonly is_error?: boolean;
94
+ }
95
+ export type AdapterSend<Request> = (request: Request, call: ModelCall, context: ModelAdapterContext) => Promise<unknown>;
96
+ export interface AnthropicAdapterOptions {
97
+ readonly defaultMaxOutputTokens: number;
98
+ readonly send: AdapterSend<MessagesRequest>;
99
+ }
100
+ /** Translate a Harness call to the OpenAI Chat Completions request shape. */
101
+ export declare function toChatCompletions(call: ModelCall): ChatCompletionsRequest;
102
+ /** Translate a Chat Completions response into a Harness candidate. */
103
+ export declare function fromChatCompletions(value: unknown): ModelCandidate;
104
+ /** Return a Harness adapter backed by an application-owned Chat Completions send function. */
105
+ export declare function chatCompletionsAdapter(send: AdapterSend<ChatCompletionsRequest>): ModelAdapter;
106
+ /** Translate a Harness call to the OpenAI Responses request shape. */
107
+ export declare function toResponses(call: ModelCall): ResponsesRequest;
108
+ /** Translate an OpenAI Responses response into a Harness candidate. */
109
+ export declare function fromResponses(value: unknown): ModelCandidate;
110
+ /** Return a Harness adapter backed by an application-owned Responses send function. */
111
+ export declare function responsesAdapter(send: AdapterSend<ResponsesRequest>): ModelAdapter;
112
+ /** Translate a Harness call to the Anthropic Messages request shape. */
113
+ export declare function toMessages(call: ModelCall, defaultMaxOutputTokens: number): MessagesRequest;
114
+ /** Translate an Anthropic Messages response into a Harness candidate. */
115
+ export declare function fromMessages(value: unknown): ModelCandidate;
116
+ /** Return a Harness adapter backed by an application-owned Anthropic Messages send function. */
117
+ export declare function anthropicAdapter(options: AnthropicAdapterOptions): ModelAdapter;
@@ -0,0 +1,431 @@
1
+ import { HarnessError } from "../errors.js";
2
+ import { copyJsonObject } from "../utils/immutable.js";
3
+ /** Translate a Harness call to the OpenAI Chat Completions request shape. */
4
+ export function toChatCompletions(call) {
5
+ const messages = call.prompt.map((item) => {
6
+ if (item.kind === "instructions")
7
+ return { role: "system", content: textOf(item) };
8
+ if (item.kind === "tool-result")
9
+ return { role: "tool", tool_call_id: item.toolCallId, content: textOf(item) };
10
+ if (item.kind === "message" && item.role === "assistant") {
11
+ const toolCalls = toolCallsOf(item.content).map((part) => ({
12
+ id: part.id,
13
+ type: "function",
14
+ function: { name: part.name, arguments: JSON.stringify(part.args) },
15
+ }));
16
+ const text = textOf(item);
17
+ return {
18
+ role: "assistant",
19
+ content: text === "" ? null : text,
20
+ ...(toolCalls.length === 0 ? {} : { tool_calls: toolCalls }),
21
+ };
22
+ }
23
+ return { role: "user", content: textOf(item) };
24
+ });
25
+ return {
26
+ messages,
27
+ ...(call.tools.length === 0
28
+ ? {}
29
+ : {
30
+ tools: call.tools.map((tool) => ({
31
+ type: "function",
32
+ function: {
33
+ name: tool.name,
34
+ ...(tool.description === undefined ? {} : { description: tool.description }),
35
+ parameters: tool.inputSchema,
36
+ },
37
+ })),
38
+ }),
39
+ ...chatControls(call),
40
+ };
41
+ }
42
+ /** Translate a Chat Completions response into a Harness candidate. */
43
+ export function fromChatCompletions(value) {
44
+ const response = record(value, "response");
45
+ const choices = array(response.choices, "response.choices");
46
+ if (choices.length === 0)
47
+ throw invalidResponse("response.choices must contain a choice", "response.choices");
48
+ const choice = record(choices[0], "response.choices[0]");
49
+ const message = record(choice.message, "response.choices[0].message");
50
+ const output = [];
51
+ if (typeof message.content === "string" && message.content !== "")
52
+ output.push({ type: "text", text: message.content });
53
+ if (typeof message.reasoning_content === "string" && message.reasoning_content !== "")
54
+ output.push({ type: "reasoning", text: message.reasoning_content });
55
+ for (const [index, raw] of optionalArray(message.tool_calls, "response.choices[0].message.tool_calls").entries()) {
56
+ const call = record(raw, `response.choices[0].message.tool_calls[${index}]`);
57
+ const fn = record(call.function, `response.choices[0].message.tool_calls[${index}].function`);
58
+ output.push({
59
+ type: "tool-call",
60
+ id: string(call.id, `response.choices[0].message.tool_calls[${index}].id`),
61
+ name: string(fn.name, `response.choices[0].message.tool_calls[${index}].function.name`),
62
+ ...argumentsOf(fn.arguments, `response.choices[0].message.tool_calls[${index}].function.arguments`),
63
+ });
64
+ }
65
+ return candidate({
66
+ output,
67
+ finishReason: chatFinishReason(choice.finish_reason, output),
68
+ usage: chatUsage(response.usage),
69
+ evidence: evidence(response),
70
+ });
71
+ }
72
+ /** Return a Harness adapter backed by an application-owned Chat Completions send function. */
73
+ export function chatCompletionsAdapter(send) {
74
+ return async (call, context) => fromChatCompletions(await send(toChatCompletions(call), call, context));
75
+ }
76
+ /** Translate a Harness call to the OpenAI Responses request shape. */
77
+ export function toResponses(call) {
78
+ const instructions = call.prompt.filter((item) => item.kind === "instructions").map(textOf);
79
+ const input = call.prompt.flatMap((item) => {
80
+ if (item.kind === "instructions")
81
+ return [];
82
+ if (item.kind === "tool-result")
83
+ return [{ type: "function_call_output", call_id: item.toolCallId, output: textOf(item) }];
84
+ if (item.kind === "message" && item.role === "assistant") {
85
+ const text = textOf(item);
86
+ return [
87
+ ...(text === ""
88
+ ? []
89
+ : [{ type: "message", role: "assistant", content: text }]),
90
+ ...toolCallsOf(item.content).map((part) => ({
91
+ type: "function_call",
92
+ call_id: part.id,
93
+ name: part.name,
94
+ arguments: JSON.stringify(part.args),
95
+ })),
96
+ ];
97
+ }
98
+ return [{ type: "message", role: "user", content: textOf(item) }];
99
+ });
100
+ return {
101
+ ...(instructions.length === 0 ? {} : { instructions: instructions.join("\n\n") }),
102
+ input,
103
+ ...(call.tools.length === 0
104
+ ? {}
105
+ : {
106
+ tools: call.tools.map((tool) => ({
107
+ type: "function",
108
+ name: tool.name,
109
+ ...(tool.description === undefined ? {} : { description: tool.description }),
110
+ parameters: tool.inputSchema,
111
+ })),
112
+ }),
113
+ ...responsesControls(call),
114
+ };
115
+ }
116
+ /** Translate an OpenAI Responses response into a Harness candidate. */
117
+ export function fromResponses(value) {
118
+ const response = record(value, "response");
119
+ if (response.error !== undefined && response.error !== null)
120
+ throw invalidResponse("response.error is present", "response.error");
121
+ const output = [];
122
+ for (const [index, raw] of array(response.output, "response.output").entries()) {
123
+ const item = record(raw, `response.output[${index}]`);
124
+ if (item.type === "function_call") {
125
+ output.push({
126
+ type: "tool-call",
127
+ id: string(item.call_id, `response.output[${index}].call_id`),
128
+ name: string(item.name, `response.output[${index}].name`),
129
+ ...argumentsOf(item.arguments, `response.output[${index}].arguments`),
130
+ });
131
+ continue;
132
+ }
133
+ if (item.type === "message") {
134
+ for (const [partIndex, rawPart] of optionalArray(item.content, `response.output[${index}].content`).entries()) {
135
+ const part = record(rawPart, `response.output[${index}].content[${partIndex}]`);
136
+ if (part.type === "output_text" && typeof part.text === "string")
137
+ output.push({ type: "text", text: part.text });
138
+ }
139
+ continue;
140
+ }
141
+ if (item.type === "reasoning") {
142
+ const summary = optionalArray(item.summary, `response.output[${index}].summary`)
143
+ .map((rawPart, partIndex) => record(rawPart, `response.output[${index}].summary[${partIndex}]`))
144
+ .filter((part) => part.type === "summary_text" && typeof part.text === "string")
145
+ .map((part) => part.text)
146
+ .join("\n");
147
+ if (summary !== "")
148
+ output.push({ type: "reasoning", text: summary });
149
+ }
150
+ }
151
+ return candidate({
152
+ output,
153
+ finishReason: responsesFinishReason(response, output),
154
+ usage: responsesUsage(response.usage),
155
+ evidence: evidence(response),
156
+ });
157
+ }
158
+ /** Return a Harness adapter backed by an application-owned Responses send function. */
159
+ export function responsesAdapter(send) {
160
+ return async (call, context) => fromResponses(await send(toResponses(call), call, context));
161
+ }
162
+ /** Translate a Harness call to the Anthropic Messages request shape. */
163
+ export function toMessages(call, defaultMaxOutputTokens) {
164
+ checkedMaxOutputTokens(defaultMaxOutputTokens);
165
+ const instructions = call.prompt.filter((item) => item.kind === "instructions").map(textOf);
166
+ const messages = call.prompt.flatMap((item) => {
167
+ if (item.kind === "instructions")
168
+ return [];
169
+ if (item.kind === "tool-result")
170
+ return [
171
+ {
172
+ role: "user",
173
+ content: [
174
+ {
175
+ type: "tool_result",
176
+ tool_use_id: item.toolCallId,
177
+ content: textOf(item),
178
+ ...(item.status === "completed" ? {} : { is_error: true }),
179
+ },
180
+ ],
181
+ },
182
+ ];
183
+ if (item.kind === "message" && item.role === "assistant")
184
+ return [
185
+ {
186
+ role: "assistant",
187
+ content: item.content.map((part) => part.type === "text"
188
+ ? { type: "text", text: part.text }
189
+ : { type: "tool_use", id: part.id, name: part.name, input: part.args }),
190
+ },
191
+ ];
192
+ return [{ role: "user", content: textOf(item) }];
193
+ });
194
+ return {
195
+ ...(instructions.length === 0 ? {} : { system: instructions.join("\n\n") }),
196
+ messages,
197
+ ...(call.tools.length === 0
198
+ ? {}
199
+ : {
200
+ tools: call.tools.map((tool) => ({
201
+ name: tool.name,
202
+ ...(tool.description === undefined ? {} : { description: tool.description }),
203
+ input_schema: tool.inputSchema,
204
+ })),
205
+ }),
206
+ ...(call.model?.controls?.temperature === undefined
207
+ ? {}
208
+ : { temperature: call.model.controls.temperature }),
209
+ max_tokens: call.model?.controls?.maxOutputTokens ?? defaultMaxOutputTokens,
210
+ };
211
+ }
212
+ /** Translate an Anthropic Messages response into a Harness candidate. */
213
+ export function fromMessages(value) {
214
+ const response = record(value, "response");
215
+ const output = [];
216
+ for (const [index, raw] of array(response.content, "response.content").entries()) {
217
+ const part = record(raw, `response.content[${index}]`);
218
+ if (part.type === "text" && typeof part.text === "string") {
219
+ output.push({ type: "text", text: part.text });
220
+ continue;
221
+ }
222
+ if (part.type === "thinking" && typeof part.thinking === "string") {
223
+ output.push({ type: "reasoning", text: part.thinking });
224
+ continue;
225
+ }
226
+ if (part.type === "tool_use") {
227
+ output.push({
228
+ type: "tool-call",
229
+ id: string(part.id, `response.content[${index}].id`),
230
+ name: string(part.name, `response.content[${index}].name`),
231
+ args: jsonObject(part.input, `response.content[${index}].input`),
232
+ });
233
+ }
234
+ }
235
+ return candidate({
236
+ output,
237
+ finishReason: messagesFinishReason(response.stop_reason, output),
238
+ usage: messagesUsage(response.usage),
239
+ evidence: evidence(response),
240
+ });
241
+ }
242
+ /** Return a Harness adapter backed by an application-owned Anthropic Messages send function. */
243
+ export function anthropicAdapter(options) {
244
+ checkedMaxOutputTokens(options.defaultMaxOutputTokens);
245
+ return async (call, context) => fromMessages(await options.send(toMessages(call, options.defaultMaxOutputTokens), call, context));
246
+ }
247
+ function textOf(item) {
248
+ return item.content
249
+ .filter((part) => part.type === "text")
250
+ .map((part) => part.text)
251
+ .join("");
252
+ }
253
+ function toolCallsOf(parts) {
254
+ return parts.filter((part) => part.type === "tool-call");
255
+ }
256
+ function chatControls(call) {
257
+ return {
258
+ ...(call.model?.controls?.temperature === undefined
259
+ ? {}
260
+ : { temperature: call.model.controls.temperature }),
261
+ ...(call.model?.controls?.maxOutputTokens === undefined
262
+ ? {}
263
+ : { max_completion_tokens: call.model.controls.maxOutputTokens }),
264
+ };
265
+ }
266
+ function responsesControls(call) {
267
+ return {
268
+ ...(call.model?.controls?.temperature === undefined
269
+ ? {}
270
+ : { temperature: call.model.controls.temperature }),
271
+ ...(call.model?.controls?.maxOutputTokens === undefined
272
+ ? {}
273
+ : { max_output_tokens: call.model.controls.maxOutputTokens }),
274
+ };
275
+ }
276
+ function argumentsOf(value, path) {
277
+ const raw = string(value, path);
278
+ try {
279
+ return { args: jsonObject(JSON.parse(raw), path), raw };
280
+ }
281
+ catch (error) {
282
+ throw invalidResponse(`${path} must be a JSON object`, path, error);
283
+ }
284
+ }
285
+ function jsonObject(value, path) {
286
+ try {
287
+ return copyJsonObject(value, path);
288
+ }
289
+ catch (error) {
290
+ throw invalidResponse(`${path} must be a JSON object`, path, error);
291
+ }
292
+ }
293
+ function candidate(value) {
294
+ return {
295
+ output: value.output,
296
+ ...(value.finishReason === undefined ? {} : { finishReason: value.finishReason }),
297
+ ...(value.usage === undefined ? {} : { usage: value.usage }),
298
+ ...(value.evidence === undefined ? {} : { evidence: value.evidence }),
299
+ };
300
+ }
301
+ function chatFinishReason(value, output) {
302
+ if (value === "tool_calls" || value === "function_call")
303
+ return "tool-calls";
304
+ if (value === "length")
305
+ return "length";
306
+ if (value === "content_filter")
307
+ return "content-filter";
308
+ if (value === "stop" || value === null || value === undefined)
309
+ return hasToolCall(output) ? "tool-calls" : "stop";
310
+ return "other";
311
+ }
312
+ function responsesFinishReason(response, output) {
313
+ if (response.status === "incomplete") {
314
+ const details = response.incomplete_details === undefined
315
+ ? undefined
316
+ : record(response.incomplete_details, "response.incomplete_details");
317
+ return details?.reason === "max_output_tokens" ? "length" : "other";
318
+ }
319
+ if (response.status === "failed" || response.status === "cancelled")
320
+ throw invalidResponse(`response.status is ${String(response.status)}`, "response.status");
321
+ return hasToolCall(output) ? "tool-calls" : "stop";
322
+ }
323
+ function messagesFinishReason(value, output) {
324
+ if (value === "tool_use")
325
+ return "tool-calls";
326
+ if (value === "max_tokens")
327
+ return "length";
328
+ if (value === "end_turn" || value === "stop_sequence" || value === undefined || value === null)
329
+ return hasToolCall(output) ? "tool-calls" : "stop";
330
+ return "other";
331
+ }
332
+ function chatUsage(value) {
333
+ const usage = optionalRecord(value, "response.usage");
334
+ if (usage === undefined)
335
+ return undefined;
336
+ return usageOf({
337
+ inputTokens: usage.prompt_tokens,
338
+ outputTokens: usage.completion_tokens,
339
+ totalTokens: usage.total_tokens,
340
+ cachedTokens: optionalRecord(usage.prompt_tokens_details, "response.usage.prompt_tokens_details")?.cached_tokens,
341
+ reasoningTokens: optionalRecord(usage.completion_tokens_details, "response.usage.completion_tokens_details")?.reasoning_tokens,
342
+ }, "response.usage");
343
+ }
344
+ function responsesUsage(value) {
345
+ const usage = optionalRecord(value, "response.usage");
346
+ if (usage === undefined)
347
+ return undefined;
348
+ return usageOf({
349
+ inputTokens: usage.input_tokens,
350
+ outputTokens: usage.output_tokens,
351
+ totalTokens: usage.total_tokens,
352
+ cachedTokens: optionalRecord(usage.input_tokens_details, "response.usage.input_tokens_details")?.cached_tokens,
353
+ reasoningTokens: optionalRecord(usage.output_tokens_details, "response.usage.output_tokens_details")?.reasoning_tokens,
354
+ }, "response.usage");
355
+ }
356
+ function messagesUsage(value) {
357
+ const usage = optionalRecord(value, "response.usage");
358
+ if (usage === undefined)
359
+ return undefined;
360
+ return usageOf({
361
+ inputTokens: usage.input_tokens,
362
+ outputTokens: usage.output_tokens,
363
+ cachedTokens: usage.cache_read_input_tokens,
364
+ }, "response.usage");
365
+ }
366
+ function usageOf(value, path) {
367
+ const fields = Object.entries(value).flatMap(([key, raw]) => {
368
+ if (raw === undefined)
369
+ return [];
370
+ if (!isNonNegativeInteger(raw))
371
+ throw invalidResponse(`${path}.${key} must be a non-negative integer`, `${path}.${key}`);
372
+ return [[key, raw]];
373
+ });
374
+ return Object.fromEntries(fields);
375
+ }
376
+ function evidence(response) {
377
+ const requestId = optionalString(response.id, "response.id");
378
+ const resolvedModel = optionalString(response.model, "response.model");
379
+ return requestId === undefined && resolvedModel === undefined
380
+ ? undefined
381
+ : {
382
+ ...(requestId === undefined ? {} : { requestId }),
383
+ ...(resolvedModel === undefined ? {} : { resolvedModel }),
384
+ };
385
+ }
386
+ function record(value, path) {
387
+ if (!value || typeof value !== "object" || Array.isArray(value))
388
+ throw invalidResponse(`${path} must be an object`, path);
389
+ return value;
390
+ }
391
+ function optionalRecord(value, path) {
392
+ return value === undefined || value === null ? undefined : record(value, path);
393
+ }
394
+ function array(value, path) {
395
+ if (!Array.isArray(value))
396
+ throw invalidResponse(`${path} must be an array`, path);
397
+ return value;
398
+ }
399
+ function optionalArray(value, path) {
400
+ if (value === undefined || value === null)
401
+ return [];
402
+ if (!Array.isArray(value))
403
+ throw invalidResponse(`${path} must be an array`, path);
404
+ return value;
405
+ }
406
+ function string(value, path) {
407
+ if (typeof value !== "string")
408
+ throw invalidResponse(`${path} must be a string`, path);
409
+ return value;
410
+ }
411
+ function optionalString(value, path) {
412
+ if (value === undefined || value === null)
413
+ return undefined;
414
+ return string(value, path);
415
+ }
416
+ function checkedMaxOutputTokens(value) {
417
+ if (!isNonNegativeInteger(value))
418
+ throw new HarnessError("model.adapter-invalid-options", "Anthropic defaultMaxOutputTokens must be a non-negative integer", { details: { path: "defaultMaxOutputTokens" } });
419
+ }
420
+ function isNonNegativeInteger(value) {
421
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
422
+ }
423
+ function hasToolCall(output) {
424
+ return output.some((part) => part.type === "tool-call");
425
+ }
426
+ function invalidResponse(message, path, cause) {
427
+ return new HarnessError("model.adapter-invalid-response", message, {
428
+ ...(cause === undefined ? {} : { cause }),
429
+ details: { path },
430
+ });
431
+ }
@@ -1,5 +1,5 @@
1
- import { HarnessError } from "./errors.js";
2
- import type { ModelCandidate, ModelDirective, ModelOutputBlock } from "./types/model.js";
1
+ import { HarnessError } from "../errors.js";
2
+ import type { ModelCandidate, ModelDirective, ModelOutputBlock } from "../types/model.js";
3
3
  export declare function normalizeDirective(value: unknown): ModelDirective | HarnessError;
4
4
  export declare function sameDirective(left: ModelDirective, right: ModelDirective): boolean;
5
5
  export declare function textFromOutput(output: readonly ModelOutputBlock[]): string;
@@ -1,6 +1,6 @@
1
- import { HarnessError, isHarnessError } from "./errors.js";
2
- import { digest } from "./utils/digest.js";
3
- import { copyJsonObject } from "./utils/immutable.js";
1
+ import { HarnessError, isHarnessError } from "../errors.js";
2
+ import { digest } from "../utils/digest.js";
3
+ import { copyJsonObject } from "../utils/immutable.js";
4
4
  const FINISH_REASONS = new Set([
5
5
  "stop",
6
6
  "length",
@@ -1,5 +1,5 @@
1
1
  import { HarnessError } from "../errors.js";
2
- import { normalizeCandidate } from "../model-normalize.js";
2
+ import { normalizeCandidate } from "../model/normalize.js";
3
3
  import { assertJson, copyJson, copyJsonObject } from "../utils/immutable.js";
4
4
  export function normalizeSessionSeed(seed) {
5
5
  try {
@@ -1,6 +1,6 @@
1
1
  import { bindTool } from "../build/bind-tool.js";
2
2
  import { HarnessError, isHarnessError } from "../errors.js";
3
- import { normalizeDirective, sameDirective } from "../model-normalize.js";
3
+ import { normalizeDirective, sameDirective } from "../model/normalize.js";
4
4
  import { digest } from "../utils/digest.js";
5
5
  import { copyJson } from "../utils/immutable.js";
6
6
  import { checkedReason, slotOwner, SlotDraft } from "./slot-assembly.js";
package/dist/step/run.js CHANGED
@@ -1,4 +1,4 @@
1
- import { normalizeCandidate } from "../model-normalize.js";
1
+ import { normalizeCandidate } from "../model/normalize.js";
2
2
  import { HarnessError, isHarnessError } from "../errors.js";
3
3
  import { copyJson } from "../utils/immutable.js";
4
4
  import { runMiddleware } from "./compose.js";
package/dist/step/seal.js CHANGED
@@ -1,4 +1,4 @@
1
- import { textFromOutput } from "../model-normalize.js";
1
+ import { textFromOutput } from "../model/normalize.js";
2
2
  import { createId } from "../utils/ids.js";
3
3
  import { HarnessError, isHarnessError } from "../errors.js";
4
4
  import { assertJson, copyJson } from "../utils/immutable.js";
@@ -1,6 +1,6 @@
1
1
  import { HarnessError, isHarnessError } from "../errors.js";
2
2
  import { callsFromCanonical, candidateFromCanonical, canonicalizeOutput, identityKey, } from "./canonicalize.js";
3
- import { normalizeCandidate } from "../model-normalize.js";
3
+ import { normalizeCandidate } from "../model/normalize.js";
4
4
  import { ContextDraft } from "./context-draft.js";
5
5
  import { ModelConfigurationDraft } from "./model-configuration.js";
6
6
  const branded = new WeakSet();
@@ -1,11 +1,12 @@
1
- import type { BoundMiddleware } from "./middleware.js";
1
+ import type { BoundMiddleware, MiddlewareContributions } from "./middleware.js";
2
2
  import type { BuildDiagnostic } from "./shared.js";
3
+ export interface MiddlewareManifest extends MiddlewareContributions {
4
+ readonly id: string;
5
+ }
3
6
  export interface AgentManifest {
4
7
  readonly id: string;
5
8
  readonly name: string;
6
- readonly middleware: readonly {
7
- readonly id: string;
8
- }[];
9
+ readonly middleware: readonly MiddlewareManifest[];
9
10
  }
10
11
  export type BuildResult<Agent> = {
11
12
  readonly ok: true;
@@ -78,9 +78,18 @@ export interface CapabilityDeclaration<State = never> {
78
78
  readonly state?: CapabilityState<State>;
79
79
  readonly middleware?: StepMiddleware<State>;
80
80
  }
81
+ export interface MiddlewareContributions {
82
+ readonly instructions?: readonly string[];
83
+ readonly tools?: readonly {
84
+ readonly name: string;
85
+ readonly description?: string;
86
+ }[];
87
+ readonly model?: Pick<ModelDirective, "id" | "controls">;
88
+ }
81
89
  export interface BoundMiddleware {
82
90
  readonly id: string;
83
91
  readonly handle: StepMiddleware;
84
92
  readonly state?: CapabilityState<unknown>;
93
+ readonly contributions?: MiddlewareContributions;
85
94
  }
86
95
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/harness",
3
- "version": "0.8.0-beta.1",
3
+ "version": "0.9.0-beta.1",
4
4
  "description": "A provider-neutral, in-memory agent loop for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -27,6 +27,10 @@
27
27
  ".": {
28
28
  "types": "./dist/index.d.ts",
29
29
  "import": "./dist/index.js"
30
+ },
31
+ "./model/adapters": {
32
+ "types": "./dist/model/adapters.d.ts",
33
+ "import": "./dist/model/adapters.js"
30
34
  }
31
35
  },
32
36
  "files": [