@velum-labs/routekit-tool-opencode 0.18.6 → 1.0.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/driver.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { z } from "zod";
2
1
  import type { DriverContext, HarnessDriver, StartSessionOptions } from "@velum-labs/routekit-harness-core";
2
+ import { z } from "zod";
3
3
  export declare const opencodeDriverConfigSchema: z.ZodObject<{
4
4
  command: z.ZodDefault<z.ZodString>;
5
5
  serverUrl: z.ZodOptional<z.ZodString>;
package/dist/driver.js CHANGED
@@ -1,7 +1,7 @@
1
- import { z } from "zod";
2
1
  import { createOpencodeClient } from "@opencode-ai/sdk/client";
3
2
  import { createOpencodeServer } from "@opencode-ai/sdk/server";
4
- import { HarnessError, DEFAULT_AUTOMATION_APPROVAL_POLICY, asHarnessError, buildChildEnv, createCachedHarnessDriver, probeCliVersion, resolveDriverEnv } from "@velum-labs/routekit-harness-core";
3
+ import { asHarnessError, buildChildEnv, createCachedHarnessDriver, DEFAULT_AUTOMATION_APPROVAL_POLICY, HarnessError, nowIso, probeCliVersion, resolveDriverEnv, resumeStringField, SessionResourceRegistry, SingleFlightTurnController } from "@velum-labs/routekit-harness-core";
4
+ import { z } from "zod";
5
5
  import { opencodeProviderConfig } from "./launch.js";
6
6
  const RESUME_CURSOR_VERSION = 1;
7
7
  const DEFAULT_COMMAND = "opencode";
@@ -17,9 +17,6 @@ export const opencodeDriverConfigSchema = z.object({
17
17
  model: z.string().optional(),
18
18
  providerId: z.string().optional()
19
19
  });
20
- function nowIso() {
21
- return new Date().toISOString();
22
- }
23
20
  function itemTypeForTool(tool) {
24
21
  const lower = tool.toLowerCase();
25
22
  if (lower.includes("bash") || lower.includes("shell"))
@@ -40,6 +37,7 @@ class OpencodeSession {
40
37
  #reasoning;
41
38
  #sessionId;
42
39
  #stopped = false;
40
+ #turns = new SingleFlightTurnController();
43
41
  constructor(input) {
44
42
  this.#backend = input.backend;
45
43
  this.#sessionId = input.sessionId;
@@ -55,90 +53,119 @@ class OpencodeSession {
55
53
  async *sendTurn(input) {
56
54
  if (this.#stopped)
57
55
  throw new HarnessError("session_closed", "opencode session is stopped");
58
- const base = { kind: this.#kind, sessionId: this.#sessionId, at: nowIso() };
59
- const turnId = `${this.#sessionId}:turn:${Date.now()}`;
60
- // A function call so TS does not narrow `aborted` to a constant across the
61
- // await below (the signal can fire mid-turn).
62
- const isAborted = () => input.signal?.aborted === true;
63
- yield { ...base, type: "turn.started", turnId };
64
- if (isAborted()) {
65
- yield { ...base, type: "turn.completed", turnId, endReason: "aborted" };
66
- return;
67
- }
68
- let result;
69
- const reasoning = input.reasoning ?? this.#reasoning;
70
- if (reasoning !== undefined &&
71
- reasoning.mode !== "auto" &&
72
- reasoning.mode !== "effort") {
73
- throw new HarnessError("invalid_config", `OpenCode variants cannot represent reasoning mode "${reasoning.mode}"`);
74
- }
56
+ const turn = this.#turns.start(input.signal);
57
+ let settled = false;
75
58
  try {
76
- result = await this.#backend.prompt({
77
- sessionId: this.#sessionId,
78
- cwd: this.#cwd,
79
- prompt: input.prompt,
80
- ...(this.#model !== undefined ? { model: this.#model } : {}),
81
- ...(this.#providerId !== undefined ? { providerId: this.#providerId } : {}),
82
- ...(reasoning !== undefined ? { reasoning } : {}),
83
- ...(input.signal !== undefined ? { signal: input.signal } : {})
84
- });
85
- }
86
- catch (error) {
59
+ const base = { kind: this.#kind, sessionId: this.#sessionId, at: nowIso() };
60
+ const turnId = `${this.#sessionId}:turn:${Date.now()}`;
61
+ // A function call so TS does not narrow `aborted` to a constant across the
62
+ // await below (the signal can fire mid-turn).
63
+ const isAborted = () => turn.signal.aborted;
64
+ yield { ...base, type: "turn.started", turnId };
87
65
  if (isAborted()) {
88
66
  yield { ...base, type: "turn.completed", turnId, endReason: "aborted" };
89
67
  return;
90
68
  }
91
- const harnessError = asHarnessError(error);
92
- yield { ...base, type: "turn.failed", turnId, errorCode: harnessError.code, message: harnessError.message };
93
- return;
94
- }
95
- let usage;
96
- for (const part of result.parts) {
97
- const raw = { source: "opencode.sdk.part", method: part.type };
98
- switch (part.type) {
99
- case "text":
100
- if (part.text.length > 0) {
101
- yield { ...base, type: "content.delta", turnId, stream: "assistant_text", text: part.text, raw };
102
- }
103
- break;
104
- case "reasoning":
105
- if (part.text.length > 0) {
106
- yield { ...base, type: "content.delta", turnId, stream: "reasoning_text", text: part.text, raw };
107
- }
108
- break;
109
- case "tool":
110
- yield {
111
- ...base,
112
- type: "item.completed",
113
- turnId,
114
- itemId: part.callId,
115
- itemType: itemTypeForTool(part.tool),
116
- status: part.status === "failed" ? "failed" : "completed",
117
- raw
118
- };
119
- break;
120
- case "step-finish":
121
- if (part.tokens !== undefined) {
122
- usage = {
123
- inputTokens: part.tokens.input,
124
- outputTokens: part.tokens.output,
125
- reasoningOutputTokens: part.tokens.reasoning
69
+ let result;
70
+ const reasoning = input.reasoning ?? this.#reasoning;
71
+ if (reasoning !== undefined && reasoning.mode !== "auto" && reasoning.mode !== "effort") {
72
+ throw new HarnessError("invalid_config", `OpenCode variants cannot represent reasoning mode "${reasoning.mode}"`);
73
+ }
74
+ try {
75
+ result = await this.#backend.prompt({
76
+ sessionId: this.#sessionId,
77
+ cwd: this.#cwd,
78
+ prompt: input.prompt,
79
+ ...(this.#model !== undefined ? { model: this.#model } : {}),
80
+ ...(this.#providerId !== undefined ? { providerId: this.#providerId } : {}),
81
+ ...(reasoning !== undefined ? { reasoning } : {}),
82
+ signal: turn.signal
83
+ });
84
+ settled = true;
85
+ }
86
+ catch (error) {
87
+ settled = true;
88
+ if (isAborted()) {
89
+ yield { ...base, type: "turn.completed", turnId, endReason: "aborted" };
90
+ return;
91
+ }
92
+ const harnessError = asHarnessError(error);
93
+ yield {
94
+ ...base,
95
+ type: "turn.failed",
96
+ turnId,
97
+ errorCode: harnessError.code,
98
+ message: harnessError.message
99
+ };
100
+ return;
101
+ }
102
+ let usage;
103
+ for (const part of result.parts) {
104
+ const raw = { source: "opencode.sdk.part", method: part.type };
105
+ switch (part.type) {
106
+ case "text":
107
+ if (part.text.length > 0) {
108
+ yield {
109
+ ...base,
110
+ type: "content.delta",
111
+ turnId,
112
+ stream: "assistant_text",
113
+ text: part.text,
114
+ raw
115
+ };
116
+ }
117
+ break;
118
+ case "reasoning":
119
+ if (part.text.length > 0) {
120
+ yield {
121
+ ...base,
122
+ type: "content.delta",
123
+ turnId,
124
+ stream: "reasoning_text",
125
+ text: part.text,
126
+ raw
127
+ };
128
+ }
129
+ break;
130
+ case "tool":
131
+ yield {
132
+ ...base,
133
+ type: "item.completed",
134
+ turnId,
135
+ itemId: part.callId,
136
+ itemType: itemTypeForTool(part.tool),
137
+ status: part.status === "failed" ? "failed" : "completed",
138
+ raw
126
139
  };
140
+ break;
141
+ case "step-finish":
142
+ if (part.tokens !== undefined) {
143
+ usage = {
144
+ inputTokens: part.tokens.input,
145
+ outputTokens: part.tokens.output,
146
+ reasoningOutputTokens: part.tokens.reasoning
147
+ };
148
+ }
149
+ break;
150
+ default: {
151
+ const exhausted = part;
152
+ throw new Error(`unsupported opencode part: ${String(exhausted)}`);
127
153
  }
128
- break;
129
- default: {
130
- const exhausted = part;
131
- throw new Error(`unsupported opencode part: ${String(exhausted)}`);
132
154
  }
133
155
  }
156
+ yield {
157
+ ...base,
158
+ type: "turn.completed",
159
+ turnId,
160
+ endReason: "completed",
161
+ ...(usage !== undefined ? { usage } : {})
162
+ };
163
+ }
164
+ finally {
165
+ if (settled)
166
+ turn.complete();
167
+ turn.dispose();
134
168
  }
135
- yield {
136
- ...base,
137
- type: "turn.completed",
138
- turnId,
139
- endReason: "completed",
140
- ...(usage !== undefined ? { usage } : {})
141
- };
142
169
  }
143
170
  async respondToRequest() {
144
171
  // Unattended runs use an auto-approve policy applied at session
@@ -146,16 +173,26 @@ class OpencodeSession {
146
173
  throw new HarnessError("protocol_parse", "opencode driver does not surface interactive approval requests in buffered mode");
147
174
  }
148
175
  async interrupt() {
149
- await this.#backend.abort({ sessionId: this.#sessionId, cwd: this.#cwd }).catch(() => undefined);
176
+ this.#turns.interrupt();
177
+ await this.#backend
178
+ .abort({ sessionId: this.#sessionId, cwd: this.#cwd })
179
+ .catch(() => undefined);
150
180
  }
151
181
  resumeCursor() {
152
- return { version: RESUME_CURSOR_VERSION, kind: this.#kind, data: { sessionId: this.#sessionId } };
182
+ return {
183
+ version: RESUME_CURSOR_VERSION,
184
+ kind: this.#kind,
185
+ data: { sessionId: this.#sessionId }
186
+ };
153
187
  }
154
188
  async stop() {
155
189
  if (this.#stopped)
156
190
  return;
157
191
  this.#stopped = true;
158
- await this.#backend.abort({ sessionId: this.#sessionId, cwd: this.#cwd }).catch(() => undefined);
192
+ this.#turns.interrupt(new Error("opencode session stopped"));
193
+ await this.#backend
194
+ .abort({ sessionId: this.#sessionId, cwd: this.#cwd })
195
+ .catch(() => undefined);
159
196
  }
160
197
  // Kept for symmetry with other drivers; opencode's policy is applied at
161
198
  // session creation rather than per request.
@@ -164,10 +201,7 @@ class OpencodeSession {
164
201
  }
165
202
  }
166
203
  function resumeSessionId(resume) {
167
- if (resume === undefined || resume.kind !== "opencode")
168
- return undefined;
169
- const data = resume.data;
170
- return typeof data.sessionId === "string" ? data.sessionId : undefined;
204
+ return resumeStringField(resume, "opencode", "sessionId");
171
205
  }
172
206
  class OpencodeInstance {
173
207
  kind = "opencode";
@@ -176,7 +210,7 @@ class OpencodeInstance {
176
210
  #status;
177
211
  #backendFactory;
178
212
  #backend;
179
- #sessions = new Set();
213
+ #sessions = new SessionResourceRegistry();
180
214
  constructor(input) {
181
215
  this.#config = input.config;
182
216
  this.#context = input.context;
@@ -193,6 +227,7 @@ class OpencodeInstance {
193
227
  return this.#backend;
194
228
  }
195
229
  async startSession(options) {
230
+ this.#sessions.assertOpen();
196
231
  const backend = await this.#ensureBackend();
197
232
  const resume = resumeSessionId(options.resume);
198
233
  const created = await backend.createSession({
@@ -204,23 +239,16 @@ class OpencodeInstance {
204
239
  sessionId: created.sessionId,
205
240
  cwd: options.cwd,
206
241
  approvalPolicy: options.approvalPolicy ?? DEFAULT_AUTOMATION_APPROVAL_POLICY,
207
- ...(options.model ?? this.#config.model !== undefined
242
+ ...((options.model ?? this.#config.model !== undefined)
208
243
  ? { model: options.model ?? this.#config.model }
209
244
  : {}),
210
- ...(this.#config.providerId !== undefined
211
- ? { providerId: this.#config.providerId }
212
- : {}),
213
- ...(options.reasoning !== undefined
214
- ? { reasoning: options.reasoning }
215
- : {})
245
+ ...(this.#config.providerId !== undefined ? { providerId: this.#config.providerId } : {}),
246
+ ...(options.reasoning !== undefined ? { reasoning: options.reasoning } : {})
216
247
  });
217
- this.#sessions.add(session);
218
- return session;
248
+ return this.#sessions.manage(session);
219
249
  }
220
250
  async dispose() {
221
- for (const session of this.#sessions)
222
- await session.stop();
223
- this.#sessions.clear();
251
+ await this.#sessions.dispose();
224
252
  await this.#backend?.dispose().catch(() => undefined);
225
253
  this.#backend = undefined;
226
254
  }
@@ -258,7 +286,11 @@ const defaultBackendFactory = async (config, context) => {
258
286
  type: "tool",
259
287
  tool: String(part.tool ?? "tool"),
260
288
  callId: String(part.callID ?? part.id ?? "call"),
261
- status: state?.status === "error" ? "failed" : state?.status === "completed" ? "completed" : "running"
289
+ status: state?.status === "error"
290
+ ? "failed"
291
+ : state?.status === "completed"
292
+ ? "completed"
293
+ : "running"
262
294
  });
263
295
  }
264
296
  else if (part.type === "step-finish") {
@@ -288,9 +320,7 @@ const defaultBackendFactory = async (config, context) => {
288
320
  body: {
289
321
  parts: [{ type: "text", text: prompt }],
290
322
  ...modelBody,
291
- ...(reasoning?.mode === "effort"
292
- ? { variant: reasoning.effort }
293
- : {})
323
+ ...(reasoning?.mode === "effort" ? { variant: reasoning.effort } : {})
294
324
  },
295
325
  ...(signal !== undefined ? { signal } : {})
296
326
  });
package/dist/launch.js CHANGED
@@ -2,7 +2,8 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { reasoningEffortDescriptors } from "@velum-labs/routekit-contracts";
5
- import { spawnTool, trimTrailingSlashes } from "@velum-labs/routekit-runtime";
5
+ import { gatewayOpenAiBaseUrl } from "@velum-labs/routekit-runtime/network";
6
+ import { spawnTool } from "@velum-labs/routekit-runtime/process";
6
7
  const PROVIDER_ID = "routekit";
7
8
  export function opencodeModelArg(model) {
8
9
  return `${PROVIDER_ID}/${model}`;
@@ -40,7 +41,7 @@ export function opencodeProviderConfig(spec) {
40
41
  npm: "@ai-sdk/openai-compatible",
41
42
  name: "RouteKit gateway",
42
43
  options: {
43
- baseURL: `${trimTrailingSlashes(spec.gatewayUrl)}/v1`,
44
+ baseURL: gatewayOpenAiBaseUrl(spec.gatewayUrl),
44
45
  ...(spec.auth?.token !== undefined ? { apiKey: spec.auth.token } : {})
45
46
  },
46
47
  models
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-tool-opencode",
3
3
  "private": false,
4
- "version": "0.18.6",
4
+ "version": "1.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -28,10 +28,10 @@
28
28
  "dependencies": {
29
29
  "@opencode-ai/sdk": "1.17.13",
30
30
  "zod": "4.4.3",
31
- "@velum-labs/routekit-harness-core": "0.18.6",
32
- "@velum-labs/routekit-contracts": "0.18.6",
33
- "@velum-labs/routekit-runtime": "0.18.6",
34
- "@velum-labs/routekit-tools": "0.18.6"
31
+ "@velum-labs/routekit-contracts": "1.0.0",
32
+ "@velum-labs/routekit-harness-core": "1.0.0",
33
+ "@velum-labs/routekit-tools": "1.0.0",
34
+ "@velum-labs/routekit-runtime": "1.0.0"
35
35
  },
36
36
  "keywords": [
37
37
  "llm",
@@ -41,7 +41,7 @@
41
41
  "adapter"
42
42
  ],
43
43
  "scripts": {
44
- "build": "tsc -b",
44
+ "build": "tsc -p tsconfig.json",
45
45
  "clean": "tsc -b --clean",
46
46
  "test": "node --test \"dist/test/*.test.js\""
47
47
  }