@open-cr-agent/runtime-direct 0.5.0 → 0.6.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/dist/effort.d.ts CHANGED
@@ -2,9 +2,9 @@ import type { AppliedSettings, Effort, Sampling } from "@open-cr-agent/core";
2
2
  import type { CallParams } from "./openai.js";
3
3
  export type EffortStyle = "openai" | "openrouter";
4
4
  export declare class EffortLedger {
5
- private readonly sampling;
6
5
  private readonly refused;
7
6
  private readonly applied;
7
+ private readonly sampling;
8
8
  constructor(sampling: Sampling);
9
9
  params(agent: string, effort: Effort | undefined, model: string, style?: EffortStyle): CallParams;
10
10
  refuse(agent: string, model: string): void;
package/dist/effort.js CHANGED
@@ -4,11 +4,11 @@
4
4
  // "none" sends no sampling settings, even after its endpoint refused the
5
5
  // effort: the agent's calls stay alike within a run.
6
6
  export class EffortLedger {
7
- sampling;
8
7
  // Models whose endpoint refused the effort parameter: later calls go
9
8
  // without it instead of paying for another refusal.
10
9
  refused = new Set();
11
10
  applied = new Map();
11
+ sampling;
12
12
  constructor(sampling) {
13
13
  this.sampling = sampling;
14
14
  }
package/dist/loop.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { ModelPrice, ReviewContext, ToolDefinition, Usage } from "@open-cr-agent/core";
2
- import { type AttemptOutcome } from "@open-cr-agent/core/internal";
1
+ import { type AttemptOutcome, type ModelPrice, type ReviewContext, type ToolDefinition, type Usage } from "@open-cr-agent/core";
3
2
  import { type CallParams, type Endpoint } from "./openai.js";
4
3
  export interface LoopInput {
5
4
  endpoint: Endpoint;
package/dist/loop.js CHANGED
@@ -1,4 +1,4 @@
1
- import { addUsage, emptyUsage, errorMessage, REVIEW_TOOLS, } from "@open-cr-agent/core/internal";
1
+ import { addUsage, emptyUsage, errorMessage, REVIEW_TOOLS, } from "@open-cr-agent/core";
2
2
  import { chat, toolSpec, withoutEffort, } from "./openai.js";
3
3
  // The agent loop: ask the model, run the tools it calls against the review
4
4
  // context, hand the results back, until it calls task_done, answers without
package/dist/openai.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { Effort, ModelPrice, ToolDefinition, Usage } from "@open-cr-agent/core";
2
- import { type QuotaError } from "@open-cr-agent/core/internal";
1
+ import { type Effort, type ModelPrice, type QuotaError, type ToolDefinition, type Usage } from "@open-cr-agent/core";
3
2
  import { z } from "zod";
4
3
  declare const toolCallSchema: z.ZodObject<{
5
4
  id: z.ZodString;
@@ -46,7 +45,7 @@ export interface ChatRequest {
46
45
  };
47
46
  }
48
47
  export type CallParams = Pick<ChatRequest, "temperature" | "seed" | "reasoning_effort" | "reasoning">;
49
- export interface ChatError {
48
+ interface ChatError {
50
49
  message: string;
51
50
  retryable: boolean;
52
51
  quota?: QuotaError;
@@ -64,7 +63,6 @@ export type ChatResponse = ({
64
63
  }) & {
65
64
  effortDropped?: true;
66
65
  };
67
- export declare const TRANSIENT_RETRY_MS = 1000;
68
66
  export declare function toolSpec(tool: ToolDefinition): ToolSpec;
69
67
  export declare function chat(endpoint: Endpoint, request: ChatRequest, price: ModelPrice, signal: AbortSignal): Promise<ChatResponse>;
70
68
  export declare function withoutEffort<T extends CallParams>(params: T): T;
package/dist/openai.js CHANGED
@@ -1,4 +1,5 @@
1
- import { errorMessage, parseQuotaError, sleep, withoutSecrets, } from "@open-cr-agent/core/internal";
1
+ import { setTimeout } from "node:timers/promises";
2
+ import { errorMessage, parseQuotaError, withoutSecrets, } from "@open-cr-agent/core";
2
3
  import { z } from "zod";
3
4
  // The subset of the OpenAI chat completions protocol the loop needs: one
4
5
  // choice, its text and tool calls, and the token counts. Anything else an
@@ -7,15 +8,15 @@ const toolCallSchema = z.object({
7
8
  id: z.string(),
8
9
  function: z.object({ name: z.string(), arguments: z.string() }),
9
10
  });
11
+ const choiceSchema = z.object({
12
+ message: z.object({
13
+ content: z.string().nullable().optional(),
14
+ tool_calls: z.array(toolCallSchema).optional(),
15
+ }),
16
+ });
10
17
  const completionSchema = z.object({
11
- choices: z
12
- .array(z.object({
13
- message: z.object({
14
- content: z.string().nullable().optional(),
15
- tool_calls: z.array(toolCallSchema).optional(),
16
- }),
17
- }))
18
- .min(1),
18
+ // At least one: the first is the answer.
19
+ choices: z.tuple([choiceSchema], choiceSchema),
19
20
  usage: z
20
21
  .object({
21
22
  prompt_tokens: z.number().optional(),
@@ -37,7 +38,7 @@ const EFFORT_PARAMETER = /\breasoning(?:_effort)?\b/i;
37
38
  const BODY_EXCERPT = 300;
38
39
  // A transient failure is sent once more after this pause; a second failure
39
40
  // goes to the failback, which decides what it means for the model.
40
- export const TRANSIENT_RETRY_MS = 1_000;
41
+ const TRANSIENT_RETRY_MS = 1_000;
41
42
  export function toolSpec(tool) {
42
43
  const { $schema: _, ...parameters } = z.toJSONSchema(tool.inputSchema);
43
44
  return {
@@ -70,7 +71,7 @@ async function sendRetrying(endpoint, request, price, signal) {
70
71
  const first = await send(endpoint, request, price, signal);
71
72
  if (first.ok || !first.error.transient)
72
73
  return first;
73
- await sleep(TRANSIENT_RETRY_MS, signal);
74
+ await setTimeout(TRANSIENT_RETRY_MS, undefined, { signal }).catch(() => undefined);
74
75
  if (signal.aborted)
75
76
  return first;
76
77
  return send(endpoint, request, price, signal);
@@ -92,7 +93,14 @@ async function send(endpoint, request, price, signal) {
92
93
  catch (error) {
93
94
  if (signal.aborted)
94
95
  return { ok: false, error: { message: "cancelled", retryable: false } };
95
- return { ok: false, error: { message: errorMessage(error), retryable: true, transient: true } };
96
+ return {
97
+ ok: false,
98
+ error: {
99
+ message: errorMessage(error),
100
+ retryable: true,
101
+ transient: true,
102
+ },
103
+ };
96
104
  }
97
105
  const body = await response.text().catch(() => "");
98
106
  if (!response.ok)
@@ -112,7 +120,7 @@ async function send(endpoint, request, price, signal) {
112
120
  if (!parsed.success) {
113
121
  return { ok: false, error: malformed("answered without a chat completion", body, secrets) };
114
122
  }
115
- const choice = parsed.data.choices[0];
123
+ const [choice] = parsed.data.choices;
116
124
  return {
117
125
  ok: true,
118
126
  content: choice.message.content ?? "",
package/dist/runtime.d.ts CHANGED
@@ -3,19 +3,22 @@ export interface DirectRuntimeOptions extends RuntimeOptions {
3
3
  fetch?: typeof fetch;
4
4
  }
5
5
  export declare class DirectRuntime implements AgentRuntime {
6
- private readonly options;
7
6
  readonly name = "direct";
8
7
  readonly sampling: AppliedSampling;
9
- private readonly health;
8
+ private readonly chains;
10
9
  private readonly tools;
11
10
  private readonly fetch;
12
11
  private readonly efforts;
12
+ private readonly options;
13
13
  constructor(options: DirectRuntimeOptions);
14
14
  appliedTo(agent: string): AppliedSettings | undefined;
15
15
  runTask(spec: AgentTaskSpec, signal: AbortSignal): AsyncIterable<AgentEvent>;
16
16
  complete(request: CompletionRequest, signal: AbortSignal): Promise<CompletionResult>;
17
+ private attemptTask;
18
+ private attemptCompletion;
17
19
  private unreachable;
18
20
  private call;
21
+ private declared;
19
22
  private target;
20
23
  }
21
24
  //# sourceMappingURL=runtime.d.ts.map
package/dist/runtime.js CHANGED
@@ -1,5 +1,4 @@
1
- import { OcraError, } from "@open-cr-agent/core";
2
- import { callChain, completeWithFailback, MAX_AGENT_STEPS, ModelHealth, parseModel, proxiedFetch, RESUME_MESSAGE, reviewTools, withFailback, } from "@open-cr-agent/core/internal";
1
+ import { ChainRunner, MAX_AGENT_STEPS, OcraError, parseModel, proxiedFetch, RESUME_MESSAGE, reviewTools, } from "@open-cr-agent/core";
3
2
  import { EffortLedger } from "./effort.js";
4
3
  import { runLoop } from "./loop.js";
5
4
  // Talks to the declared OpenAI-compatible endpoints itself: no OpenCode
@@ -7,14 +6,14 @@ import { runLoop } from "./loop.js";
7
6
  // a provider that is not declared in configuration is refused, since the
8
7
  // runtime knows no other address to send code to.
9
8
  export class DirectRuntime {
10
- options;
11
9
  name = "direct";
12
10
  // The chat completions protocol takes both settings.
13
11
  sampling;
14
- health = new ModelHealth();
12
+ chains;
15
13
  tools;
16
14
  fetch;
17
15
  efforts;
16
+ options;
18
17
  constructor(options) {
19
18
  this.options = options;
20
19
  this.tools = [...reviewTools, ...options.tools];
@@ -25,61 +24,47 @@ export class DirectRuntime {
25
24
  ...(seed === undefined ? {} : { seed }),
26
25
  };
27
26
  this.efforts = new EffortLedger(this.sampling);
27
+ this.chains = new ChainRunner(options.models, {
28
+ refuse: (chain) => this.unreachable(chain),
29
+ task: (model, spec, signal, onUsage) => this.attemptTask(model, spec, signal, onUsage),
30
+ complete: (model, request, signal) => this.attemptCompletion(model, request, signal),
31
+ });
28
32
  }
29
33
  appliedTo(agent) {
30
34
  return this.efforts.appliedTo(agent);
31
35
  }
32
- async *runTask(spec, signal) {
33
- const chain = callChain(this.options.models, spec.modelTier, spec.models);
34
- const refused = chain.length === 0 ? noModel(spec.modelTier) : this.unreachable(chain);
35
- if (refused) {
36
- yield { type: "error", taskId: spec.taskId, error: refused.message, retryable: false };
37
- return;
38
- }
39
- yield* withFailback({
40
- taskId: spec.taskId,
41
- tier: spec.modelTier,
42
- ...(spec.models?.length ? { agent: spec.reviewer } : {}),
43
- chain,
44
- health: this.health,
36
+ runTask(spec, signal) {
37
+ return this.chains.runTask(spec, signal);
38
+ }
39
+ complete(request, signal) {
40
+ return this.chains.complete(request, signal);
41
+ }
42
+ attemptTask(model, spec, signal, onUsage) {
43
+ return runLoop({
44
+ ...this.target(model),
45
+ ...this.call({ agent: spec.reviewer, effort: spec.effort, model }),
46
+ system: spec.systemPrompt,
47
+ user: spec.userPrompt,
48
+ tools: this.tools,
49
+ context: spec.context,
50
+ maxSteps: MAX_AGENT_STEPS,
51
+ resume: RESUME_MESSAGE,
52
+ timeoutMs: spec.timeoutMs,
45
53
  signal,
46
- attempt: (model, onUsage) => runLoop({
47
- ...this.target(model),
48
- ...this.call({ agent: spec.reviewer, effort: spec.effort, model }),
49
- system: spec.systemPrompt,
50
- user: spec.userPrompt,
51
- tools: this.tools,
52
- context: spec.context,
53
- maxSteps: MAX_AGENT_STEPS,
54
- resume: RESUME_MESSAGE,
55
- timeoutMs: spec.timeoutMs,
56
- signal,
57
- onUsage,
58
- }),
54
+ onUsage,
59
55
  });
60
56
  }
61
- async complete(request, signal) {
62
- const chain = callChain(this.options.models, request.tier, request.models);
63
- const refused = chain.length === 0 ? noModel(request.tier) : this.unreachable(chain);
64
- if (refused)
65
- throw refused;
66
- return completeWithFailback({
67
- tier: request.tier,
68
- ...(request.models?.length ? { agent: request.agent ?? request.tier } : {}),
69
- chain,
70
- health: this.health,
57
+ attemptCompletion(model, request, signal) {
58
+ return runLoop({
59
+ ...this.target(model),
60
+ ...this.call({ agent: request.agent ?? request.tier, effort: request.effort, model }),
61
+ system: request.system,
62
+ user: request.user,
63
+ tools: [],
64
+ context: NO_CONTEXT,
65
+ maxSteps: 1,
66
+ timeoutMs: request.timeoutMs,
71
67
  signal,
72
- attempt: (model) => runLoop({
73
- ...this.target(model),
74
- ...this.call({ agent: request.agent ?? request.tier, effort: request.effort, model }),
75
- system: request.system,
76
- user: request.user,
77
- tools: [],
78
- context: NO_CONTEXT,
79
- maxSteps: 1,
80
- timeoutMs: request.timeoutMs,
81
- signal,
82
- }),
83
68
  });
84
69
  }
85
70
  // Why a chain cannot be served, before any request: a provider the
@@ -87,16 +72,12 @@ export class DirectRuntime {
87
72
  // variable that is not set.
88
73
  unreachable(chain) {
89
74
  for (const model of chain) {
90
- const { providerID, modelID } = parseModel(model);
91
- const provider = this.options.providers?.[providerID];
92
- if (!provider) {
93
- return new OcraError("CONFIG_INVALID", `"${model}" names no provider declared in configuration; the direct runtime reaches only declared OpenAI-compatible endpoints (use the opencode runtime for ${providerID})`);
94
- }
95
- if (!provider.models[modelID]) {
96
- return new OcraError("CONFIG_INVALID", `"${model}" has no price in the declaration of provider "${providerID}"`);
97
- }
98
- if (provider.apiKeyEnv && !this.options.env[provider.apiKeyEnv]) {
99
- return new OcraError("CONFIG_CREDENTIALS_MISSING", `No API key for provider "${providerID}": set ${provider.apiKeyEnv}`);
75
+ const declared = this.declared(model);
76
+ if (declared instanceof OcraError)
77
+ return declared;
78
+ const { apiKeyEnv } = declared.provider;
79
+ if (apiKeyEnv && !this.options.env[apiKeyEnv]) {
80
+ return new OcraError("CONFIG_CREDENTIALS_MISSING", `No API key for provider "${parseModel(model).providerID}": set ${apiKeyEnv}`);
100
81
  }
101
82
  }
102
83
  return undefined;
@@ -111,9 +92,25 @@ export class DirectRuntime {
111
92
  onEffortRefused: () => this.efforts.refuse(agent, model),
112
93
  };
113
94
  }
114
- target(model) {
95
+ // A model's provider and price, from the configuration's declaration.
96
+ declared(model) {
115
97
  const { providerID, modelID } = parseModel(model);
116
98
  const provider = this.options.providers?.[providerID];
99
+ if (!provider) {
100
+ return new OcraError("CONFIG_INVALID", `"${model}" names no provider declared in configuration; the direct runtime reaches only declared OpenAI-compatible endpoints (use the opencode runtime for ${providerID})`);
101
+ }
102
+ const price = provider.models[modelID];
103
+ if (!price) {
104
+ return new OcraError("CONFIG_INVALID", `"${model}" has no price in the declaration of provider "${providerID}"`);
105
+ }
106
+ return { provider, modelID, price };
107
+ }
108
+ // Only for a model unreachable() let through, so declared() cannot fail.
109
+ target(model) {
110
+ const declared = this.declared(model);
111
+ if (declared instanceof OcraError)
112
+ throw declared;
113
+ const { provider, modelID, price } = declared;
117
114
  const key = provider.apiKeyEnv ? this.options.env[provider.apiKeyEnv] : undefined;
118
115
  return {
119
116
  endpoint: {
@@ -122,7 +119,7 @@ export class DirectRuntime {
122
119
  fetch: this.fetch,
123
120
  },
124
121
  model: modelID,
125
- price: provider.models[modelID],
122
+ price,
126
123
  };
127
124
  }
128
125
  }
@@ -132,7 +129,4 @@ const NO_CONTEXT = {
132
129
  readDiff: () => undefined,
133
130
  searchCode: async () => [],
134
131
  };
135
- function noModel(tier) {
136
- return new OcraError("CONFIG_INVALID", `No ${tier} model configured (set OCRA_MODEL_${tier.toUpperCase()} or models.${tier})`);
137
- }
138
132
  //# sourceMappingURL=runtime.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-cr-agent/runtime-direct",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Agent runtime for Open-CR-Agent that calls OpenAI-compatible endpoints directly",
5
5
  "keywords": [
6
6
  "code-review",
@@ -33,7 +33,7 @@
33
33
  "!dist/**/*.map"
34
34
  ],
35
35
  "dependencies": {
36
- "@open-cr-agent/core": "0.5.0",
36
+ "@open-cr-agent/core": "0.6.0",
37
37
  "zod": "^4.6.5"
38
38
  },
39
39
  "publishConfig": {