@open-cr-agent/runtime-direct 0.3.0 → 0.5.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
@@ -1,3 +1,3 @@
1
1
  # @open-cr-agent/runtime-direct
2
2
 
3
- The agent runtime of [Open-CR-Agent](https://github.com/jma49/Open-CR-Agent), the multi-agent code reviewer, that calls the OpenAI-compatible model endpoints declared in its configuration itself, with nothing else on the network. Most users want [`@open-cr-agent/cli`](https://www.npmjs.com/package/@open-cr-agent/cli), which provides the `ocra` command and installs this package; see the [manual](https://ocra.majincheng.com). All `@open-cr-agent` packages are released together at one version. Apache-2.0.
3
+ The agent runtime of [Open-CR-Agent](https://github.com/jma49/Open-CR-Agent), the multi-agent code reviewer, that calls the OpenAI-compatible model endpoints declared in its configuration itself, with nothing else on the network. Most users want [`@open-cr-agent/cli`](https://www.npmjs.com/package/@open-cr-agent/cli), which provides the `ocra` command and installs this package; see the [manual](https://ocracloud.com). All `@open-cr-agent` packages are released together at one version. Apache-2.0.
@@ -0,0 +1,14 @@
1
+ import type { AppliedSettings, Effort, Sampling } from "@open-cr-agent/core";
2
+ import type { CallParams } from "./openai.js";
3
+ export type EffortStyle = "openai" | "openrouter";
4
+ export declare class EffortLedger {
5
+ private readonly sampling;
6
+ private readonly refused;
7
+ private readonly applied;
8
+ constructor(sampling: Sampling);
9
+ params(agent: string, effort: Effort | undefined, model: string, style?: EffortStyle): CallParams;
10
+ refuse(agent: string, model: string): void;
11
+ appliedTo(agent: string): AppliedSettings | undefined;
12
+ private record;
13
+ }
14
+ //# sourceMappingURL=effort.d.ts.map
package/dist/effort.js ADDED
@@ -0,0 +1,49 @@
1
+ // What each call sends besides the conversation, and what the run applied
2
+ // per agent (ADR-0025). Reasoning models refuse a temperature (OpenAI) or
3
+ // need their own (Anthropic), so a call that asks for an effort other than
4
+ // "none" sends no sampling settings, even after its endpoint refused the
5
+ // effort: the agent's calls stay alike within a run.
6
+ export class EffortLedger {
7
+ sampling;
8
+ // Models whose endpoint refused the effort parameter: later calls go
9
+ // without it instead of paying for another refusal.
10
+ refused = new Set();
11
+ applied = new Map();
12
+ constructor(sampling) {
13
+ this.sampling = sampling;
14
+ }
15
+ params(agent, effort, model, style = "openai") {
16
+ if (effort === undefined)
17
+ return { ...this.sampling };
18
+ const keepsSampling = effort === "none";
19
+ const sends = !this.refused.has(model);
20
+ this.record(agent, sends, keepsSampling ? [] : requested(this.sampling));
21
+ return {
22
+ ...(keepsSampling ? this.sampling : {}),
23
+ ...(sends ? effortParam(effort, style) : {}),
24
+ };
25
+ }
26
+ refuse(agent, model) {
27
+ this.refused.add(model);
28
+ this.record(agent, false, []);
29
+ }
30
+ appliedTo(agent) {
31
+ const applied = this.applied.get(agent);
32
+ return applied && { ...applied };
33
+ }
34
+ record(agent, sent, notApplied) {
35
+ const before = this.applied.get(agent);
36
+ const left = [...new Set([...(before?.notApplied ?? []), ...notApplied])];
37
+ this.applied.set(agent, {
38
+ effort: (before?.effort ?? true) && sent,
39
+ ...(left.length > 0 ? { notApplied: left } : {}),
40
+ });
41
+ }
42
+ }
43
+ function effortParam(effort, style) {
44
+ return style === "openrouter" ? { reasoning: { effort } } : { reasoning_effort: effort };
45
+ }
46
+ function requested(sampling) {
47
+ return ["temperature", "seed"].filter((key) => sampling[key] !== undefined);
48
+ }
49
+ //# sourceMappingURL=effort.js.map
package/dist/loop.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { ModelPrice, ReviewContext, Sampling, ToolDefinition, Usage } from "@open-cr-agent/core";
1
+ import type { ModelPrice, ReviewContext, ToolDefinition, Usage } from "@open-cr-agent/core";
2
2
  import { type AttemptOutcome } from "@open-cr-agent/core/internal";
3
- import { type Endpoint } from "./openai.js";
3
+ import { type CallParams, type Endpoint } from "./openai.js";
4
4
  export interface LoopInput {
5
5
  endpoint: Endpoint;
6
6
  model: string;
@@ -12,7 +12,8 @@ export interface LoopInput {
12
12
  maxSteps: number;
13
13
  resume?: string;
14
14
  timeoutMs: number;
15
- sampling?: Sampling;
15
+ params?: CallParams;
16
+ onEffortRefused?: () => void;
16
17
  signal: AbortSignal;
17
18
  onUsage?: (spent: Usage) => void;
18
19
  }
package/dist/loop.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { addUsage, emptyUsage, errorMessage, REVIEW_TOOLS, } from "@open-cr-agent/core/internal";
2
- import { chat, toolSpec } from "./openai.js";
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
5
5
  // tools, or runs out of steps. The model reaches nothing but these tools and
@@ -23,14 +23,19 @@ export async function runLoop(input) {
23
23
  };
24
24
  const texts = [];
25
25
  let done = false;
26
+ let params = input.params ?? {};
26
27
  while (outcome.steps < input.maxSteps) {
27
28
  const response = await chat(input.endpoint, {
28
29
  model: input.model,
29
30
  messages,
30
31
  ...(specs.length > 0 ? { tools: specs } : {}),
31
- ...input.sampling,
32
+ ...params,
32
33
  }, input.price, signal);
33
34
  outcome.steps += 1;
35
+ if (response.effortDropped) {
36
+ params = withoutEffort(params);
37
+ input.onEffortRefused?.();
38
+ }
34
39
  if (!response.ok) {
35
40
  outcome.error = timeout.aborted
36
41
  ? { message: `timed out after ${Math.round(input.timeoutMs / 1000)}s`, retryable: true }
package/dist/openai.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ModelPrice, ToolDefinition, Usage } from "@open-cr-agent/core";
1
+ import type { Effort, ModelPrice, ToolDefinition, Usage } from "@open-cr-agent/core";
2
2
  import { type QuotaError } from "@open-cr-agent/core/internal";
3
3
  import { z } from "zod";
4
4
  declare const toolCallSchema: z.ZodObject<{
@@ -40,14 +40,20 @@ export interface ChatRequest {
40
40
  tools?: readonly ToolSpec[];
41
41
  temperature?: number;
42
42
  seed?: number;
43
+ reasoning_effort?: Effort;
44
+ reasoning?: {
45
+ effort: Effort;
46
+ };
43
47
  }
48
+ export type CallParams = Pick<ChatRequest, "temperature" | "seed" | "reasoning_effort" | "reasoning">;
44
49
  export interface ChatError {
45
50
  message: string;
46
51
  retryable: boolean;
47
52
  quota?: QuotaError;
48
53
  transient?: boolean;
54
+ effortRefused?: boolean;
49
55
  }
50
- export type ChatResponse = {
56
+ export type ChatResponse = ({
51
57
  ok: true;
52
58
  content: string;
53
59
  toolCalls: ToolCall[];
@@ -55,9 +61,12 @@ export type ChatResponse = {
55
61
  } | {
56
62
  ok: false;
57
63
  error: ChatError;
64
+ }) & {
65
+ effortDropped?: true;
58
66
  };
59
67
  export declare const TRANSIENT_RETRY_MS = 1000;
60
68
  export declare function toolSpec(tool: ToolDefinition): ToolSpec;
61
69
  export declare function chat(endpoint: Endpoint, request: ChatRequest, price: ModelPrice, signal: AbortSignal): Promise<ChatResponse>;
70
+ export declare function withoutEffort<T extends CallParams>(params: T): T;
62
71
  export {};
63
72
  //# sourceMappingURL=openai.d.ts.map
package/dist/openai.js CHANGED
@@ -30,6 +30,8 @@ const errorBodySchema = z.object({
30
30
  error: z.object({ message: z.string(), code: z.number().optional() }),
31
31
  });
32
32
  const AUTH_STATUS = new Set([401, 403]);
33
+ // How an endpoint names the effort parameters in a 400 that refuses them.
34
+ const EFFORT_PARAMETER = /\breasoning(?:_effort)?\b/i;
33
35
  // How much of an error body an error message keeps: enough to diagnose, not
34
36
  // a page of HTML.
35
37
  const BODY_EXCERPT = 300;
@@ -43,10 +45,28 @@ export function toolSpec(tool) {
43
45
  function: { name: tool.name, description: tool.description, parameters },
44
46
  };
45
47
  }
46
- // One request to the endpoint, sent a second time when the first failed in
47
- // a way a moment may cure. Only the key named for this provider goes out,
48
- // and only to its base URL.
48
+ // One request to the endpoint. A refused effort parameter is not the model's
49
+ // failure: the request goes once more without it, so the review still runs
50
+ // and the failback chain does not move.
49
51
  export async function chat(endpoint, request, price, signal) {
52
+ const first = await sendRetrying(endpoint, request, price, signal);
53
+ if (first.ok || !first.error.effortRefused || !hasEffort(request))
54
+ return first;
55
+ return {
56
+ ...(await sendRetrying(endpoint, withoutEffort(request), price, signal)),
57
+ effortDropped: true,
58
+ };
59
+ }
60
+ function hasEffort(params) {
61
+ return params.reasoning_effort !== undefined || params.reasoning !== undefined;
62
+ }
63
+ export function withoutEffort(params) {
64
+ const { reasoning_effort: _effort, reasoning: _reasoning, ...rest } = params;
65
+ return rest;
66
+ }
67
+ // Sent a second time when the first failed in a way a moment may cure. Only
68
+ // the key named for this provider goes out, and only to its base URL.
69
+ async function sendRetrying(endpoint, request, price, signal) {
50
70
  const first = await send(endpoint, request, price, signal);
51
71
  if (first.ok || !first.error.transient)
52
72
  return first;
@@ -121,6 +141,7 @@ function failure(response, body, secrets, reportedCode) {
121
141
  return {
122
142
  message,
123
143
  retryable: !AUTH_STATUS.has(status),
144
+ ...(status === 400 && EFFORT_PARAMETER.test(body) ? { effortRefused: true } : {}),
124
145
  ...(quota ? { quota } : {}),
125
146
  ...(!quota && (status >= 500 || status === 408) ? { transient: true } : {}),
126
147
  };
package/dist/runtime.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type AgentEvent, type AgentRuntime, type AgentTaskSpec, type AppliedSampling, type CompletionRequest, type CompletionResult, type RuntimeOptions } from "@open-cr-agent/core";
1
+ import { type AgentEvent, type AgentRuntime, type AgentTaskSpec, type AppliedSampling, type AppliedSettings, type CompletionRequest, type CompletionResult, type RuntimeOptions } from "@open-cr-agent/core";
2
2
  export interface DirectRuntimeOptions extends RuntimeOptions {
3
3
  fetch?: typeof fetch;
4
4
  }
@@ -9,10 +9,13 @@ export declare class DirectRuntime implements AgentRuntime {
9
9
  private readonly health;
10
10
  private readonly tools;
11
11
  private readonly fetch;
12
+ private readonly efforts;
12
13
  constructor(options: DirectRuntimeOptions);
14
+ appliedTo(agent: string): AppliedSettings | undefined;
13
15
  runTask(spec: AgentTaskSpec, signal: AbortSignal): AsyncIterable<AgentEvent>;
14
16
  complete(request: CompletionRequest, signal: AbortSignal): Promise<CompletionResult>;
15
17
  private unreachable;
18
+ private call;
16
19
  private target;
17
20
  }
18
21
  //# sourceMappingURL=runtime.d.ts.map
package/dist/runtime.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { OcraError, } from "@open-cr-agent/core";
2
- import { completeWithFailback, MAX_AGENT_STEPS, ModelHealth, parseModel, proxiedFetch, RESUME_MESSAGE, reviewTools, withFailback, } from "@open-cr-agent/core/internal";
2
+ import { callChain, completeWithFailback, MAX_AGENT_STEPS, ModelHealth, parseModel, proxiedFetch, RESUME_MESSAGE, reviewTools, withFailback, } from "@open-cr-agent/core/internal";
3
+ import { EffortLedger } from "./effort.js";
3
4
  import { runLoop } from "./loop.js";
4
5
  // Talks to the declared OpenAI-compatible endpoints itself: no OpenCode
5
6
  // process, no catalog fetch, no package install, nothing on disk. A model of
@@ -13,6 +14,7 @@ export class DirectRuntime {
13
14
  health = new ModelHealth();
14
15
  tools;
15
16
  fetch;
17
+ efforts;
16
18
  constructor(options) {
17
19
  this.options = options;
18
20
  this.tools = [...reviewTools, ...options.tools];
@@ -22,9 +24,13 @@ export class DirectRuntime {
22
24
  ...(temperature === undefined ? {} : { temperature }),
23
25
  ...(seed === undefined ? {} : { seed }),
24
26
  };
27
+ this.efforts = new EffortLedger(this.sampling);
28
+ }
29
+ appliedTo(agent) {
30
+ return this.efforts.appliedTo(agent);
25
31
  }
26
32
  async *runTask(spec, signal) {
27
- const chain = this.options.models[spec.modelTier] ?? [];
33
+ const chain = callChain(this.options.models, spec.modelTier, spec.models);
28
34
  const refused = chain.length === 0 ? noModel(spec.modelTier) : this.unreachable(chain);
29
35
  if (refused) {
30
36
  yield { type: "error", taskId: spec.taskId, error: refused.message, retryable: false };
@@ -33,11 +39,13 @@ export class DirectRuntime {
33
39
  yield* withFailback({
34
40
  taskId: spec.taskId,
35
41
  tier: spec.modelTier,
42
+ ...(spec.models?.length ? { agent: spec.reviewer } : {}),
36
43
  chain,
37
44
  health: this.health,
38
45
  signal,
39
46
  attempt: (model, onUsage) => runLoop({
40
47
  ...this.target(model),
48
+ ...this.call({ agent: spec.reviewer, effort: spec.effort, model }),
41
49
  system: spec.systemPrompt,
42
50
  user: spec.userPrompt,
43
51
  tools: this.tools,
@@ -45,31 +53,31 @@ export class DirectRuntime {
45
53
  maxSteps: MAX_AGENT_STEPS,
46
54
  resume: RESUME_MESSAGE,
47
55
  timeoutMs: spec.timeoutMs,
48
- sampling: this.sampling,
49
56
  signal,
50
57
  onUsage,
51
58
  }),
52
59
  });
53
60
  }
54
61
  async complete(request, signal) {
55
- const chain = this.options.models[request.tier] ?? [];
62
+ const chain = callChain(this.options.models, request.tier, request.models);
56
63
  const refused = chain.length === 0 ? noModel(request.tier) : this.unreachable(chain);
57
64
  if (refused)
58
65
  throw refused;
59
66
  return completeWithFailback({
60
67
  tier: request.tier,
68
+ ...(request.models?.length ? { agent: request.agent ?? request.tier } : {}),
61
69
  chain,
62
70
  health: this.health,
63
71
  signal,
64
72
  attempt: (model) => runLoop({
65
73
  ...this.target(model),
74
+ ...this.call({ agent: request.agent ?? request.tier, effort: request.effort, model }),
66
75
  system: request.system,
67
76
  user: request.user,
68
77
  tools: [],
69
78
  context: NO_CONTEXT,
70
79
  maxSteps: 1,
71
80
  timeoutMs: request.timeoutMs,
72
- sampling: this.sampling,
73
81
  signal,
74
82
  }),
75
83
  });
@@ -93,6 +101,16 @@ export class DirectRuntime {
93
101
  }
94
102
  return undefined;
95
103
  }
104
+ // What one attempt sends besides the conversation, and where a refused
105
+ // effort is recorded.
106
+ call({ agent, effort, model }) {
107
+ const { providerID } = parseModel(model);
108
+ const style = this.options.providers?.[providerID]?.effort;
109
+ return {
110
+ params: this.efforts.params(agent, effort, model, style),
111
+ onEffortRefused: () => this.efforts.refuse(agent, model),
112
+ };
113
+ }
96
114
  target(model) {
97
115
  const { providerID, modelID } = parseModel(model);
98
116
  const provider = this.options.providers?.[providerID];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-cr-agent/runtime-direct",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Agent runtime for Open-CR-Agent that calls OpenAI-compatible endpoints directly",
5
5
  "keywords": [
6
6
  "code-review",
@@ -8,7 +8,7 @@
8
8
  "agents"
9
9
  ],
10
10
  "license": "Apache-2.0",
11
- "homepage": "https://ocra.majincheng.com",
11
+ "homepage": "https://ocracloud.com",
12
12
  "repository": {
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/jma49/Open-CR-Agent.git",
@@ -33,7 +33,7 @@
33
33
  "!dist/**/*.map"
34
34
  ],
35
35
  "dependencies": {
36
- "@open-cr-agent/core": "0.3.0",
36
+ "@open-cr-agent/core": "0.5.0",
37
37
  "zod": "^4.6.5"
38
38
  },
39
39
  "publishConfig": {