@inflexa-ai/harness 0.30.0 → 0.30.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/dist/index.d.ts CHANGED
@@ -67,9 +67,9 @@ export { createConfiguredAiSdkProvider, DEFAULT_MAX_OUTPUT_TOKENS } from "./prov
67
67
  export type { AiSdkProviderConfig, ConfiguredAiSdkProviderDeps } from "./providers/ai-sdk.js";
68
68
  export { createEmbeddingProvider } from "./providers/embedding.js";
69
69
  export type { EmbeddingProviderDeps } from "./providers/embedding.js";
70
- export type { AgentChat, ChatProvider, EmbeddingProvider, ChatRequest, ChatResponse, ChatStreamEvent, ChatUsage, ModelMessage, PromptCachePolicy, ReasoningEffort, ReasoningPolicy, } from "./providers/types.js";
70
+ export type { AgentChat, ChatProvider, EmbeddingProvider, ChatRequest, ChatResponse, ChatStreamEvent, ChatUsage, ModelMessage, PromptCachePolicy, ReasoningPolicy, } from "./providers/types.js";
71
71
  export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "./providers/prompt-cache.js";
72
- export { DEFAULT_REASONING, reasoningProviderOptions } from "./providers/reasoning.js";
72
+ export { DEFAULT_REASONING } from "./providers/reasoning.js";
73
73
  export { toProviderError, isProviderError } from "./providers/errors.js";
74
74
  export type { ProviderError } from "./providers/errors.js";
75
75
  export { createStreamingChat } from "./providers/streaming-chat.js";
package/dist/index.js CHANGED
@@ -104,13 +104,15 @@ export { createEmbeddingProvider } from "./providers/embedding.js";
104
104
  // `"off"`; the `cortex.harness.agent.cache_*_tokens` metrics show which case a
105
105
  // deployment is actually in.
106
106
  export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "./providers/prompt-cache.js";
107
- // Reasoning depth. `ReasoningPolicy` is the vendor-neutral directive for how
108
- // deep a model reasons; a composition root sets it per run through
109
- // `RunAgentOptions.reasoning`. It defaults to `DEFAULT_REASONING` — adaptive
110
- // thinking at the `xhigh` effort — because an agent loop drives tools over many
111
- // iterations, and a shallow turn there wastes more calls than the deeper turn
112
- // costs in tokens. A host on a model with no reasoning support passes `"off"`.
113
- export { DEFAULT_REASONING, reasoningProviderOptions } from "./providers/reasoning.js";
107
+ // Reasoning depth. `ReasoningPolicy` is the vendor-neutral name for how deep a
108
+ // model reasons; a composition root sets it per run through
109
+ // `RunAgentOptions.reasoning`. It defaults to `DEFAULT_REASONING` — `xhigh` —
110
+ // because an agent loop drives tools over many iterations, and a shallow turn
111
+ // there wastes more calls than the deeper turn costs in tokens. The provider
112
+ // package resolves the name for the model that it is bound to, thus a model
113
+ // that accepts no `xhigh` gets the nearest name that it accepts. A host on a
114
+ // model with no reasoning support passes `"provider-default"`.
115
+ export { DEFAULT_REASONING } from "./providers/reasoning.js";
114
116
  // Provider error channel. `ProviderError` is the value `chat`/`embed` fail
115
117
  // with; `toProviderError` is its sole constructor. Exposed so an embedder
116
118
  // realizing its own `ChatProvider`/`EmbeddingProvider` fails with the exact
@@ -76,14 +76,15 @@ export interface RunAgentOptions {
76
76
  readonly promptCache?: PromptCachePolicy;
77
77
  /**
78
78
  * Reasoning policy for every LLM call this run makes. Defaults to
79
- * `DEFAULT_REASONING` (adaptive thinking at `xhigh`) — an agent loop drives
80
- * tools over many iterations, and a shallow turn there costs more in wasted
81
- * calls than the deeper turn costs in tokens.
79
+ * `DEFAULT_REASONING` (`xhigh`) — an agent loop drives tools over many
80
+ * iterations, and a shallow turn there costs more in wasted calls than the
81
+ * deeper turn costs in tokens. The provider package resolves the name for
82
+ * the model that it is bound to.
82
83
  *
83
84
  * The policy lives here, on the run, rather than on the provider, for the
84
85
  * same reason that the cache policy does: a one-shot LLM call elsewhere has
85
86
  * its own depth needs and must not inherit the depth of a loop. A host on a
86
- * model with no reasoning support passes `"off"`.
87
+ * model with no reasoning support passes `"provider-default"`.
87
88
  */
88
89
  readonly reasoning?: ReasoningPolicy;
89
90
  /**
@@ -8,7 +8,7 @@ import { markInterruptedMessage, syntheticUserMessage } from "../memory/ai-sdk-m
8
8
  import { stripUnansweredToolCalls } from "../memory/tool-call-integrity.js";
9
9
  import { classifyProviderError } from "../providers/errors.js";
10
10
  import { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions } from "../providers/prompt-cache.js";
11
- import { DEFAULT_REASONING, mergeProviderOptions, reasoningProviderOptions } from "../providers/reasoning.js";
11
+ import { DEFAULT_REASONING } from "../providers/reasoning.js";
12
12
  import { resultStep } from "./run-step.js";
13
13
  import { AskRejectedError, UnavailableAsk } from "../tools/approval/contract.js";
14
14
  import { isToolError, readToolResultImages } from "../tools/define-tool.js";
@@ -85,7 +85,10 @@ export async function runAgent(agent, initial, session, opts) {
85
85
  // Resolved once, not per iteration: an identical options object across every
86
86
  // call is itself part of the cache contract — the request prefix has to be
87
87
  // byte-identical to be read back.
88
- const providerOptions = mergeProviderOptions(promptCacheProviderOptions(opts.promptCache ?? DEFAULT_PROMPT_CACHE), reasoningProviderOptions(opts.reasoning ?? DEFAULT_REASONING));
88
+ const providerOptions = promptCacheProviderOptions(opts.promptCache ?? DEFAULT_PROMPT_CACHE);
89
+ // The depth is a neutral name, and the provider package resolves it for the
90
+ // model. A vendor key here would turn that resolution off.
91
+ const reasoning = opts.reasoning ?? DEFAULT_REASONING;
89
92
  const usage = {};
90
93
  // Exactly one record per completed run — never one per iteration. That bound is
91
94
  // what keeps the default level affordable for an agent that runs long; the
@@ -233,6 +236,7 @@ export async function runAgent(agent, initial, session, opts) {
233
236
  tools: toolDefs,
234
237
  ...(opts.toolChoice !== undefined ? { toolChoice: opts.toolChoice } : {}),
235
238
  providerOptions,
239
+ reasoning,
236
240
  };
237
241
  const llmStepName = formatStepName.llm(i);
238
242
  const reply = await resultStep(runStep)(llmStepName, () => provider.chat(request, session, signal));
@@ -343,7 +347,7 @@ export async function runAgent(agent, initial, session, opts) {
343
347
  // because it is the one call whose write is pure waste, and the
344
348
  // cache_write_tokens counter is what makes that waste visible.
345
349
  const wrapUpStepName = formatStepName.llm(agent.maxIterations);
346
- const wrapUp = await resultStep(runStep)(wrapUpStepName, () => provider.chat({ system: agent.systemPrompt, messages, tools: {}, toolChoice: "none", providerOptions }, session, signal));
350
+ const wrapUp = await resultStep(runStep)(wrapUpStepName, () => provider.chat({ system: agent.systemPrompt, messages, tools: {}, toolChoice: "none", providerOptions, reasoning }, session, signal));
347
351
  accountForCall(wrapUp, wrapUpStepName);
348
352
  if (wrapUp.finishReason === "aborted") {
349
353
  // An abort during the tool-less wrap-up is still the user cutting the turn — the
@@ -421,6 +421,7 @@ export function createAiSdkProvider(deps) {
421
421
  // bound, because the headers of a non-streaming response arrive
422
422
  // when the model completes.
423
423
  providerOptions: req.providerOptions,
424
+ reasoning: req.reasoning,
424
425
  });
425
426
  });
426
427
  return ok(responseFromGenerate(result, { requestedModelId, servedModelId: capture.servedModelId() }));
@@ -467,6 +468,7 @@ export function createAiSdkProvider(deps) {
467
468
  // thus it feeds neither bound.
468
469
  ...(requestTimeoutMs !== undefined ? { timeout: { firstChunkMs: requestTimeoutMs, chunkMs: requestTimeoutMs } } : {}),
469
470
  providerOptions: req.providerOptions,
471
+ reasoning: req.reasoning,
470
472
  });
471
473
  const iterator = result.fullStream[Symbol.asyncIterator]();
472
474
  const first = await pullNextDelta(iterator);
@@ -622,39 +624,6 @@ function withStoreDirective(model, store) {
622
624
  },
623
625
  });
624
626
  }
625
- /**
626
- * Mirror the neutral reasoning effort into the namespace of an
627
- * OpenAI-compatible connection.
628
- *
629
- * The compatible provider reads `providerOptions[<the name of the connection>]`
630
- * rather than `providerOptions.openai`, and it derives that namespace from the
631
- * first dot-separated segment of the name. The loop cannot write that key,
632
- * because only this seam knows the name. Thus the middleware copies
633
- * `reasoningEffort` across at request time. A value that the caller already set
634
- * on the namespace wins, and a request with no effort passes through untouched.
635
- */
636
- function withCompatibleReasoning(model, name) {
637
- const namespace = name.split(".")[0]?.trim();
638
- if (namespace === undefined || namespace === "")
639
- return model;
640
- return wrapLanguageModel({
641
- model,
642
- middleware: {
643
- transformParams: async ({ params }) => {
644
- const effort = params.providerOptions?.["openai"]?.["reasoningEffort"];
645
- if (effort === undefined)
646
- return params;
647
- return {
648
- ...params,
649
- providerOptions: {
650
- ...params.providerOptions,
651
- [namespace]: { reasoningEffort: effort, ...params.providerOptions?.[namespace] },
652
- },
653
- };
654
- },
655
- },
656
- });
657
- }
658
627
  /**
659
628
  * Realize an `AiSdkProviderConfig` into a `ChatProvider` bound to that config's
660
629
  * connection and model. The model is closed into the returned provider here (it
@@ -735,7 +704,9 @@ export function createConfiguredAiSdkProvider(deps) {
735
704
  fetch: effectiveFetch,
736
705
  });
737
706
  return createAiSdkProvider({
738
- model: withCompatibleReasoning(provider.chatModel(config.model), config.name),
707
+ // The compatible package reads the neutral `reasoning` of the call, thus
708
+ // the seam needs no middleware to carry the depth into its namespace.
709
+ model: provider.chatModel(config.model),
739
710
  resolveBilling: deps.resolveBilling,
740
711
  capabilities: config.capabilities,
741
712
  logger: deps.logger,
@@ -1,35 +1,18 @@
1
1
  /**
2
- * Reasoning policy → provider wire options.
2
+ * The reasoning depth of a run.
3
3
  *
4
- * This is the **only** place in the harness that names a vendor for reasoning
5
- * depth. Everything upstream — `ReasoningPolicy` and `RunAgentOptions` — speaks
6
- * the neutral harness concept. This module translates it one time, at the
7
- * provider seam, thus the loop never learns which vendor it talks to. It is the
8
- * sibling of `prompt-cache.ts`, and it obeys the same rules.
4
+ * The harness states the depth one time, on `ChatRequest.reasoning`, with the
5
+ * neutral names of the AI SDK. It names no vendor key. Each provider package
6
+ * holds the table of what its own models accept, and it maps the neutral name
7
+ * onto the wire. The Anthropic package also selects adaptive thinking there.
9
8
  *
10
- * ## Why the emitted options are safe on every provider
9
+ * ## Why the harness names no vendor key
11
10
  *
12
- * `providerOptions` of the AI SDK is a namespaced bag. Each provider reads only
13
- * `providerOptions[<its own name>]`, and it ignores each other key. Thus the
14
- * `anthropic` namespace is inert on an OpenAI model, and the `openai` namespace
15
- * is inert on an Anthropic model. Neither one is an error.
16
- *
17
- * ## What each namespace does
18
- *
19
- * The Anthropic namespace carries two keys, because the vendor splits the
20
- * concept in two:
21
- *
22
- * - `thinking: { type: "adaptive" }` says that the model reasons, and it lets
23
- * the model choose the depth for each turn. A fixed `budgetTokens` is removed
24
- * on the current models, and it returns a 400 there.
25
- * - `effort` says how deep the model reasons and how much it spends in total.
26
- * The vendor default is `high`.
27
- *
28
- * The OpenAI namespace carries one key, `reasoningEffort`, which takes the same
29
- * five values. An OpenAI-compatible endpoint reads the same key, but from a
30
- * namespace that carries the *name of the connection* rather than `openai`. The
31
- * provider seam mirrors the value into that namespace, because only the seam
32
- * knows the name (`providers/ai-sdk.ts`).
11
+ * The harness wrote `providerOptions.anthropic.effort` before. A value on that
12
+ * key turns the per-model table of the Anthropic package off, thus the raw name
13
+ * reached the wire. A model that accepts `high` but not `xhigh` answered 400.
14
+ * The neutral field has no such hazard, because the package resolves the name
15
+ * for the model that it is bound to.
33
16
  *
34
17
  * ## Where the policy belongs
35
18
  *
@@ -37,29 +20,16 @@
37
20
  * cache policy does. A one-shot LLM call elsewhere in the harness has its own
38
21
  * depth needs, and it must not inherit the depth of an agent loop.
39
22
  */
40
- import type { ProviderOptions, ReasoningPolicy } from "./types.js";
23
+ import type { ReasoningPolicy } from "./types.js";
41
24
  /**
42
- * The default policy: adaptive thinking at the `xhigh` effort.
25
+ * The default policy: the deepest name of the neutral ladder.
43
26
  *
44
- * `xhigh` sits between `high` and `max`. It is the vendor recommendation for
45
- * coding and agentic work, and each agent of the harness drives tools over many
46
- * iterations. `max` costs more than the remaining quality is worth for a routine
47
- * turn, thus it is not the default. A host that wants a cheaper loop passes a
48
- * lower effort, and a host on a model with no reasoning support passes `"off"`.
49
- */
50
- export declare const DEFAULT_REASONING: ReasoningPolicy;
51
- /**
52
- * Translate a neutral reasoning policy into provider wire options.
27
+ * Each agent of the harness drives tools over many iterations. A shallow turn
28
+ * there wastes more calls than a deeper turn costs in tokens. The Anthropic
29
+ * package sends `xhigh` to a model that accepts it, and `max` to a model that
30
+ * does not. The OpenAI-compatible package sends the name as it is.
53
31
  *
54
- * Returns `undefined` for `"off"`, thus the caller can leave `providerOptions`
55
- * unset rather than send an empty bag.
32
+ * A host that wants a cheaper loop passes a lower name. A host on a model with
33
+ * no reasoning support passes `"provider-default"`.
56
34
  */
57
- export declare function reasoningProviderOptions(policy: ReasoningPolicy): ProviderOptions | undefined;
58
- /**
59
- * Merge two option bags one namespace at a time.
60
- *
61
- * A plain spread of the two bags drops a whole namespace when both sides carry
62
- * it, and the cache policy and the reasoning policy both write `anthropic`. The
63
- * right-hand bag wins for a key that both sides set.
64
- */
65
- export declare function mergeProviderOptions(left: ProviderOptions | undefined, right: ProviderOptions | undefined): ProviderOptions | undefined;
35
+ export declare const DEFAULT_REASONING: ReasoningPolicy;
@@ -1,35 +1,18 @@
1
1
  /**
2
- * Reasoning policy → provider wire options.
2
+ * The reasoning depth of a run.
3
3
  *
4
- * This is the **only** place in the harness that names a vendor for reasoning
5
- * depth. Everything upstream — `ReasoningPolicy` and `RunAgentOptions` — speaks
6
- * the neutral harness concept. This module translates it one time, at the
7
- * provider seam, thus the loop never learns which vendor it talks to. It is the
8
- * sibling of `prompt-cache.ts`, and it obeys the same rules.
4
+ * The harness states the depth one time, on `ChatRequest.reasoning`, with the
5
+ * neutral names of the AI SDK. It names no vendor key. Each provider package
6
+ * holds the table of what its own models accept, and it maps the neutral name
7
+ * onto the wire. The Anthropic package also selects adaptive thinking there.
9
8
  *
10
- * ## Why the emitted options are safe on every provider
9
+ * ## Why the harness names no vendor key
11
10
  *
12
- * `providerOptions` of the AI SDK is a namespaced bag. Each provider reads only
13
- * `providerOptions[<its own name>]`, and it ignores each other key. Thus the
14
- * `anthropic` namespace is inert on an OpenAI model, and the `openai` namespace
15
- * is inert on an Anthropic model. Neither one is an error.
16
- *
17
- * ## What each namespace does
18
- *
19
- * The Anthropic namespace carries two keys, because the vendor splits the
20
- * concept in two:
21
- *
22
- * - `thinking: { type: "adaptive" }` says that the model reasons, and it lets
23
- * the model choose the depth for each turn. A fixed `budgetTokens` is removed
24
- * on the current models, and it returns a 400 there.
25
- * - `effort` says how deep the model reasons and how much it spends in total.
26
- * The vendor default is `high`.
27
- *
28
- * The OpenAI namespace carries one key, `reasoningEffort`, which takes the same
29
- * five values. An OpenAI-compatible endpoint reads the same key, but from a
30
- * namespace that carries the *name of the connection* rather than `openai`. The
31
- * provider seam mirrors the value into that namespace, because only the seam
32
- * knows the name (`providers/ai-sdk.ts`).
11
+ * The harness wrote `providerOptions.anthropic.effort` before. A value on that
12
+ * key turns the per-model table of the Anthropic package off, thus the raw name
13
+ * reached the wire. A model that accepts `high` but not `xhigh` answered 400.
14
+ * The neutral field has no such hazard, because the package resolves the name
15
+ * for the model that it is bound to.
33
16
  *
34
17
  * ## Where the policy belongs
35
18
  *
@@ -38,44 +21,14 @@
38
21
  * depth needs, and it must not inherit the depth of an agent loop.
39
22
  */
40
23
  /**
41
- * The default policy: adaptive thinking at the `xhigh` effort.
24
+ * The default policy: the deepest name of the neutral ladder.
42
25
  *
43
- * `xhigh` sits between `high` and `max`. It is the vendor recommendation for
44
- * coding and agentic work, and each agent of the harness drives tools over many
45
- * iterations. `max` costs more than the remaining quality is worth for a routine
46
- * turn, thus it is not the default. A host that wants a cheaper loop passes a
47
- * lower effort, and a host on a model with no reasoning support passes `"off"`.
48
- */
49
- export const DEFAULT_REASONING = { effort: "xhigh" };
50
- /**
51
- * Translate a neutral reasoning policy into provider wire options.
52
- *
53
- * Returns `undefined` for `"off"`, thus the caller can leave `providerOptions`
54
- * unset rather than send an empty bag.
55
- */
56
- export function reasoningProviderOptions(policy) {
57
- if (policy === "off")
58
- return undefined;
59
- return {
60
- anthropic: { effort: policy.effort, thinking: { type: "adaptive" } },
61
- openai: { reasoningEffort: policy.effort },
62
- };
63
- }
64
- /**
65
- * Merge two option bags one namespace at a time.
26
+ * Each agent of the harness drives tools over many iterations. A shallow turn
27
+ * there wastes more calls than a deeper turn costs in tokens. The Anthropic
28
+ * package sends `xhigh` to a model that accepts it, and `max` to a model that
29
+ * does not. The OpenAI-compatible package sends the name as it is.
66
30
  *
67
- * A plain spread of the two bags drops a whole namespace when both sides carry
68
- * it, and the cache policy and the reasoning policy both write `anthropic`. The
69
- * right-hand bag wins for a key that both sides set.
31
+ * A host that wants a cheaper loop passes a lower name. A host on a model with
32
+ * no reasoning support passes `"provider-default"`.
70
33
  */
71
- export function mergeProviderOptions(left, right) {
72
- if (left === undefined)
73
- return right;
74
- if (right === undefined)
75
- return left;
76
- const merged = { ...left };
77
- for (const [namespace, options] of Object.entries(right)) {
78
- merged[namespace] = { ...merged[namespace], ...options };
79
- }
80
- return merged;
81
- }
34
+ export const DEFAULT_REASONING = "xhigh";
@@ -6,6 +6,7 @@
6
6
  * attribution is resolved lazily at the provider wire boundary.
7
7
  */
8
8
  import type { FinishReason, LanguageModel, ModelMessage, ToolSet } from "ai";
9
+ import type { LanguageModelV4CallOptions } from "@ai-sdk/provider";
9
10
  import type { ProviderOptions } from "@ai-sdk/provider-utils";
10
11
  import type { ResultAsync } from "neverthrow";
11
12
  import type { AgentSession } from "../auth/types.js";
@@ -40,6 +41,11 @@ export interface ChatRequest {
40
41
  readonly toolName: string;
41
42
  };
42
43
  readonly providerOptions?: ProviderOptions;
44
+ /**
45
+ * How deep the model reasons on this call. Absent sends no directive, thus
46
+ * the model applies its own default. Refer to `ReasoningPolicy`.
47
+ */
48
+ readonly reasoning?: ReasoningPolicy;
43
49
  }
44
50
  /**
45
51
  * Vendor-neutral prompt-cache policy — a harness concept, not a vendor one.
@@ -57,17 +63,23 @@ export type PromptCachePolicy = {
57
63
  /**
58
64
  * How deep a model reasons, in harness-neutral names.
59
65
  *
60
- * `{ effort }` asks the provider for that reasoning depth, and `"off"` sends no
61
- * reasoning directive at all. The five values are the ladder that both vendor
62
- * families accept. A model with no reasoning support ignores the directive
63
- * rather than fails on it, thus the policy is a no-op for that model. Refer to
64
- * `./reasoning.ts`, the single place where the harness translates this into
65
- * vendor wire options.
66
+ * This is the `reasoning` call setting of the AI SDK, and the harness passes the
67
+ * value through without a change. Each provider package maps the name onto its
68
+ * own wire shape, and each one holds the table of what its models accept. The
69
+ * Anthropic package selects adaptive thinking with the matching `effort`, and it
70
+ * lowers a level that the model does not accept. The OpenAI-compatible package
71
+ * sends the name as `reasoning_effort`.
72
+ *
73
+ * `"provider-default"` sends no directive, thus the model applies its own
74
+ * default. `"none"` asks for no reasoning. The other names are the ladder from
75
+ * the shallowest depth to the deepest depth.
76
+ *
77
+ * CAUTION: never write `providerOptions.anthropic.effort` or
78
+ * `providerOptions.openai.reasoningEffort` in place of this field. A value on
79
+ * either key turns the per-model table off, and the raw name then reaches the
80
+ * wire. A model that does not accept that name answers 400.
66
81
  */
67
- export type ReasoningEffort = "low" | "medium" | "high" | "xhigh" | "max";
68
- export type ReasoningPolicy = {
69
- readonly effort: ReasoningEffort;
70
- } | "off";
82
+ export type ReasoningPolicy = NonNullable<LanguageModelV4CallOptions["reasoning"]>;
71
83
  /**
72
84
  * Token accounting for one chat call, in harness-neutral names.
73
85
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",