@open-cr-agent/runtime-opencode 0.4.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-setup.d.ts +14 -0
- package/dist/effort-setup.js +43 -0
- package/dist/effort.d.ts +27 -0
- package/dist/effort.js +149 -0
- package/dist/opencode-config.d.ts +101 -0
- package/dist/opencode-config.js +137 -0
- package/dist/runtime.d.ts +10 -102
- package/dist/runtime.js +97 -174
- package/dist/server-env.d.ts +0 -2
- package/dist/server-env.js +2 -2
- package/dist/session-outcome.d.ts +36 -33
- package/dist/session-outcome.js +41 -1
- package/dist/session-prompt.d.ts +3 -10
- package/dist/session-prompt.js +13 -12
- package/dist/tool-server.js +1 -1
- package/dist/transport.js +4 -0
- package/package.json +2 -2
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CustomProvider } from "@open-cr-agent/core";
|
|
2
|
+
import type { OpencodeClient } from "@opencode-ai/sdk/v2";
|
|
3
|
+
import { EffortRoutes } from "./effort.js";
|
|
4
|
+
type ConfigApi = Pick<OpencodeClient["config"], "providers">;
|
|
5
|
+
export interface EffortSetup {
|
|
6
|
+
models: readonly string[];
|
|
7
|
+
custom?: Readonly<Record<string, CustomProvider>>;
|
|
8
|
+
probe: ConfigApi;
|
|
9
|
+
workspace: ConfigApi;
|
|
10
|
+
file: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function setUpEfforts(setup: EffortSetup): Promise<EffortRoutes>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=effort-setup.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { EffortRoutes } from "./effort.js";
|
|
3
|
+
const CATALOG_TIMEOUT_MS = 30_000;
|
|
4
|
+
// The variants derive thinking budgets from the models' output limits,
|
|
5
|
+
// which only OpenCode's catalog knows, and the catalog is loaded only once
|
|
6
|
+
// OpenCode runs: the variants are written after it started and before the
|
|
7
|
+
// workspace is first used. Any failure leaves effort unsent, with a warning
|
|
8
|
+
// per agent, and never fails the review.
|
|
9
|
+
export async function setUpEfforts(setup) {
|
|
10
|
+
const { models, custom = {} } = setup;
|
|
11
|
+
const everyModel = new Map(models.map((m) => [m, 0]));
|
|
12
|
+
if (new EffortRoutes(models, custom, everyModel).empty)
|
|
13
|
+
return new EffortRoutes([]);
|
|
14
|
+
try {
|
|
15
|
+
const catalog = await catalogModels(setup.probe);
|
|
16
|
+
const routes = new EffortRoutes(models, custom, new Map([...catalog].map(([model, entry]) => [model, entry.outputLimit])));
|
|
17
|
+
if (routes.empty)
|
|
18
|
+
return routes;
|
|
19
|
+
await writeFile(setup.file, JSON.stringify({ provider: routes.providerConfig() }), {
|
|
20
|
+
mode: 0o600,
|
|
21
|
+
});
|
|
22
|
+
const loaded = await catalogModels(setup.workspace);
|
|
23
|
+
routes.keepLoaded(new Map([...loaded].map(([model, entry]) => [model, entry.variants])));
|
|
24
|
+
return routes;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return new EffortRoutes([]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function catalogModels(config) {
|
|
31
|
+
const answer = await config.providers({}, { signal: AbortSignal.timeout(CATALOG_TIMEOUT_MS) });
|
|
32
|
+
const models = new Map();
|
|
33
|
+
for (const provider of answer.data?.providers ?? []) {
|
|
34
|
+
for (const [id, model] of Object.entries(provider.models)) {
|
|
35
|
+
models.set(`${provider.id}/${id}`, {
|
|
36
|
+
outputLimit: model.limit?.output ?? 0,
|
|
37
|
+
variants: new Set(Object.keys(model.variants ?? {})),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return models;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=effort-setup.js.map
|
package/dist/effort.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type AppliedSettings, type CustomProvider, type Effort, type Sampling } from "@open-cr-agent/core";
|
|
2
|
+
type Options = Record<string, unknown>;
|
|
3
|
+
export declare function effortOptions(model: string, level: Effort, custom?: Readonly<Record<string, CustomProvider>>, outputLimit?: number): Options | undefined;
|
|
4
|
+
export declare class EffortRoutes {
|
|
5
|
+
private readonly routes;
|
|
6
|
+
constructor(models: readonly string[], custom?: Readonly<Record<string, CustomProvider>>, known?: ReadonlyMap<string, number>);
|
|
7
|
+
get empty(): boolean;
|
|
8
|
+
providerConfig(): Record<string, {
|
|
9
|
+
models: Record<string, {
|
|
10
|
+
variants: Options;
|
|
11
|
+
}>;
|
|
12
|
+
}>;
|
|
13
|
+
keepLoaded(loaded: ReadonlyMap<string, ReadonlySet<string>>): void;
|
|
14
|
+
route(model: string, level: Effort): {
|
|
15
|
+
variant?: string;
|
|
16
|
+
} | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare class AppliedEfforts {
|
|
19
|
+
private readonly applied;
|
|
20
|
+
private readonly sampling;
|
|
21
|
+
constructor(sampling?: Sampling);
|
|
22
|
+
record(agent: string, effort: Effort, model: string, sent: boolean): boolean;
|
|
23
|
+
appliedTo(agent: string): AppliedSettings | undefined;
|
|
24
|
+
private requested;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=effort.d.ts.map
|
package/dist/effort.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { EFFORT_LEVELS, effortCapability, parseModel, thinkingBudget, } from "@open-cr-agent/core";
|
|
2
|
+
const GOOGLE_PROVIDERS = new Set(["google", "google-vertex"]);
|
|
3
|
+
// OpenCode merges a configured variant over a built-in one of the same name
|
|
4
|
+
// (1.18.32 has "high" and "max" for most models), so ocra's carry a prefix.
|
|
5
|
+
function effortVariant(level) {
|
|
6
|
+
return `ocra-${level}`;
|
|
7
|
+
}
|
|
8
|
+
// The provider options that carry a level to one model through OpenCode, in
|
|
9
|
+
// the option names of the provider's AI SDK package; {} when the level means
|
|
10
|
+
// sending nothing (no thinking for Anthropic), undefined when ocra knows no
|
|
11
|
+
// way to send it. A provider declared in configuration takes a level the
|
|
12
|
+
// way it says (`effort`), for any model the table does not rule out, as on
|
|
13
|
+
// the direct runtime.
|
|
14
|
+
export function effortOptions(model, level, custom = {}, outputLimit) {
|
|
15
|
+
const { providerID } = parseModel(model);
|
|
16
|
+
const capability = effortCapability(model);
|
|
17
|
+
if (capability && !capability.levels.includes(level))
|
|
18
|
+
return undefined;
|
|
19
|
+
const declared = custom[providerID];
|
|
20
|
+
if (declared) {
|
|
21
|
+
return declared.effort === "openrouter"
|
|
22
|
+
? { reasoning: { effort: level } }
|
|
23
|
+
: { reasoningEffort: level };
|
|
24
|
+
}
|
|
25
|
+
if (!capability)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (providerID === "openrouter")
|
|
28
|
+
return { reasoning: { effort: level } };
|
|
29
|
+
const google = GOOGLE_PROVIDERS.has(providerID);
|
|
30
|
+
switch (capability.parameter) {
|
|
31
|
+
case "reasoningEffort":
|
|
32
|
+
return providerID === "openai" ? { reasoningEffort: level } : undefined;
|
|
33
|
+
case "thinkingLevel":
|
|
34
|
+
return google ? { thinkingConfig: { thinkingLevel: level } } : undefined;
|
|
35
|
+
case "thinkingBudget": {
|
|
36
|
+
const budget = thinkingBudget(capability, level, outputLimit);
|
|
37
|
+
if (budget === undefined)
|
|
38
|
+
return undefined;
|
|
39
|
+
if (providerID === "anthropic") {
|
|
40
|
+
return budget === 0 ? {} : { thinking: { type: "enabled", budgetTokens: budget } };
|
|
41
|
+
}
|
|
42
|
+
// OpenCode asks Gemini for thoughts, which Gemini refuses with
|
|
43
|
+
// thinking off.
|
|
44
|
+
if (google) {
|
|
45
|
+
return {
|
|
46
|
+
thinkingConfig: budget === 0
|
|
47
|
+
? { thinkingBudget: 0, includeThoughts: false }
|
|
48
|
+
: { thinkingBudget: budget },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// What each model can take, as OpenCode variants: one per (model, level)
|
|
56
|
+
// that sends something, so two agents on one model at different levels get
|
|
57
|
+
// different options. Only models OpenCode's catalog (or the configuration)
|
|
58
|
+
// knows get any, since declaring an unknown model would make OpenCode
|
|
59
|
+
// accept it at a price of 0.
|
|
60
|
+
export class EffortRoutes {
|
|
61
|
+
routes = new Map();
|
|
62
|
+
constructor(models, custom = {},
|
|
63
|
+
// Output limits by model, from OpenCode's catalog; 0 when it has none.
|
|
64
|
+
known = new Map()) {
|
|
65
|
+
for (const model of new Set(models)) {
|
|
66
|
+
if (!known.has(model))
|
|
67
|
+
continue;
|
|
68
|
+
const levels = new Map();
|
|
69
|
+
for (const level of EFFORT_LEVELS) {
|
|
70
|
+
const options = effortOptions(model, level, custom, known.get(model));
|
|
71
|
+
if (options)
|
|
72
|
+
levels.set(level, options);
|
|
73
|
+
}
|
|
74
|
+
if (levels.size > 0)
|
|
75
|
+
this.routes.set(model, levels);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
get empty() {
|
|
79
|
+
return this.routes.size === 0;
|
|
80
|
+
}
|
|
81
|
+
// The `provider` section that declares the variants.
|
|
82
|
+
providerConfig() {
|
|
83
|
+
const config = {};
|
|
84
|
+
for (const [model, levels] of this.routes) {
|
|
85
|
+
const { providerID, modelID } = parseModel(model);
|
|
86
|
+
const variants = Object.fromEntries([...levels]
|
|
87
|
+
.filter(([, options]) => Object.keys(options).length > 0)
|
|
88
|
+
.map(([level, options]) => [effortVariant(level), options]));
|
|
89
|
+
if (Object.keys(variants).length === 0)
|
|
90
|
+
continue;
|
|
91
|
+
config[providerID] ??= { models: {} };
|
|
92
|
+
config[providerID].models[modelID] = { variants };
|
|
93
|
+
}
|
|
94
|
+
return config;
|
|
95
|
+
}
|
|
96
|
+
// Keeps the levels whose variant OpenCode loaded, so a level is never
|
|
97
|
+
// recorded as sent when OpenCode would have ignored its variant.
|
|
98
|
+
keepLoaded(loaded) {
|
|
99
|
+
for (const [model, levels] of this.routes) {
|
|
100
|
+
for (const [level, options] of levels) {
|
|
101
|
+
const needsVariant = Object.keys(options).length > 0;
|
|
102
|
+
if (needsVariant && !loaded.get(model)?.has(effortVariant(level)))
|
|
103
|
+
levels.delete(level);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Undefined: the level cannot be sent to the model. No variant: the level
|
|
108
|
+
// is sent by sending nothing.
|
|
109
|
+
route(model, level) {
|
|
110
|
+
const options = this.routes.get(model)?.get(level);
|
|
111
|
+
if (!options)
|
|
112
|
+
return undefined;
|
|
113
|
+
return Object.keys(options).length > 0 ? { variant: effortVariant(level) } : {};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// What the run applied per agent (ADR-0025), as the direct runtime records
|
|
117
|
+
// it: a call that asks for an effort other than "none" goes without the
|
|
118
|
+
// configured sampling, even when its level could not be sent, so an
|
|
119
|
+
// agent's calls stay alike within a run.
|
|
120
|
+
export class AppliedEfforts {
|
|
121
|
+
applied = new Map();
|
|
122
|
+
sampling;
|
|
123
|
+
constructor(sampling = {}) {
|
|
124
|
+
this.sampling = sampling;
|
|
125
|
+
}
|
|
126
|
+
// Whether the call keeps the configured sampling.
|
|
127
|
+
record(agent, effort, model, sent) {
|
|
128
|
+
const keepsSampling = effort === "none";
|
|
129
|
+
const before = this.applied.get(agent);
|
|
130
|
+
const notApplied = [
|
|
131
|
+
...new Set([...(before?.notApplied ?? []), ...(keepsSampling ? [] : this.requested())]),
|
|
132
|
+
];
|
|
133
|
+
const unsupported = [...new Set([...(before?.unsupported ?? []), ...(sent ? [] : [model])])];
|
|
134
|
+
this.applied.set(agent, {
|
|
135
|
+
effort: (before?.effort ?? true) && sent,
|
|
136
|
+
...(unsupported.length > 0 ? { unsupported } : {}),
|
|
137
|
+
...(notApplied.length > 0 ? { notApplied } : {}),
|
|
138
|
+
});
|
|
139
|
+
return keepsSampling;
|
|
140
|
+
}
|
|
141
|
+
appliedTo(agent) {
|
|
142
|
+
const applied = this.applied.get(agent);
|
|
143
|
+
return applied && structuredClone(applied);
|
|
144
|
+
}
|
|
145
|
+
requested() {
|
|
146
|
+
return ["temperature", "seed"].filter((key) => this.sampling[key] !== undefined);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=effort.js.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { type AppliedSampling, type CustomProvider, type Sampling } from "@open-cr-agent/core";
|
|
2
|
+
import type { ToolServer } from "./tool-server.js";
|
|
3
|
+
export declare const MCP_SERVER = "ocra";
|
|
4
|
+
export declare const REVIEW_AGENT = "ocra-reviewer";
|
|
5
|
+
export declare const HELPER_AGENT = "ocra-helper";
|
|
6
|
+
export declare const WITHOUT_SAMPLING = "-effort";
|
|
7
|
+
export declare const HELPER_AGENT_STEPS = 2;
|
|
8
|
+
export declare const OPENCODE_BUILTIN_TOOLS: readonly ["invalid", "question", "bash", "read", "glob", "grep", "edit", "write", "task", "webfetch", "todowrite", "websearch", "skill", "apply_patch"];
|
|
9
|
+
export declare const DISABLED_BUILTINS: {
|
|
10
|
+
[k: string]: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function openCodeSampling(sampling?: Sampling): AppliedSampling;
|
|
13
|
+
export declare function openCodeConfig(tools: Pick<ToolServer, "url" | "headers">, helperTools: Record<string, boolean>, custom?: Readonly<Record<string, CustomProvider>>, sampling?: Sampling): {
|
|
14
|
+
share: string;
|
|
15
|
+
autoupdate: boolean;
|
|
16
|
+
provider?: {
|
|
17
|
+
[k: string]: {
|
|
18
|
+
npm: string;
|
|
19
|
+
name: string;
|
|
20
|
+
options: {
|
|
21
|
+
baseURL: string;
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
};
|
|
24
|
+
models: {
|
|
25
|
+
[k: string]: {
|
|
26
|
+
name: string;
|
|
27
|
+
temperature?: boolean;
|
|
28
|
+
cost: {
|
|
29
|
+
input: number;
|
|
30
|
+
output: number;
|
|
31
|
+
cache_read?: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
mcp: {
|
|
38
|
+
ocra: {
|
|
39
|
+
type: string;
|
|
40
|
+
url: string;
|
|
41
|
+
headers: Record<string, string>;
|
|
42
|
+
oauth: boolean;
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
agent: {
|
|
47
|
+
[k: string]: {
|
|
48
|
+
mode: string;
|
|
49
|
+
prompt: string;
|
|
50
|
+
steps: number;
|
|
51
|
+
tools: {
|
|
52
|
+
[k: string]: boolean;
|
|
53
|
+
};
|
|
54
|
+
permission: {
|
|
55
|
+
edit: string;
|
|
56
|
+
bash: string;
|
|
57
|
+
webfetch: string;
|
|
58
|
+
skill: string;
|
|
59
|
+
};
|
|
60
|
+
} | {
|
|
61
|
+
mode: string;
|
|
62
|
+
prompt: string;
|
|
63
|
+
steps: number;
|
|
64
|
+
tools: Record<string, boolean>;
|
|
65
|
+
permission: {
|
|
66
|
+
edit: string;
|
|
67
|
+
bash: string;
|
|
68
|
+
webfetch: string;
|
|
69
|
+
skill: string;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
} | {
|
|
73
|
+
"ocra-reviewer": {
|
|
74
|
+
mode: string;
|
|
75
|
+
prompt: string;
|
|
76
|
+
steps: number;
|
|
77
|
+
tools: {
|
|
78
|
+
[k: string]: boolean;
|
|
79
|
+
};
|
|
80
|
+
permission: {
|
|
81
|
+
edit: string;
|
|
82
|
+
bash: string;
|
|
83
|
+
webfetch: string;
|
|
84
|
+
skill: string;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
"ocra-helper": {
|
|
88
|
+
mode: string;
|
|
89
|
+
prompt: string;
|
|
90
|
+
steps: number;
|
|
91
|
+
tools: Record<string, boolean>;
|
|
92
|
+
permission: {
|
|
93
|
+
edit: string;
|
|
94
|
+
bash: string;
|
|
95
|
+
webfetch: string;
|
|
96
|
+
skill: string;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=opencode-config.d.ts.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { MAX_AGENT_STEPS, OcraError, } from "@open-cr-agent/core";
|
|
2
|
+
export const MCP_SERVER = "ocra";
|
|
3
|
+
export const REVIEW_AGENT = "ocra-reviewer";
|
|
4
|
+
export const HELPER_AGENT = "ocra-helper";
|
|
5
|
+
// The same agents without the configured temperature, for calls that send
|
|
6
|
+
// an effort (ADR-0025): OpenCode sets sampling per agent.
|
|
7
|
+
export const WITHOUT_SAMPLING = "-effort";
|
|
8
|
+
// Must not be empty: OpenCode falls back to its full coding prompt otherwise.
|
|
9
|
+
const REVIEW_AGENT_PROMPT = "You are a code review agent run by ocra. Follow the review instructions below.";
|
|
10
|
+
const HELPER_AGENT_PROMPT = "You answer exactly as the instructions below ask, with no tools.";
|
|
11
|
+
// The helper answers in one step and has no tools. It still needs two:
|
|
12
|
+
// OpenCode appends an assistant message on an agent's last allowed step, and
|
|
13
|
+
// Gemini rejects a request that ends with a model turn (#66).
|
|
14
|
+
export const HELPER_AGENT_STEPS = 2;
|
|
15
|
+
// Every OpenCode built-in tool of the pinned version; a test fails when an
|
|
16
|
+
// upgrade adds one, so a new write-capable tool can never be enabled silently.
|
|
17
|
+
export const OPENCODE_BUILTIN_TOOLS = [
|
|
18
|
+
"invalid",
|
|
19
|
+
"question",
|
|
20
|
+
"bash",
|
|
21
|
+
"read",
|
|
22
|
+
"glob",
|
|
23
|
+
"grep",
|
|
24
|
+
"edit",
|
|
25
|
+
"write",
|
|
26
|
+
"task",
|
|
27
|
+
"webfetch",
|
|
28
|
+
"todowrite",
|
|
29
|
+
"websearch",
|
|
30
|
+
"skill",
|
|
31
|
+
"apply_patch",
|
|
32
|
+
];
|
|
33
|
+
export const DISABLED_BUILTINS = Object.fromEntries(OPENCODE_BUILTIN_TOOLS.map((t) => [t, false]));
|
|
34
|
+
// OpenCode 1.18.32 takes a temperature per agent and sends it when the
|
|
35
|
+
// model's catalog entry says the model accepts one; a model declared in
|
|
36
|
+
// configuration is marked so (providerConfig). It has no seed setting: the
|
|
37
|
+
// request it builds carries temperature, topP, topK and the output limit only.
|
|
38
|
+
export function openCodeSampling(sampling = {}) {
|
|
39
|
+
return {
|
|
40
|
+
...(sampling.temperature === undefined ? {} : { temperature: sampling.temperature }),
|
|
41
|
+
...(sampling.seed === undefined ? {} : { notApplied: ["seed"] }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function openCodeConfig(tools, helperTools, custom = {}, sampling = {}) {
|
|
45
|
+
const permission = { edit: "deny", bash: "deny", webfetch: "deny", skill: "deny" };
|
|
46
|
+
const { temperature } = sampling;
|
|
47
|
+
const agents = {
|
|
48
|
+
[REVIEW_AGENT]: {
|
|
49
|
+
mode: "primary",
|
|
50
|
+
prompt: REVIEW_AGENT_PROMPT,
|
|
51
|
+
steps: MAX_AGENT_STEPS,
|
|
52
|
+
tools: DISABLED_BUILTINS,
|
|
53
|
+
permission,
|
|
54
|
+
},
|
|
55
|
+
[HELPER_AGENT]: {
|
|
56
|
+
mode: "primary",
|
|
57
|
+
prompt: HELPER_AGENT_PROMPT,
|
|
58
|
+
steps: HELPER_AGENT_STEPS,
|
|
59
|
+
tools: helperTools,
|
|
60
|
+
permission,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
share: "disabled",
|
|
65
|
+
autoupdate: false,
|
|
66
|
+
...(Object.keys(custom).length > 0
|
|
67
|
+
? { provider: providerConfig(custom, temperature !== undefined) }
|
|
68
|
+
: {}),
|
|
69
|
+
mcp: {
|
|
70
|
+
[MCP_SERVER]: {
|
|
71
|
+
type: "remote",
|
|
72
|
+
url: tools.url,
|
|
73
|
+
headers: tools.headers,
|
|
74
|
+
oauth: false,
|
|
75
|
+
enabled: true,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
agent: temperature === undefined
|
|
79
|
+
? agents
|
|
80
|
+
: Object.fromEntries(Object.entries(agents).flatMap(([name, agent]) => [
|
|
81
|
+
[name, { ...agent, temperature }],
|
|
82
|
+
[`${name}${WITHOUT_SAMPLING}`, agent],
|
|
83
|
+
])),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// OpenCode's form of a provider declared in configuration. OpenCode bundles
|
|
87
|
+
// the OpenAI-compatible client, so nothing is installed to reach it
|
|
88
|
+
// (docs/spikes/0002). The key stays in the environment: OpenCode reads
|
|
89
|
+
// {env:NAME} itself, and the configuration, which OpenCode may log, never
|
|
90
|
+
// holds it. Prices are per million tokens, as OpenCode's catalog has them.
|
|
91
|
+
// OpenCode assumes a declared model takes no temperature and drops one;
|
|
92
|
+
// with a temperature configured, the models are marked as taking it.
|
|
93
|
+
function providerConfig(custom, temperature) {
|
|
94
|
+
for (const [id, provider] of Object.entries(custom))
|
|
95
|
+
checkProvider(id, provider);
|
|
96
|
+
return Object.fromEntries(Object.entries(custom).map(([id, provider]) => [
|
|
97
|
+
id,
|
|
98
|
+
{
|
|
99
|
+
npm: "@ai-sdk/openai-compatible",
|
|
100
|
+
name: id,
|
|
101
|
+
options: {
|
|
102
|
+
baseURL: provider.baseUrl,
|
|
103
|
+
...(provider.apiKeyEnv ? { apiKey: `{env:${provider.apiKeyEnv}}` } : {}),
|
|
104
|
+
},
|
|
105
|
+
models: Object.fromEntries(Object.entries(provider.models).map(([model, price]) => [
|
|
106
|
+
model,
|
|
107
|
+
{
|
|
108
|
+
name: model,
|
|
109
|
+
...(temperature ? { temperature: true } : {}),
|
|
110
|
+
cost: {
|
|
111
|
+
input: price.input,
|
|
112
|
+
output: price.output,
|
|
113
|
+
...(price.cachedInput === undefined ? {} : { cache_read: price.cachedInput }),
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
])),
|
|
117
|
+
},
|
|
118
|
+
]));
|
|
119
|
+
}
|
|
120
|
+
// Checked here once, whatever declared the provider: a configuration file,
|
|
121
|
+
// ocra Cloud or another caller of this package.
|
|
122
|
+
function checkProvider(id, provider) {
|
|
123
|
+
const fields = [
|
|
124
|
+
["id", id],
|
|
125
|
+
["baseUrl", provider.baseUrl],
|
|
126
|
+
...Object.keys(provider.models).map((model) => ["model", model]),
|
|
127
|
+
];
|
|
128
|
+
for (const [field, value] of fields) {
|
|
129
|
+
if (/[{}]/.test(value)) {
|
|
130
|
+
throw new OcraError("CONFIG_INVALID", `Provider "${id}": ${field} must not contain { or }`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (provider.apiKeyEnv !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(provider.apiKeyEnv)) {
|
|
134
|
+
throw new OcraError("CONFIG_INVALID", `Provider "${id}": apiKeyEnv must be an environment variable name`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=opencode-config.js.map
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,120 +1,28 @@
|
|
|
1
|
-
import { type AgentEvent, type AgentRuntime, type AgentTaskSpec, type AppliedSampling, type
|
|
2
|
-
import { type ToolServer } from "./tool-server.js";
|
|
3
|
-
export declare const MCP_SERVER = "ocra";
|
|
4
|
-
export declare const HELPER_AGENT_STEPS = 2;
|
|
5
|
-
export declare const REVIEW_RESUME: {
|
|
6
|
-
doneTool: "task_done";
|
|
7
|
-
maxSteps: number;
|
|
8
|
-
message: string;
|
|
9
|
-
};
|
|
10
|
-
export declare const OPENCODE_BUILTIN_TOOLS: readonly ["invalid", "question", "bash", "read", "glob", "grep", "edit", "write", "task", "webfetch", "todowrite", "websearch", "skill", "apply_patch"];
|
|
1
|
+
import { type AgentEvent, type AgentRuntime, type AgentTaskSpec, type AppliedSampling, type AppliedSettings, type CompletionRequest, type CompletionResult, type RuntimeOptions } from "@open-cr-agent/core";
|
|
11
2
|
export interface OpenCodeRuntimeOptions extends RuntimeOptions {
|
|
12
3
|
binary?: string;
|
|
13
4
|
}
|
|
14
5
|
export declare class OpenCodeRuntime implements AgentRuntime {
|
|
15
|
-
private readonly options;
|
|
16
6
|
readonly name = "opencode";
|
|
17
7
|
readonly sampling: AppliedSampling;
|
|
18
8
|
private infra;
|
|
19
9
|
private context;
|
|
20
|
-
private readonly
|
|
10
|
+
private readonly chains;
|
|
21
11
|
private readonly helperTools;
|
|
12
|
+
private readonly applied;
|
|
13
|
+
private readonly options;
|
|
22
14
|
constructor(options: OpenCodeRuntimeOptions);
|
|
15
|
+
appliedTo(agent: string): AppliedSettings | undefined;
|
|
23
16
|
runTask(spec: AgentTaskSpec, signal: AbortSignal): AsyncIterable<AgentEvent>;
|
|
24
17
|
complete(request: CompletionRequest, signal: AbortSignal): Promise<CompletionResult>;
|
|
25
18
|
dispose(): Promise<void>;
|
|
19
|
+
private attemptTask;
|
|
20
|
+
private attemptCompletion;
|
|
21
|
+
private effortCall;
|
|
26
22
|
private prompt;
|
|
23
|
+
private unprepared;
|
|
27
24
|
private start;
|
|
28
25
|
private launch;
|
|
29
26
|
}
|
|
30
|
-
export declare function
|
|
31
|
-
share: string;
|
|
32
|
-
autoupdate: boolean;
|
|
33
|
-
provider?: {
|
|
34
|
-
[k: string]: {
|
|
35
|
-
npm: string;
|
|
36
|
-
name: string;
|
|
37
|
-
options: {
|
|
38
|
-
baseURL: string;
|
|
39
|
-
apiKey?: string;
|
|
40
|
-
};
|
|
41
|
-
models: {
|
|
42
|
-
[k: string]: {
|
|
43
|
-
name: string;
|
|
44
|
-
temperature?: boolean;
|
|
45
|
-
cost: {
|
|
46
|
-
input: number;
|
|
47
|
-
output: number;
|
|
48
|
-
cache_read?: number;
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
mcp: {
|
|
55
|
-
ocra: {
|
|
56
|
-
type: string;
|
|
57
|
-
url: string;
|
|
58
|
-
headers: Record<string, string>;
|
|
59
|
-
oauth: boolean;
|
|
60
|
-
enabled: boolean;
|
|
61
|
-
};
|
|
62
|
-
};
|
|
63
|
-
agent: {
|
|
64
|
-
"ocra-reviewer": {
|
|
65
|
-
temperature?: never;
|
|
66
|
-
mode: string;
|
|
67
|
-
prompt: string;
|
|
68
|
-
steps: number;
|
|
69
|
-
tools: {
|
|
70
|
-
[k: string]: boolean;
|
|
71
|
-
};
|
|
72
|
-
permission: {
|
|
73
|
-
edit: string;
|
|
74
|
-
bash: string;
|
|
75
|
-
webfetch: string;
|
|
76
|
-
skill: string;
|
|
77
|
-
};
|
|
78
|
-
} | {
|
|
79
|
-
temperature: number;
|
|
80
|
-
mode: string;
|
|
81
|
-
prompt: string;
|
|
82
|
-
steps: number;
|
|
83
|
-
tools: {
|
|
84
|
-
[k: string]: boolean;
|
|
85
|
-
};
|
|
86
|
-
permission: {
|
|
87
|
-
edit: string;
|
|
88
|
-
bash: string;
|
|
89
|
-
webfetch: string;
|
|
90
|
-
skill: string;
|
|
91
|
-
};
|
|
92
|
-
};
|
|
93
|
-
"ocra-helper": {
|
|
94
|
-
temperature?: never;
|
|
95
|
-
mode: string;
|
|
96
|
-
prompt: string;
|
|
97
|
-
steps: number;
|
|
98
|
-
tools: Record<string, boolean>;
|
|
99
|
-
permission: {
|
|
100
|
-
edit: string;
|
|
101
|
-
bash: string;
|
|
102
|
-
webfetch: string;
|
|
103
|
-
skill: string;
|
|
104
|
-
};
|
|
105
|
-
} | {
|
|
106
|
-
temperature: number;
|
|
107
|
-
mode: string;
|
|
108
|
-
prompt: string;
|
|
109
|
-
steps: number;
|
|
110
|
-
tools: Record<string, boolean>;
|
|
111
|
-
permission: {
|
|
112
|
-
edit: string;
|
|
113
|
-
bash: string;
|
|
114
|
-
webfetch: string;
|
|
115
|
-
skill: string;
|
|
116
|
-
};
|
|
117
|
-
};
|
|
118
|
-
};
|
|
119
|
-
};
|
|
27
|
+
export declare function providersOf(options: Pick<RuntimeOptions, "models" | "agentModels">): string[];
|
|
120
28
|
//# sourceMappingURL=runtime.d.ts.map
|
package/dist/runtime.js
CHANGED
|
@@ -2,58 +2,32 @@ import { rmSync } from "node:fs";
|
|
|
2
2
|
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { OcraError, } from "@open-cr-agent/core";
|
|
6
|
-
import { completeWithFailback, MAX_AGENT_STEPS, ModelHealth, parseModel, RESUME_MESSAGE, REVIEW_TOOLS, reviewTools, withFailback, withoutSecrets, } from "@open-cr-agent/core/internal";
|
|
5
|
+
import { ChainRunner, MAX_AGENT_STEPS, OcraError, parseModel, RESUME_MESSAGE, REVIEW_TOOLS, reviewTools, withoutSecrets, } from "@open-cr-agent/core";
|
|
7
6
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2";
|
|
8
7
|
import { resolveOpencodeBinary } from "./binary.js";
|
|
8
|
+
import { AppliedEfforts } from "./effort.js";
|
|
9
|
+
import { setUpEfforts } from "./effort-setup.js";
|
|
10
|
+
import { DISABLED_BUILTINS, HELPER_AGENT, MCP_SERVER, openCodeConfig, openCodeSampling, REVIEW_AGENT, WITHOUT_SAMPLING, } from "./opencode-config.js";
|
|
9
11
|
import { startOpencodeServer } from "./opencode-server.js";
|
|
10
12
|
import { credentialValues, missingCredentials, serverEnv } from "./server-env.js";
|
|
11
13
|
import { promptSession } from "./session-prompt.js";
|
|
12
14
|
import { startToolServer } from "./tool-server.js";
|
|
13
15
|
import { createUntimedDispatcher, untimedFetch } from "./transport.js";
|
|
14
|
-
export const MCP_SERVER = "ocra";
|
|
15
|
-
const REVIEW_AGENT = "ocra-reviewer";
|
|
16
|
-
const HELPER_AGENT = "ocra-helper";
|
|
17
|
-
// Must not be empty: OpenCode falls back to its full coding prompt otherwise.
|
|
18
|
-
const REVIEW_AGENT_PROMPT = "You are a code review agent run by ocra. Follow the review instructions below.";
|
|
19
|
-
const HELPER_AGENT_PROMPT = "You answer exactly as the instructions below ask, with no tools.";
|
|
20
|
-
// The helper answers in one step and has no tools. It still needs two:
|
|
21
|
-
// OpenCode appends an assistant message on an agent's last allowed step, and
|
|
22
|
-
// Gemini rejects a request that ends with a model turn (#66).
|
|
23
|
-
export const HELPER_AGENT_STEPS = 2;
|
|
24
16
|
// Sent once to a review agent that stopped before finishing (session-prompt.ts).
|
|
25
|
-
|
|
17
|
+
const REVIEW_RESUME = {
|
|
26
18
|
doneTool: REVIEW_TOOLS.taskDone,
|
|
27
19
|
maxSteps: MAX_AGENT_STEPS,
|
|
28
20
|
message: RESUME_MESSAGE,
|
|
29
21
|
};
|
|
30
|
-
// Every OpenCode built-in tool of the pinned version; a test fails when an
|
|
31
|
-
// upgrade adds one, so a new write-capable tool can never be enabled silently.
|
|
32
|
-
export const OPENCODE_BUILTIN_TOOLS = [
|
|
33
|
-
"invalid",
|
|
34
|
-
"question",
|
|
35
|
-
"bash",
|
|
36
|
-
"read",
|
|
37
|
-
"glob",
|
|
38
|
-
"grep",
|
|
39
|
-
"edit",
|
|
40
|
-
"write",
|
|
41
|
-
"task",
|
|
42
|
-
"webfetch",
|
|
43
|
-
"todowrite",
|
|
44
|
-
"websearch",
|
|
45
|
-
"skill",
|
|
46
|
-
"apply_patch",
|
|
47
|
-
];
|
|
48
|
-
const DISABLED_BUILTINS = Object.fromEntries(OPENCODE_BUILTIN_TOOLS.map((t) => [t, false]));
|
|
49
22
|
export class OpenCodeRuntime {
|
|
50
|
-
options;
|
|
51
23
|
name = "opencode";
|
|
52
24
|
sampling;
|
|
53
25
|
infra;
|
|
54
26
|
context;
|
|
55
|
-
|
|
27
|
+
chains;
|
|
56
28
|
helperTools;
|
|
29
|
+
applied;
|
|
30
|
+
options;
|
|
57
31
|
constructor(options) {
|
|
58
32
|
this.options = options;
|
|
59
33
|
const mcpTools = [...reviewTools, ...options.tools].map((t) => [
|
|
@@ -62,61 +36,28 @@ export class OpenCodeRuntime {
|
|
|
62
36
|
]);
|
|
63
37
|
this.helperTools = { ...DISABLED_BUILTINS, ...Object.fromEntries(mcpTools) };
|
|
64
38
|
this.sampling = openCodeSampling(options.sampling);
|
|
39
|
+
this.applied = new AppliedEfforts(options.sampling);
|
|
40
|
+
this.chains = new ChainRunner(options.models, {
|
|
41
|
+
refuse: (chain, own) => this.unprepared(chain, own),
|
|
42
|
+
ready: async () => {
|
|
43
|
+
await this.start();
|
|
44
|
+
},
|
|
45
|
+
task: (model, spec, signal, onUsage) => this.attemptTask(model, spec, signal, onUsage),
|
|
46
|
+
complete: (model, request, signal) => this.attemptCompletion(model, request, signal),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
appliedTo(agent) {
|
|
50
|
+
return this.applied.appliedTo(agent);
|
|
65
51
|
}
|
|
66
52
|
async *runTask(spec, signal) {
|
|
67
53
|
if (this.context && this.context !== spec.context) {
|
|
68
54
|
throw new OcraError("INTERNAL", "An OpenCodeRuntime instance serves a single review run");
|
|
69
55
|
}
|
|
70
56
|
this.context = spec.context;
|
|
71
|
-
|
|
72
|
-
if (chain.length === 0) {
|
|
73
|
-
yield {
|
|
74
|
-
type: "error",
|
|
75
|
-
taskId: spec.taskId,
|
|
76
|
-
retryable: false,
|
|
77
|
-
error: noModel(spec.modelTier),
|
|
78
|
-
};
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const infra = await this.start();
|
|
82
|
-
yield* withFailback({
|
|
83
|
-
taskId: spec.taskId,
|
|
84
|
-
tier: spec.modelTier,
|
|
85
|
-
chain,
|
|
86
|
-
health: this.health,
|
|
87
|
-
signal,
|
|
88
|
-
attempt: (model, onUsage) => this.prompt(infra, {
|
|
89
|
-
title: `ocra ${spec.taskId}`,
|
|
90
|
-
agent: REVIEW_AGENT,
|
|
91
|
-
model,
|
|
92
|
-
system: spec.systemPrompt,
|
|
93
|
-
user: spec.userPrompt,
|
|
94
|
-
tools: DISABLED_BUILTINS,
|
|
95
|
-
toolPrefix: `${MCP_SERVER}_`,
|
|
96
|
-
resume: REVIEW_RESUME,
|
|
97
|
-
}, signal, onUsage),
|
|
98
|
-
});
|
|
57
|
+
yield* this.chains.runTask(spec, signal);
|
|
99
58
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (chain.length === 0)
|
|
103
|
-
throw new OcraError("CONFIG_INVALID", noModel(request.tier));
|
|
104
|
-
const infra = await this.start();
|
|
105
|
-
return completeWithFailback({
|
|
106
|
-
tier: request.tier,
|
|
107
|
-
chain,
|
|
108
|
-
health: this.health,
|
|
109
|
-
signal,
|
|
110
|
-
attempt: (model) => this.prompt(infra, {
|
|
111
|
-
title: "ocra helper",
|
|
112
|
-
agent: HELPER_AGENT,
|
|
113
|
-
model,
|
|
114
|
-
system: request.system,
|
|
115
|
-
user: request.user,
|
|
116
|
-
tools: this.helperTools,
|
|
117
|
-
toolPrefix: `${MCP_SERVER}_`,
|
|
118
|
-
}, signal),
|
|
119
|
-
});
|
|
59
|
+
complete(request, signal) {
|
|
60
|
+
return this.chains.complete(request, signal);
|
|
120
61
|
}
|
|
121
62
|
async dispose() {
|
|
122
63
|
const infra = await this.infra?.catch(() => undefined);
|
|
@@ -127,6 +68,45 @@ export class OpenCodeRuntime {
|
|
|
127
68
|
await Promise.allSettled([infra.server.close(), infra.tools.close(), infra.dispatcher.close()]);
|
|
128
69
|
await rm(infra.root, { recursive: true, force: true });
|
|
129
70
|
}
|
|
71
|
+
async attemptTask(model, spec, signal, onUsage) {
|
|
72
|
+
const infra = await this.start();
|
|
73
|
+
return this.prompt(infra, {
|
|
74
|
+
title: `ocra ${spec.taskId}`,
|
|
75
|
+
...this.effortCall(infra, REVIEW_AGENT, spec.reviewer, spec.effort, model),
|
|
76
|
+
model,
|
|
77
|
+
system: spec.systemPrompt,
|
|
78
|
+
user: spec.userPrompt,
|
|
79
|
+
tools: DISABLED_BUILTINS,
|
|
80
|
+
toolPrefix: `${MCP_SERVER}_`,
|
|
81
|
+
resume: REVIEW_RESUME,
|
|
82
|
+
}, signal, onUsage);
|
|
83
|
+
}
|
|
84
|
+
async attemptCompletion(model, request, signal) {
|
|
85
|
+
const infra = await this.start();
|
|
86
|
+
return this.prompt(infra, {
|
|
87
|
+
title: "ocra helper",
|
|
88
|
+
...this.effortCall(infra, HELPER_AGENT, request.agent ?? request.tier, request.effort, model),
|
|
89
|
+
model,
|
|
90
|
+
system: request.system,
|
|
91
|
+
user: request.user,
|
|
92
|
+
tools: this.helperTools,
|
|
93
|
+
toolPrefix: `${MCP_SERVER}_`,
|
|
94
|
+
}, signal);
|
|
95
|
+
}
|
|
96
|
+
// The OpenCode agent and variant one attempt runs with, recorded for the
|
|
97
|
+
// agent's provenance.
|
|
98
|
+
effortCall(infra, base, agent, effort, model) {
|
|
99
|
+
if (effort === undefined)
|
|
100
|
+
return { agent: base };
|
|
101
|
+
const route = infra.efforts.route(model, effort);
|
|
102
|
+
const keepsSampling = this.applied.record(agent, effort, model, route !== undefined);
|
|
103
|
+
return {
|
|
104
|
+
agent: keepsSampling || this.options.sampling?.temperature === undefined
|
|
105
|
+
? base
|
|
106
|
+
: `${base}${WITHOUT_SAMPLING}`,
|
|
107
|
+
...(route?.variant ? { variant: route.variant } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
130
110
|
prompt(infra, input, signal, onUsage) {
|
|
131
111
|
return promptSession(infra.client.session, input, `${MCP_SERVER}_${REVIEW_TOOLS.reportFinding}`, signal, onUsage ? { onUsage } : {}).then((outcome) => outcome.error
|
|
132
112
|
? {
|
|
@@ -138,13 +118,25 @@ export class OpenCodeRuntime {
|
|
|
138
118
|
}
|
|
139
119
|
: outcome);
|
|
140
120
|
}
|
|
121
|
+
// The server holds the credentials of the providers it started with; an
|
|
122
|
+
// agent's own chain naming another would fail at OpenCode as "model not
|
|
123
|
+
// found". The tier chains are always among them.
|
|
124
|
+
unprepared(chain, own) {
|
|
125
|
+
if (!own)
|
|
126
|
+
return undefined;
|
|
127
|
+
const prepared = new Set(providersOf(this.options));
|
|
128
|
+
const model = chain.find((m) => !prepared.has(parseModel(m).providerID));
|
|
129
|
+
return model === undefined
|
|
130
|
+
? undefined
|
|
131
|
+
: new OcraError("CONFIG_INVALID", `"${model}" is not among the models the runtime was started with (RuntimeOptions.models or agentModels)`);
|
|
132
|
+
}
|
|
141
133
|
start() {
|
|
142
134
|
this.infra ??= this.launch();
|
|
143
135
|
return this.infra;
|
|
144
136
|
}
|
|
145
137
|
async launch() {
|
|
146
138
|
const custom = this.options.providers ?? {};
|
|
147
|
-
const missing = missingCredentials(this.options.env, providersOf(this.options
|
|
139
|
+
const missing = missingCredentials(this.options.env, providersOf(this.options), custom);
|
|
148
140
|
if (missing.length > 0)
|
|
149
141
|
throw new OcraError("CONFIG_CREDENTIALS_MISSING", missing.join("; "));
|
|
150
142
|
const root = await mkdtemp(join(tmpdir(), "ocra-opencode-"));
|
|
@@ -154,24 +146,26 @@ export class OpenCodeRuntime {
|
|
|
154
146
|
state: join(root, "state"),
|
|
155
147
|
};
|
|
156
148
|
const workspace = join(root, "workspace");
|
|
157
|
-
|
|
149
|
+
const probe = join(root, "probe");
|
|
150
|
+
await Promise.all([...Object.values(dirs), workspace, probe].map((d) => mkdir(d, { recursive: true })));
|
|
158
151
|
const tools = await startToolServer([...reviewTools, ...this.options.tools], () => this.context);
|
|
159
152
|
let started;
|
|
160
153
|
try {
|
|
161
154
|
const server = await startOpencodeServer({
|
|
162
155
|
binary: this.options.binary ?? resolveOpencodeBinary(this.options.env),
|
|
163
156
|
cwd: workspace,
|
|
164
|
-
env: serverEnv(this.options.env, dirs, providersOf(this.options
|
|
157
|
+
env: serverEnv(this.options.env, dirs, providersOf(this.options), custom),
|
|
165
158
|
config: openCodeConfig(tools, this.helperTools, custom, this.options.sampling),
|
|
166
159
|
});
|
|
167
160
|
started = server;
|
|
168
161
|
const dispatcher = createUntimedDispatcher();
|
|
169
|
-
const
|
|
162
|
+
const clientFor = (directory) => createOpencodeClient({
|
|
170
163
|
baseUrl: server.url,
|
|
171
|
-
directory
|
|
164
|
+
directory,
|
|
172
165
|
headers: { Authorization: server.authorization },
|
|
173
166
|
fetch: untimedFetch(dispatcher),
|
|
174
167
|
});
|
|
168
|
+
const client = clientFor(workspace);
|
|
175
169
|
// A second Ctrl-C exits at once, before dispose() can run: stop
|
|
176
170
|
// OpenCode and remove its directory synchronously on the way out.
|
|
177
171
|
const onExit = () => {
|
|
@@ -179,8 +173,15 @@ export class OpenCodeRuntime {
|
|
|
179
173
|
rmSync(root, { recursive: true, force: true });
|
|
180
174
|
};
|
|
181
175
|
process.on("exit", onExit);
|
|
182
|
-
const
|
|
183
|
-
|
|
176
|
+
const efforts = await setUpEfforts({
|
|
177
|
+
models: chainModels(this.options),
|
|
178
|
+
custom,
|
|
179
|
+
probe: clientFor(probe).config,
|
|
180
|
+
workspace: client.config,
|
|
181
|
+
file: join(dirs.config, "opencode.json"),
|
|
182
|
+
});
|
|
183
|
+
const secrets = credentialValues(this.options.env, providersOf(this.options), custom);
|
|
184
|
+
return { root, tools, server, client, efforts, dispatcher, onExit, secrets };
|
|
184
185
|
}
|
|
185
186
|
catch (error) {
|
|
186
187
|
await Promise.allSettled([tools.close(), started?.close()]);
|
|
@@ -189,91 +190,13 @@ export class OpenCodeRuntime {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
function openCodeSampling(sampling = {}) {
|
|
197
|
-
return {
|
|
198
|
-
...(sampling.temperature === undefined ? {} : { temperature: sampling.temperature }),
|
|
199
|
-
...(sampling.seed === undefined ? {} : { notApplied: ["seed"] }),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
export function openCodeConfig(tools, helperTools, custom = {}, sampling = {}) {
|
|
203
|
-
const permission = { edit: "deny", bash: "deny", webfetch: "deny", skill: "deny" };
|
|
204
|
-
const { temperature } = sampling;
|
|
205
|
-
const agentSampling = temperature === undefined ? {} : { temperature };
|
|
206
|
-
return {
|
|
207
|
-
share: "disabled",
|
|
208
|
-
autoupdate: false,
|
|
209
|
-
...(Object.keys(custom).length > 0
|
|
210
|
-
? { provider: providerConfig(custom, temperature !== undefined) }
|
|
211
|
-
: {}),
|
|
212
|
-
mcp: {
|
|
213
|
-
[MCP_SERVER]: {
|
|
214
|
-
type: "remote",
|
|
215
|
-
url: tools.url,
|
|
216
|
-
headers: tools.headers,
|
|
217
|
-
oauth: false,
|
|
218
|
-
enabled: true,
|
|
219
|
-
},
|
|
220
|
-
},
|
|
221
|
-
agent: {
|
|
222
|
-
[REVIEW_AGENT]: {
|
|
223
|
-
mode: "primary",
|
|
224
|
-
prompt: REVIEW_AGENT_PROMPT,
|
|
225
|
-
steps: MAX_AGENT_STEPS,
|
|
226
|
-
tools: DISABLED_BUILTINS,
|
|
227
|
-
permission,
|
|
228
|
-
...agentSampling,
|
|
229
|
-
},
|
|
230
|
-
[HELPER_AGENT]: {
|
|
231
|
-
mode: "primary",
|
|
232
|
-
prompt: HELPER_AGENT_PROMPT,
|
|
233
|
-
steps: HELPER_AGENT_STEPS,
|
|
234
|
-
tools: helperTools,
|
|
235
|
-
permission,
|
|
236
|
-
...agentSampling,
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
// OpenCode's form of a provider declared in configuration. OpenCode bundles
|
|
242
|
-
// the OpenAI-compatible client, so nothing is installed to reach it
|
|
243
|
-
// (docs/spikes/0002). The key stays in the environment: OpenCode reads
|
|
244
|
-
// {env:NAME} itself, and the configuration, which OpenCode may log, never
|
|
245
|
-
// holds it. Prices are per million tokens, as OpenCode's catalog has them.
|
|
246
|
-
// OpenCode assumes a declared model takes no temperature and drops one;
|
|
247
|
-
// with a temperature configured, the models are marked as taking it.
|
|
248
|
-
function providerConfig(custom, temperature) {
|
|
249
|
-
return Object.fromEntries(Object.entries(custom).map(([id, provider]) => [
|
|
250
|
-
id,
|
|
251
|
-
{
|
|
252
|
-
npm: "@ai-sdk/openai-compatible",
|
|
253
|
-
name: id,
|
|
254
|
-
options: {
|
|
255
|
-
baseURL: provider.baseUrl,
|
|
256
|
-
...(provider.apiKeyEnv ? { apiKey: `{env:${provider.apiKeyEnv}}` } : {}),
|
|
257
|
-
},
|
|
258
|
-
models: Object.fromEntries(Object.entries(provider.models).map(([model, price]) => [
|
|
259
|
-
model,
|
|
260
|
-
{
|
|
261
|
-
name: model,
|
|
262
|
-
...(temperature ? { temperature: true } : {}),
|
|
263
|
-
cost: {
|
|
264
|
-
input: price.input,
|
|
265
|
-
output: price.output,
|
|
266
|
-
...(price.cachedInput === undefined ? {} : { cache_read: price.cachedInput }),
|
|
267
|
-
},
|
|
268
|
-
},
|
|
269
|
-
])),
|
|
270
|
-
},
|
|
271
|
-
]));
|
|
272
|
-
}
|
|
273
|
-
function providersOf(models) {
|
|
274
|
-
return Object.values(models).flatMap((chain) => (chain ?? []).map((m) => parseModel(m).providerID));
|
|
193
|
+
// Every model a call may run on: the tiers' chains and the agents' own
|
|
194
|
+
// (ADR-0025).
|
|
195
|
+
function chainModels(options) {
|
|
196
|
+
return [...Object.values(options.models), ...Object.values(options.agentModels ?? {})].flatMap((chain) => [...(chain ?? [])]);
|
|
275
197
|
}
|
|
276
|
-
|
|
277
|
-
|
|
198
|
+
// The providers of those models, so each gets its credentials like a tier's.
|
|
199
|
+
export function providersOf(options) {
|
|
200
|
+
return chainModels(options).map((m) => parseModel(m).providerID);
|
|
278
201
|
}
|
|
279
202
|
//# sourceMappingURL=runtime.js.map
|
package/dist/server-env.d.ts
CHANGED
|
@@ -6,8 +6,6 @@ export interface IsolatedDirs {
|
|
|
6
6
|
}
|
|
7
7
|
type CustomProviders = Readonly<Record<string, CustomProvider>>;
|
|
8
8
|
export declare function missingCredentials(base: Env, providers: readonly string[], custom?: CustomProviders): string[];
|
|
9
|
-
export declare const EXTRA_ENV_VARIABLE = "OCRA_RUNTIME_ENV";
|
|
10
|
-
export declare function credentialNames(base: Env, providers: readonly string[], custom?: CustomProviders): string[];
|
|
11
9
|
export declare function credentialValues(base: Env, providers: readonly string[], custom?: CustomProviders): string[];
|
|
12
10
|
export declare function serverEnv(base: Env, dirs: IsolatedDirs, providers: readonly string[], custom?: CustomProviders): Record<string, string>;
|
|
13
11
|
export {};
|
package/dist/server-env.js
CHANGED
|
@@ -88,7 +88,7 @@ export function missingCredentials(base, providers, custom = {}) {
|
|
|
88
88
|
}
|
|
89
89
|
const GOOGLE_KEY = "GOOGLE_GENERATIVE_AI_API_KEY";
|
|
90
90
|
const GOOGLE_KEY_ALIASES = ["GEMINI_API_KEY", "GOOGLE_API_KEY"];
|
|
91
|
-
|
|
91
|
+
const EXTRA_ENV_VARIABLE = "OCRA_RUNTIME_ENV";
|
|
92
92
|
// OpenCode installs its plugin package from npm into every config directory
|
|
93
93
|
// when it starts, and the SDK of a provider it does not bundle when a model
|
|
94
94
|
// first uses it; no flag turns either off. ocra loads no OpenCode plugins and
|
|
@@ -121,7 +121,7 @@ const NEVER_BY_PREFIX = [
|
|
|
121
121
|
// a provider declared in configuration only the one it names, for a known
|
|
122
122
|
// catalog provider its variables, for any other every variable with its
|
|
123
123
|
// prefix that is not a platform token.
|
|
124
|
-
|
|
124
|
+
function credentialNames(base, providers, custom = {}) {
|
|
125
125
|
const names = [];
|
|
126
126
|
for (const provider of new Set(providers)) {
|
|
127
127
|
const declared = custom[provider];
|
|
@@ -1,36 +1,39 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
info: {
|
|
5
|
-
role:
|
|
6
|
-
cost
|
|
7
|
-
tokens
|
|
8
|
-
input
|
|
9
|
-
output
|
|
10
|
-
reasoning
|
|
11
|
-
cache
|
|
12
|
-
read
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
error
|
|
16
|
-
name
|
|
17
|
-
data
|
|
18
|
-
message
|
|
19
|
-
statusCode
|
|
20
|
-
isRetryable
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
parts: {
|
|
25
|
-
type:
|
|
26
|
-
text
|
|
27
|
-
tool
|
|
28
|
-
state
|
|
29
|
-
status
|
|
30
|
-
input
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
import { type AttemptOutcome, type Usage } from "@open-cr-agent/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
declare const sessionMessageSchema: z.ZodObject<{
|
|
4
|
+
info: z.ZodObject<{
|
|
5
|
+
role: z.ZodString;
|
|
6
|
+
cost: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
tokens: z.ZodOptional<z.ZodObject<{
|
|
8
|
+
input: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
output: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
reasoning: z.ZodOptional<z.ZodNumber>;
|
|
11
|
+
cache: z.ZodOptional<z.ZodObject<{
|
|
12
|
+
read: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
}, z.core.$strip>>;
|
|
14
|
+
}, z.core.$strip>>;
|
|
15
|
+
error: z.ZodOptional<z.ZodObject<{
|
|
16
|
+
name: z.ZodOptional<z.ZodString>;
|
|
17
|
+
data: z.ZodOptional<z.ZodObject<{
|
|
18
|
+
message: z.ZodOptional<z.ZodString>;
|
|
19
|
+
statusCode: z.ZodOptional<z.ZodNumber>;
|
|
20
|
+
isRetryable: z.ZodOptional<z.ZodBoolean>;
|
|
21
|
+
}, z.core.$strip>>;
|
|
22
|
+
}, z.core.$strip>>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
parts: z.ZodArray<z.ZodObject<{
|
|
25
|
+
type: z.ZodString;
|
|
26
|
+
text: z.ZodOptional<z.ZodString>;
|
|
27
|
+
tool: z.ZodOptional<z.ZodString>;
|
|
28
|
+
state: z.ZodOptional<z.ZodObject<{
|
|
29
|
+
status: z.ZodOptional<z.ZodString>;
|
|
30
|
+
input: z.ZodOptional<z.ZodUnknown>;
|
|
31
|
+
}, z.core.$strip>>;
|
|
32
|
+
}, z.core.$strip>>;
|
|
33
|
+
}, z.core.$strip>;
|
|
34
|
+
export type SessionMessage = z.output<typeof sessionMessageSchema>;
|
|
35
|
+
export declare function parseSessionMessages(data: unknown): SessionMessage[];
|
|
34
36
|
export declare function summarizeSession(messages: readonly SessionMessage[], reportTool: string, toolPrefix?: string): AttemptOutcome;
|
|
35
37
|
export declare function sessionUsage(messages: readonly SessionMessage[]): Usage;
|
|
38
|
+
export {};
|
|
36
39
|
//# sourceMappingURL=session-outcome.d.ts.map
|
package/dist/session-outcome.js
CHANGED
|
@@ -1,4 +1,44 @@
|
|
|
1
|
-
import { parseQuotaError } from "@open-cr-agent/core
|
|
1
|
+
import { parseQuotaError } from "@open-cr-agent/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// The part of OpenCode's session messages ocra reads, validated rather than
|
|
4
|
+
// cast: the SDK's types are not checked at runtime, and a server of another
|
|
5
|
+
// version could answer in another shape. Unknown fields are dropped.
|
|
6
|
+
const sessionMessageSchema = z.object({
|
|
7
|
+
info: z.object({
|
|
8
|
+
role: z.string(),
|
|
9
|
+
cost: z.number().optional(),
|
|
10
|
+
tokens: z
|
|
11
|
+
.object({
|
|
12
|
+
input: z.number().optional(),
|
|
13
|
+
output: z.number().optional(),
|
|
14
|
+
reasoning: z.number().optional(),
|
|
15
|
+
cache: z.object({ read: z.number().optional() }).optional(),
|
|
16
|
+
})
|
|
17
|
+
.optional(),
|
|
18
|
+
error: z
|
|
19
|
+
.object({
|
|
20
|
+
name: z.string().optional(),
|
|
21
|
+
data: z
|
|
22
|
+
.object({
|
|
23
|
+
message: z.string().optional(),
|
|
24
|
+
statusCode: z.number().optional(),
|
|
25
|
+
isRetryable: z.boolean().optional(),
|
|
26
|
+
})
|
|
27
|
+
.optional(),
|
|
28
|
+
})
|
|
29
|
+
.optional(),
|
|
30
|
+
}),
|
|
31
|
+
parts: z.array(z.object({
|
|
32
|
+
type: z.string(),
|
|
33
|
+
text: z.string().optional(),
|
|
34
|
+
tool: z.string().optional(),
|
|
35
|
+
state: z.object({ status: z.string().optional(), input: z.unknown().optional() }).optional(),
|
|
36
|
+
})),
|
|
37
|
+
});
|
|
38
|
+
// Throws when the answer is not a list of messages ocra can read.
|
|
39
|
+
export function parseSessionMessages(data) {
|
|
40
|
+
return z.array(sessionMessageSchema).parse(data ?? []);
|
|
41
|
+
}
|
|
2
42
|
const AUTH_STATUS = new Set([401, 403]);
|
|
3
43
|
const AUTH_MESSAGE = /api key|unauthori[sz]ed|permission denied|forbidden/i;
|
|
4
44
|
// Tool names come back with the MCP server's prefix (MCP_SERVER in
|
package/dist/session-prompt.d.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import { type Usage } from "@open-cr-agent/core";
|
|
2
|
-
import { type AttemptOutcome } from "@open-cr-agent/core/internal";
|
|
1
|
+
import { type AttemptOutcome, type Usage } from "@open-cr-agent/core";
|
|
3
2
|
import type { OpencodeClient } from "@opencode-ai/sdk/v2";
|
|
4
|
-
import { type SessionMessage } from "./session-outcome.js";
|
|
5
|
-
export declare const HARVEST_TIMEOUT_MS = 5000;
|
|
6
|
-
export declare const INACTIVITY_MS: number;
|
|
7
|
-
export declare const ACTIVITY_POLL_MS = 10000;
|
|
8
3
|
export interface ActivityOptions {
|
|
9
4
|
inactivityMs?: number;
|
|
10
5
|
pollMs?: number;
|
|
@@ -14,21 +9,19 @@ export interface PromptInput {
|
|
|
14
9
|
title: string;
|
|
15
10
|
agent: string;
|
|
16
11
|
model: string;
|
|
12
|
+
variant?: string;
|
|
17
13
|
system: string;
|
|
18
14
|
user: string;
|
|
19
15
|
tools: Record<string, boolean>;
|
|
20
16
|
toolPrefix?: string;
|
|
21
17
|
resume?: ResumeOptions;
|
|
22
18
|
}
|
|
23
|
-
|
|
19
|
+
interface ResumeOptions {
|
|
24
20
|
doneTool: string;
|
|
25
21
|
maxSteps: number;
|
|
26
22
|
message: string;
|
|
27
23
|
}
|
|
28
|
-
export declare function stoppedEarly(outcome: AttemptOutcome, resume: ResumeOptions): boolean;
|
|
29
24
|
type SessionApi = Pick<OpencodeClient["session"], "create" | "prompt" | "messages" | "abort">;
|
|
30
25
|
export declare function promptSession(session: SessionApi, input: PromptInput, reportTool: string, signal: AbortSignal, activity?: ActivityOptions): Promise<AttemptOutcome>;
|
|
31
|
-
export declare function activitySignature(messages: readonly SessionMessage[]): string;
|
|
32
|
-
export declare function emptyOutcome(): AttemptOutcome;
|
|
33
26
|
export {};
|
|
34
27
|
//# sourceMappingURL=session-prompt.d.ts.map
|
package/dist/session-prompt.js
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import { OcraError } from "@open-cr-agent/core";
|
|
2
|
-
import {
|
|
3
|
-
import { sessionUsage, summarizeSession } from "./session-outcome.js";
|
|
1
|
+
import { emptyUsage, errorMessage, OcraError, parseModel, } from "@open-cr-agent/core";
|
|
2
|
+
import { parseSessionMessages, sessionUsage, summarizeSession, } from "./session-outcome.js";
|
|
4
3
|
// A session that was cut off has still spent tokens and may have reported
|
|
5
4
|
// findings; this bounds the one extra request that collects them.
|
|
6
|
-
|
|
5
|
+
const HARVEST_TIMEOUT_MS = 5_000;
|
|
7
6
|
// A prompt returns only when the agent is done, so silence on the request is
|
|
8
7
|
// normal; silence in the session is not. A session whose messages have not
|
|
9
8
|
// changed for this long (no new step, no streamed text, no tool progress) is
|
|
10
9
|
// stopped and the task moves to the next model instead of waiting for its
|
|
11
10
|
// full timeout. Generous, so a slow step that is still writing survives.
|
|
12
|
-
|
|
11
|
+
const INACTIVITY_MS = 5 * 60_000;
|
|
13
12
|
// Each poll also reports what the session has spent, and a run's spend limit
|
|
14
13
|
// stops running tasks on those reports: this bounds how far past the limit a
|
|
15
14
|
// task can get before it is stopped.
|
|
16
|
-
|
|
15
|
+
const ACTIVITY_POLL_MS = 10_000;
|
|
17
16
|
// About one review attempt in twelve on Gemini ended after a step or two
|
|
18
17
|
// with no text, no done tool and steps to spare (2026-09-28), and the task
|
|
19
18
|
// counted as completed with its files unread. Continuing the same session
|
|
20
19
|
// keeps what it read and is cheaper than starting over.
|
|
21
|
-
|
|
20
|
+
function stoppedEarly(outcome, resume) {
|
|
22
21
|
return (!outcome.toolCalls.includes(resume.doneTool) &&
|
|
23
22
|
outcome.steps < resume.maxSteps &&
|
|
24
23
|
outcome.text.trim() === "");
|
|
@@ -38,6 +37,7 @@ export async function promptSession(session, input, reportTool, signal, activity
|
|
|
38
37
|
sessionID,
|
|
39
38
|
agent: input.agent,
|
|
40
39
|
model: parseModel(input.model),
|
|
40
|
+
...(input.variant ? { variant: input.variant } : {}),
|
|
41
41
|
system: input.system,
|
|
42
42
|
tools: input.tools,
|
|
43
43
|
parts: [{ type: "text", text: input.user }],
|
|
@@ -53,7 +53,7 @@ export async function promptSession(session, input, reportTool, signal, activity
|
|
|
53
53
|
let outcome;
|
|
54
54
|
try {
|
|
55
55
|
const messages = await session.messages({ sessionID }, { signal: attempt });
|
|
56
|
-
outcome = summarizeSession((messages.data
|
|
56
|
+
outcome = summarizeSession(parseSessionMessages(messages.data), reportTool, input.toolPrefix);
|
|
57
57
|
}
|
|
58
58
|
catch (error) {
|
|
59
59
|
// The session finished; running it again on the next model would pay
|
|
@@ -72,6 +72,7 @@ export async function promptSession(session, input, reportTool, signal, activity
|
|
|
72
72
|
sessionID,
|
|
73
73
|
agent: input.agent,
|
|
74
74
|
model: parseModel(input.model),
|
|
75
|
+
...(input.variant ? { variant: input.variant } : {}),
|
|
75
76
|
system: input.system,
|
|
76
77
|
tools: input.tools,
|
|
77
78
|
parts: [{ type: "text", text: input.resume.message }],
|
|
@@ -120,7 +121,7 @@ function watchActivity(session, sessionID, options) {
|
|
|
120
121
|
polling = true;
|
|
121
122
|
try {
|
|
122
123
|
const messages = await session.messages({ sessionID }, { signal: AbortSignal.timeout(HARVEST_TIMEOUT_MS) });
|
|
123
|
-
const list = (messages.data
|
|
124
|
+
const list = parseSessionMessages(messages.data);
|
|
124
125
|
if (options.onUsage) {
|
|
125
126
|
const spent = sessionUsage(list);
|
|
126
127
|
if (spent.costUsd > reported.costUsd || spent.inputTokens > reported.inputTokens) {
|
|
@@ -148,7 +149,7 @@ function watchActivity(session, sessionID, options) {
|
|
|
148
149
|
return { signal: controller.signal, stop: () => clearInterval(timer) };
|
|
149
150
|
}
|
|
150
151
|
// What changes while an agent works: steps, parts, streamed text, tool states.
|
|
151
|
-
|
|
152
|
+
function activitySignature(messages) {
|
|
152
153
|
return messages
|
|
153
154
|
.map((m) => m.parts.map((p) => `${p.type}:${p.state?.status ?? ""}:${p.text?.length ?? 0}`).join(","))
|
|
154
155
|
.join("|");
|
|
@@ -156,13 +157,13 @@ export function activitySignature(messages) {
|
|
|
156
157
|
async function harvest(session, sessionID, reportTool, toolPrefix) {
|
|
157
158
|
try {
|
|
158
159
|
const messages = await session.messages({ sessionID }, { signal: AbortSignal.timeout(HARVEST_TIMEOUT_MS) });
|
|
159
|
-
return summarizeSession((messages.data
|
|
160
|
+
return summarizeSession(parseSessionMessages(messages.data), reportTool, toolPrefix);
|
|
160
161
|
}
|
|
161
162
|
catch {
|
|
162
163
|
return emptyOutcome();
|
|
163
164
|
}
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
+
function emptyOutcome() {
|
|
166
167
|
return { findings: [], steps: 0, toolCalls: [], text: "", usage: emptyUsage() };
|
|
167
168
|
}
|
|
168
169
|
//# sourceMappingURL=session-prompt.js.map
|
package/dist/tool-server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createServer } from "node:http";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
-
import { errorMessage } from "@open-cr-agent/core
|
|
6
|
+
import { errorMessage } from "@open-cr-agent/core";
|
|
7
7
|
// src/ and dist/ both sit one level below the package root.
|
|
8
8
|
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
9
9
|
export async function startToolServer(tools, context) {
|
package/dist/transport.js
CHANGED
|
@@ -8,6 +8,10 @@ import { Agent, fetch as undiciFetch } from "undici";
|
|
|
8
8
|
export function createUntimedDispatcher() {
|
|
9
9
|
return new Agent({ headersTimeout: 0, bodyTimeout: 0 });
|
|
10
10
|
}
|
|
11
|
+
// The adapter from undici's fetch to Node's is a deliberate copy of the one in
|
|
12
|
+
// core's proxiedFetch: a runtime builds on core's public API only (ADR-0006),
|
|
13
|
+
// and sharing it there would put undici's Dispatcher type in core's contract
|
|
14
|
+
// and pass a dispatcher from this package's undici to core's copy.
|
|
11
15
|
export function untimedFetch(dispatcher) {
|
|
12
16
|
return async (input, init) => {
|
|
13
17
|
const request = new Request(input, init);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-cr-agent/runtime-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "OpenCode-based agent runtime for Open-CR-Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"code-review",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
37
|
-
"@open-cr-agent/core": "0.
|
|
37
|
+
"@open-cr-agent/core": "0.6.0",
|
|
38
38
|
"@opencode-ai/sdk": "1.18.32",
|
|
39
39
|
"opencode-ai": "1.18.32",
|
|
40
40
|
"undici": "^8.11.2",
|