@agentex/agent 0.0.26 → 0.0.28
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/CHANGELOG.md +118 -0
- package/README.md +58 -7
- package/dist/derived.d.ts.map +1 -1
- package/dist/derived.js +99 -2
- package/dist/derived.js.map +1 -1
- package/dist/goals/controller.d.ts.map +1 -1
- package/dist/goals/controller.js +21 -0
- package/dist/goals/controller.js.map +1 -1
- package/dist/goals/normalize.d.ts +1 -0
- package/dist/goals/normalize.d.ts.map +1 -1
- package/dist/goals/normalize.js +5 -0
- package/dist/goals/normalize.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/providers/claude/attach.d.ts.map +1 -1
- package/dist/providers/claude/attach.js +50 -9
- package/dist/providers/claude/attach.js.map +1 -1
- package/dist/providers/claude/index.d.ts.map +1 -1
- package/dist/providers/claude/index.js +8 -0
- package/dist/providers/claude/index.js.map +1 -1
- package/dist/providers/claude/session.d.ts +2 -0
- package/dist/providers/claude/session.d.ts.map +1 -1
- package/dist/providers/claude/session.js +17 -10
- package/dist/providers/claude/session.js.map +1 -1
- package/dist/providers/codex/attach.d.ts.map +1 -1
- package/dist/providers/codex/attach.js +40 -3
- package/dist/providers/codex/attach.js.map +1 -1
- package/dist/providers/codex/index.d.ts.map +1 -1
- package/dist/providers/codex/index.js +8 -0
- package/dist/providers/codex/index.js.map +1 -1
- package/dist/providers/codex/session.d.ts.map +1 -1
- package/dist/providers/codex/session.js +6 -0
- package/dist/providers/codex/session.js.map +1 -1
- package/dist/providers/codex/transcript.d.ts +3 -1
- package/dist/providers/codex/transcript.d.ts.map +1 -1
- package/dist/providers/codex/transcript.js +3 -1
- package/dist/providers/codex/transcript.js.map +1 -1
- package/dist/providers/cursor/discovery.d.ts +6 -0
- package/dist/providers/cursor/discovery.d.ts.map +1 -0
- package/dist/providers/cursor/discovery.js +118 -0
- package/dist/providers/cursor/discovery.js.map +1 -0
- package/dist/providers/cursor/execute.d.ts.map +1 -1
- package/dist/providers/cursor/execute.js +83 -45
- package/dist/providers/cursor/execute.js.map +1 -1
- package/dist/providers/cursor/index.d.ts.map +1 -1
- package/dist/providers/cursor/index.js +17 -4
- package/dist/providers/cursor/index.js.map +1 -1
- package/dist/providers/cursor/parse.js +1 -1
- package/dist/providers/cursor/parse.js.map +1 -1
- package/dist/providers/cursor/probe.d.ts +3 -0
- package/dist/providers/cursor/probe.d.ts.map +1 -0
- package/dist/providers/cursor/probe.js +89 -0
- package/dist/providers/cursor/probe.js.map +1 -0
- package/dist/providers/cursor/runtime.d.ts +4 -0
- package/dist/providers/cursor/runtime.d.ts.map +1 -0
- package/dist/providers/cursor/runtime.js +12 -0
- package/dist/providers/cursor/runtime.js.map +1 -0
- package/dist/providers/cursor/session.d.ts +3 -0
- package/dist/providers/cursor/session.d.ts.map +1 -0
- package/dist/providers/cursor/session.js +14 -0
- package/dist/providers/cursor/session.js.map +1 -0
- package/dist/providers/opencode/client.d.ts +14 -0
- package/dist/providers/opencode/client.d.ts.map +1 -0
- package/dist/providers/opencode/client.js +40 -0
- package/dist/providers/opencode/client.js.map +1 -0
- package/dist/providers/opencode/discovery.d.ts +4 -0
- package/dist/providers/opencode/discovery.d.ts.map +1 -0
- package/dist/providers/opencode/discovery.js +68 -0
- package/dist/providers/opencode/discovery.js.map +1 -0
- package/dist/providers/opencode/execute.d.ts.map +1 -1
- package/dist/providers/opencode/execute.js +219 -164
- package/dist/providers/opencode/execute.js.map +1 -1
- package/dist/providers/opencode/history.d.ts +24 -0
- package/dist/providers/opencode/history.d.ts.map +1 -0
- package/dist/providers/opencode/history.js +242 -0
- package/dist/providers/opencode/history.js.map +1 -0
- package/dist/providers/opencode/http-session.d.ts.map +1 -1
- package/dist/providers/opencode/http-session.js +333 -49
- package/dist/providers/opencode/http-session.js.map +1 -1
- package/dist/providers/opencode/index.d.ts.map +1 -1
- package/dist/providers/opencode/index.js +26 -3
- package/dist/providers/opencode/index.js.map +1 -1
- package/dist/providers/opencode/manager.d.ts +11 -0
- package/dist/providers/opencode/manager.d.ts.map +1 -0
- package/dist/providers/opencode/manager.js +240 -0
- package/dist/providers/opencode/manager.js.map +1 -0
- package/dist/providers/opencode/modes.d.ts +3 -0
- package/dist/providers/opencode/modes.d.ts.map +1 -0
- package/dist/providers/opencode/modes.js +30 -0
- package/dist/providers/opencode/modes.js.map +1 -0
- package/dist/providers/opencode/parse.d.ts.map +1 -1
- package/dist/providers/opencode/parse.js +2 -22
- package/dist/providers/opencode/parse.js.map +1 -1
- package/dist/providers/opencode/probe.d.ts +3 -0
- package/dist/providers/opencode/probe.d.ts.map +1 -0
- package/dist/providers/opencode/probe.js +107 -0
- package/dist/providers/opencode/probe.js.map +1 -0
- package/dist/providers/opencode/runtime.d.ts +8 -0
- package/dist/providers/opencode/runtime.d.ts.map +1 -0
- package/dist/providers/opencode/runtime.js +12 -0
- package/dist/providers/opencode/runtime.js.map +1 -0
- package/dist/providers/opencode/server.d.ts +18 -3
- package/dist/providers/opencode/server.d.ts.map +1 -1
- package/dist/providers/opencode/server.js +0 -0
- package/dist/providers/opencode/server.js.map +1 -1
- package/dist/providers/opencode/skill-config.d.ts +11 -0
- package/dist/providers/opencode/skill-config.d.ts.map +1 -0
- package/dist/providers/opencode/skill-config.js +57 -0
- package/dist/providers/opencode/skill-config.js.map +1 -0
- package/dist/sessions/exec-backed.d.ts +10 -0
- package/dist/sessions/exec-backed.d.ts.map +1 -0
- package/dist/sessions/exec-backed.js +141 -0
- package/dist/sessions/exec-backed.js.map +1 -0
- package/dist/sessions/history.d.ts +4 -0
- package/dist/sessions/history.d.ts.map +1 -0
- package/dist/sessions/history.js +59 -0
- package/dist/sessions/history.js.map +1 -0
- package/dist/sessions/index.d.ts +3 -0
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/index.js +2 -0
- package/dist/sessions/index.js.map +1 -1
- package/dist/types.d.ts +149 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +36 -6
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/binary.d.ts.map +1 -1
- package/dist/utils/binary.js +3 -0
- package/dist/utils/binary.js.map +1 -1
- package/dist/utils/endpoint.js +3 -3
- package/dist/utils/endpoint.js.map +1 -1
- package/package.json +4 -2
- package/src/derived.ts +126 -3
- package/src/goals/controller.ts +17 -0
- package/src/goals/normalize.ts +5 -0
- package/src/index.ts +13 -0
- package/src/providers/claude/attach.ts +48 -6
- package/src/providers/claude/index.ts +8 -0
- package/src/providers/claude/session.ts +15 -10
- package/src/providers/codex/attach.ts +48 -3
- package/src/providers/codex/index.ts +8 -0
- package/src/providers/codex/session.ts +5 -0
- package/src/providers/codex/transcript.ts +5 -1
- package/src/providers/cursor/discovery.ts +114 -0
- package/src/providers/cursor/execute.ts +67 -30
- package/src/providers/cursor/index.ts +19 -5
- package/src/providers/cursor/parse.ts +1 -1
- package/src/providers/cursor/probe.ts +90 -0
- package/src/providers/cursor/runtime.ts +11 -0
- package/src/providers/cursor/session.ts +15 -0
- package/src/providers/opencode/client.ts +44 -0
- package/src/providers/opencode/discovery.ts +69 -0
- package/src/providers/opencode/execute.ts +64 -12
- package/src/providers/opencode/history.ts +270 -0
- package/src/providers/opencode/http-session.ts +319 -50
- package/src/providers/opencode/index.ts +26 -3
- package/src/providers/opencode/manager.ts +269 -0
- package/src/providers/opencode/modes.ts +31 -0
- package/src/providers/opencode/parse.ts +2 -22
- package/src/providers/opencode/probe.ts +113 -0
- package/src/providers/opencode/runtime.ts +13 -0
- package/src/providers/opencode/server.ts +0 -0
- package/src/providers/opencode/skill-config.ts +64 -0
- package/src/sessions/exec-backed.ts +169 -0
- package/src/sessions/history.ts +76 -0
- package/src/sessions/index.ts +3 -0
- package/src/types.ts +160 -3
- package/src/utils/auth.ts +35 -6
- package/src/utils/binary.ts +3 -0
- package/src/utils/endpoint.ts +3 -3
|
@@ -12,13 +12,17 @@ import type {
|
|
|
12
12
|
SetGoalResult,
|
|
13
13
|
StreamEvent,
|
|
14
14
|
TurnResult,
|
|
15
|
+
UserInputResponse,
|
|
15
16
|
} from "../../types.js";
|
|
16
17
|
import { GoalController, EMULATED_GOAL_CAPABILITY } from "../../goals/index.js";
|
|
17
18
|
import { findBinary } from "../../utils/binary.js";
|
|
18
19
|
import { buildEnv, ensurePathInEnv } from "../../utils/env.js";
|
|
19
20
|
import { uuidv7 } from "../../utils/uuid.js";
|
|
20
21
|
import { acquireOpenCodeServer, type OpenCodeServerHandle } from "./server.js";
|
|
22
|
+
import type { OpenCodeClient } from "./client.js";
|
|
23
|
+
import { createSessionRecord } from "../../sessions/record.js";
|
|
21
24
|
import { opencodeSessionCodec } from "./codec.js";
|
|
25
|
+
import { prepareOpenCodeSkillConfig } from "./skill-config.js";
|
|
22
26
|
import {
|
|
23
27
|
assistantTextFromParts,
|
|
24
28
|
mapOpenCodePart,
|
|
@@ -63,7 +67,8 @@ class OpenCodeSession implements AgentSession {
|
|
|
63
67
|
private _state: SessionState = "idle";
|
|
64
68
|
private _sessionId: string | null = null;
|
|
65
69
|
private server: OpenCodeServerHandle | null = null;
|
|
66
|
-
private
|
|
70
|
+
private client: OpenCodeClient | null = null;
|
|
71
|
+
private cwd = "";
|
|
67
72
|
private readonly model: { providerID: string; modelID: string } | null;
|
|
68
73
|
|
|
69
74
|
private _turnActive = false;
|
|
@@ -79,6 +84,12 @@ class OpenCodeSession implements AgentSession {
|
|
|
79
84
|
private readonly _emittedToolCall = new Set<string>();
|
|
80
85
|
private readonly _emittedToolResult = new Set<string>();
|
|
81
86
|
private _sse: AbortController | null = null;
|
|
87
|
+
private readonly _handledInput = new Set<string>();
|
|
88
|
+
private readonly _inputInFlight = new Set<string>();
|
|
89
|
+
private readonly _inputReplies = new Map<string, { path: string; body?: Record<string, unknown> }>();
|
|
90
|
+
private readonly _inputAbort = new AbortController();
|
|
91
|
+
private _pendingPoll: ReturnType<typeof setInterval> | null = null;
|
|
92
|
+
private _skillConfigCleanup: (() => Promise<void>) | null = null;
|
|
82
93
|
|
|
83
94
|
/** Goal engine. OpenCode has no native goal surface — always emulated. */
|
|
84
95
|
private readonly _goals: GoalController;
|
|
@@ -106,12 +117,22 @@ class OpenCodeSession implements AgentSession {
|
|
|
106
117
|
async connect(): Promise<void> {
|
|
107
118
|
const config = this.ctx.config ?? {};
|
|
108
119
|
const resolved = await findBinary("opencode", config.command);
|
|
109
|
-
const
|
|
110
|
-
ensurePathInEnv(
|
|
120
|
+
const baseEnv = buildEnv(this.ctx.env);
|
|
121
|
+
ensurePathInEnv(baseEnv);
|
|
122
|
+
const skillConfig = await prepareOpenCodeSkillConfig(baseEnv, config.skillDirs);
|
|
123
|
+
const env = skillConfig.env;
|
|
124
|
+
this._skillConfigCleanup = skillConfig.cleanup;
|
|
111
125
|
const cwd = this.ctx.cwd ?? process.cwd();
|
|
126
|
+
this.cwd = cwd;
|
|
112
127
|
|
|
113
|
-
|
|
114
|
-
|
|
128
|
+
try {
|
|
129
|
+
this.server = await acquireOpenCodeServer(resolved.bin, resolved.prefixArgs, env, cwd);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
await this._skillConfigCleanup();
|
|
132
|
+
this._skillConfigCleanup = null;
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
this.client = this.server.client;
|
|
115
136
|
|
|
116
137
|
// Everything after acquiring the server must release it on failure, or the
|
|
117
138
|
// pooled `opencode serve` process leaks (refCount stuck at 1 forever).
|
|
@@ -120,7 +141,7 @@ class OpenCodeSession implements AgentSession {
|
|
|
120
141
|
if (resumeId) {
|
|
121
142
|
this._sessionId = resumeId;
|
|
122
143
|
} else {
|
|
123
|
-
const res = await
|
|
144
|
+
const res = await this.client.request("/session", {
|
|
124
145
|
method: "POST",
|
|
125
146
|
headers: { "content-type": "application/json" },
|
|
126
147
|
body: JSON.stringify({}),
|
|
@@ -132,7 +153,8 @@ class OpenCodeSession implements AgentSession {
|
|
|
132
153
|
|
|
133
154
|
// Live event stream (global; filtered to our session).
|
|
134
155
|
this._sse = new AbortController();
|
|
135
|
-
|
|
156
|
+
await this.startSse(this._sse.signal);
|
|
157
|
+
await this.reconcilePendingInput();
|
|
136
158
|
} catch (err) {
|
|
137
159
|
if (this._sse) {
|
|
138
160
|
try {
|
|
@@ -144,6 +166,8 @@ class OpenCodeSession implements AgentSession {
|
|
|
144
166
|
}
|
|
145
167
|
this.server.release();
|
|
146
168
|
this.server = null;
|
|
169
|
+
await this._skillConfigCleanup?.();
|
|
170
|
+
this._skillConfigCleanup = null;
|
|
147
171
|
this._state = "closed";
|
|
148
172
|
throw err;
|
|
149
173
|
}
|
|
@@ -158,9 +182,17 @@ class OpenCodeSession implements AgentSession {
|
|
|
158
182
|
// SSE live stream
|
|
159
183
|
// -------------------------------------------------------------------------
|
|
160
184
|
|
|
161
|
-
private async
|
|
185
|
+
private async startSse(signal: AbortSignal): Promise<void> {
|
|
186
|
+
if (!this.client) throw new Error("OpenCode client is unavailable");
|
|
187
|
+
const response = await this.client.request("/global/event", { signal });
|
|
188
|
+
if (!response.ok || !response.body) {
|
|
189
|
+
throw new Error(`opencode: failed to establish event stream (${response.status})`);
|
|
190
|
+
}
|
|
191
|
+
void this.readSse(response, signal);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async readSse(res: Response, signal: AbortSignal): Promise<void> {
|
|
162
195
|
try {
|
|
163
|
-
const res = await fetch(`${this.url}/global/event`, { signal });
|
|
164
196
|
if (!res.body) return;
|
|
165
197
|
const reader = res.body.getReader();
|
|
166
198
|
const decoder = new TextDecoder();
|
|
@@ -180,7 +212,17 @@ class OpenCodeSession implements AgentSession {
|
|
|
180
212
|
}
|
|
181
213
|
}
|
|
182
214
|
} catch {
|
|
183
|
-
//
|
|
215
|
+
// Reconciliation polling remains authoritative while a turn is active.
|
|
216
|
+
} finally {
|
|
217
|
+
if (!signal.aborted && this._state !== "closed") {
|
|
218
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
219
|
+
try {
|
|
220
|
+
await this.startSse(signal);
|
|
221
|
+
await this.reconcilePendingInput();
|
|
222
|
+
} catch {
|
|
223
|
+
// The active-turn polling path will keep pending input visible.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
184
226
|
}
|
|
185
227
|
}
|
|
186
228
|
|
|
@@ -192,13 +234,171 @@ class OpenCodeSession implements AgentSession {
|
|
|
192
234
|
return;
|
|
193
235
|
}
|
|
194
236
|
const payload = rec(rec(parsed)?.["payload"]);
|
|
195
|
-
if (!payload
|
|
237
|
+
if (!payload) return;
|
|
238
|
+
if (payload["type"] === "permission.asked" || payload["type"] === "permission.updated") {
|
|
239
|
+
const request = rec(rec(payload["properties"])?.["request"]) ?? rec(payload["properties"]);
|
|
240
|
+
if (request) await this.handlePermission(request);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (payload["type"] === "question.asked") {
|
|
244
|
+
const request = rec(rec(payload["properties"])?.["request"]) ?? rec(payload["properties"]);
|
|
245
|
+
if (request) await this.handleQuestion(request);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (payload["type"] !== "message.part.updated") return;
|
|
196
249
|
const props = rec(payload["properties"]);
|
|
197
250
|
const part = rec(props?.["part"]);
|
|
198
251
|
if (!part || str(part["sessionID"]) !== this._sessionId) return;
|
|
199
252
|
await this.emitPart(part);
|
|
200
253
|
}
|
|
201
254
|
|
|
255
|
+
private async reconcilePendingInput(): Promise<void> {
|
|
256
|
+
if (!this.client || !this._sessionId || this._state === "closed") return;
|
|
257
|
+
const [permissions, questions] = await Promise.all([
|
|
258
|
+
this.client.json<unknown[]>("/permission").catch(() => []),
|
|
259
|
+
this.client.json<unknown[]>("/question").catch(() => []),
|
|
260
|
+
]);
|
|
261
|
+
for (const value of permissions) {
|
|
262
|
+
const request = rec(value);
|
|
263
|
+
if (request && str(request["sessionID"]) === this._sessionId) await this.handlePermission(request);
|
|
264
|
+
}
|
|
265
|
+
for (const value of questions) {
|
|
266
|
+
const request = rec(value);
|
|
267
|
+
if (request && str(request["sessionID"]) === this._sessionId) await this.handleQuestion(request);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async hostInput(
|
|
272
|
+
request: Parameters<NonNullable<SessionContext["onUserInputRequest"]>>[0],
|
|
273
|
+
): Promise<UserInputResponse> {
|
|
274
|
+
const fallback: UserInputResponse = {
|
|
275
|
+
allow: (this.ctx.config?.unattendedPermissionPolicy ?? "allow") === "allow",
|
|
276
|
+
};
|
|
277
|
+
if (!this.ctx.onUserInputRequest) return fallback;
|
|
278
|
+
const timeoutSec = this.ctx.config?.inputRequestTimeoutSec ?? 300;
|
|
279
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
280
|
+
let abortListener: (() => void) | null = null;
|
|
281
|
+
try {
|
|
282
|
+
const timeout = new Promise<typeof fallback>((resolve) => {
|
|
283
|
+
timer = setTimeout(() => resolve(fallback), Math.max(0, timeoutSec) * 1000);
|
|
284
|
+
});
|
|
285
|
+
const closed = new Promise<typeof fallback>((resolve) => {
|
|
286
|
+
abortListener = () => resolve(fallback);
|
|
287
|
+
this._inputAbort.signal.addEventListener("abort", abortListener, { once: true });
|
|
288
|
+
});
|
|
289
|
+
return await Promise.race([
|
|
290
|
+
Promise.resolve(this.ctx.onUserInputRequest(request)).catch(() => fallback),
|
|
291
|
+
timeout,
|
|
292
|
+
closed,
|
|
293
|
+
]);
|
|
294
|
+
} finally {
|
|
295
|
+
if (timer) clearTimeout(timer);
|
|
296
|
+
if (abortListener) this._inputAbort.signal.removeEventListener("abort", abortListener);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private restoreAfterInput(): void {
|
|
301
|
+
if (this._state !== "closed") this._state = this._turnActive ? "thinking" : "idle";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
private async handlePermission(request: Record<string, unknown>): Promise<void> {
|
|
305
|
+
const id = str(request["id"]);
|
|
306
|
+
if (!id || this._handledInput.has(id) || this._inputInFlight.has(id)
|
|
307
|
+
|| str(request["sessionID"]) !== this._sessionId || !this.client) return;
|
|
308
|
+
this._inputInFlight.add(id);
|
|
309
|
+
try {
|
|
310
|
+
let reply = this._inputReplies.get(id);
|
|
311
|
+
if (!reply) {
|
|
312
|
+
this._state = "waiting_for_approval";
|
|
313
|
+
const permission = str(request["permission"]) ?? "Tool";
|
|
314
|
+
const response = await this.hostInput({
|
|
315
|
+
toolName: permission,
|
|
316
|
+
input: rec(request["metadata"]) ?? {},
|
|
317
|
+
toolUseId: id,
|
|
318
|
+
title: permission,
|
|
319
|
+
description: Array.isArray(request["patterns"])
|
|
320
|
+
? request["patterns"].filter((item): item is string => typeof item === "string").join(", ")
|
|
321
|
+
: undefined,
|
|
322
|
+
});
|
|
323
|
+
reply = {
|
|
324
|
+
path: `/permission/${encodeURIComponent(id)}/reply`,
|
|
325
|
+
body: {
|
|
326
|
+
reply: response.allow ? "once" : "reject",
|
|
327
|
+
...(response.message ? { message: response.message } : {}),
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
this._inputReplies.set(id, reply);
|
|
331
|
+
}
|
|
332
|
+
await this.client.ok(reply.path, {
|
|
333
|
+
method: "POST",
|
|
334
|
+
body: JSON.stringify(reply.body ?? {}),
|
|
335
|
+
});
|
|
336
|
+
this._handledInput.add(id);
|
|
337
|
+
this._inputReplies.delete(id);
|
|
338
|
+
} catch {
|
|
339
|
+
// Keep the cached host decision so reconciliation retries only the reply.
|
|
340
|
+
} finally {
|
|
341
|
+
this._inputInFlight.delete(id);
|
|
342
|
+
this.restoreAfterInput();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private async handleQuestion(request: Record<string, unknown>): Promise<void> {
|
|
347
|
+
const id = str(request["id"]);
|
|
348
|
+
if (!id || this._handledInput.has(id) || this._inputInFlight.has(id)
|
|
349
|
+
|| str(request["sessionID"]) !== this._sessionId || !this.client) return;
|
|
350
|
+
this._inputInFlight.add(id);
|
|
351
|
+
try {
|
|
352
|
+
let reply = this._inputReplies.get(id);
|
|
353
|
+
if (!reply) {
|
|
354
|
+
this._state = "waiting_for_input";
|
|
355
|
+
const questions = (Array.isArray(request["questions"]) ? request["questions"] : []).map((value) => {
|
|
356
|
+
const question = rec(value) ?? {};
|
|
357
|
+
return {
|
|
358
|
+
question: str(question["question"]) ?? str(question["header"]) ?? "Question",
|
|
359
|
+
header: str(question["header"]) ?? "Question",
|
|
360
|
+
options: (Array.isArray(question["options"]) ? question["options"] : []).map((raw) => {
|
|
361
|
+
const option = rec(raw) ?? {};
|
|
362
|
+
return {
|
|
363
|
+
label: str(option["label"]) ?? "",
|
|
364
|
+
description: str(option["description"]) ?? "",
|
|
365
|
+
};
|
|
366
|
+
}).filter((option) => option.label),
|
|
367
|
+
...(question["multiple"] === true ? { multiSelect: true } : {}),
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
const response = await this.hostInput({
|
|
371
|
+
toolName: "AskUserQuestion",
|
|
372
|
+
input: { questions },
|
|
373
|
+
toolUseId: id,
|
|
374
|
+
});
|
|
375
|
+
if (!response.allow) {
|
|
376
|
+
reply = { path: `/question/${encodeURIComponent(id)}/reject` };
|
|
377
|
+
} else {
|
|
378
|
+
const answerMap = rec(response.updatedInput?.["answers"]) ?? {};
|
|
379
|
+
const answers = questions.map((question) => {
|
|
380
|
+
const raw = answerMap[question.question] ?? answerMap[question.header];
|
|
381
|
+
if (Array.isArray(raw)) return raw.filter((item): item is string => typeof item === "string");
|
|
382
|
+
return typeof raw === "string" ? [raw] : [];
|
|
383
|
+
});
|
|
384
|
+
reply = { path: `/question/${encodeURIComponent(id)}/reply`, body: { answers } };
|
|
385
|
+
}
|
|
386
|
+
this._inputReplies.set(id, reply);
|
|
387
|
+
}
|
|
388
|
+
await this.client.ok(reply.path, {
|
|
389
|
+
method: "POST",
|
|
390
|
+
...(reply.body ? { body: JSON.stringify(reply.body) } : {}),
|
|
391
|
+
});
|
|
392
|
+
this._handledInput.add(id);
|
|
393
|
+
this._inputReplies.delete(id);
|
|
394
|
+
} catch {
|
|
395
|
+
// Keep the cached host decision so reconciliation retries only the reply.
|
|
396
|
+
} finally {
|
|
397
|
+
this._inputInFlight.delete(id);
|
|
398
|
+
this.restoreAfterInput();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
202
402
|
private async emitPart(part: Record<string, unknown>): Promise<void> {
|
|
203
403
|
// The SSE feed is global and async: drop stragglers that arrive while no turn
|
|
204
404
|
// is active so a finished turn's late parts don't surface against the next one.
|
|
@@ -233,7 +433,7 @@ class OpenCodeSession implements AgentSession {
|
|
|
233
433
|
}
|
|
234
434
|
}
|
|
235
435
|
|
|
236
|
-
private async emit(ev:
|
|
436
|
+
private async emit(ev: StreamEvent | null): Promise<void> {
|
|
237
437
|
if (ev && this.ctx.onEvent) {
|
|
238
438
|
try {
|
|
239
439
|
await this.ctx.onEvent(ev);
|
|
@@ -271,15 +471,18 @@ class OpenCodeSession implements AgentSession {
|
|
|
271
471
|
}
|
|
272
472
|
|
|
273
473
|
private async runTurn(message: string, options?: SendOptions): Promise<TurnResult> {
|
|
474
|
+
const startedAt = Date.now();
|
|
274
475
|
if (!this._sessionId) {
|
|
275
|
-
|
|
276
|
-
return {
|
|
476
|
+
const outcome: TurnResult = {
|
|
277
477
|
summary: null,
|
|
278
478
|
costUsd: null,
|
|
279
479
|
status: "failed",
|
|
280
480
|
errorCode: "not_initialized",
|
|
281
481
|
errorMessage: "OpenCode session is not initialized",
|
|
282
482
|
};
|
|
483
|
+
await this.emitTerminal(outcome, {}, startedAt);
|
|
484
|
+
this.finishTurn();
|
|
485
|
+
return outcome;
|
|
283
486
|
}
|
|
284
487
|
|
|
285
488
|
// Fresh per-turn live-stream dedup so a previous turn's part ids can't
|
|
@@ -298,63 +501,113 @@ class OpenCodeSession implements AgentSession {
|
|
|
298
501
|
|
|
299
502
|
const body: Record<string, unknown> = { parts: [{ type: "text", text: message }] };
|
|
300
503
|
if (this.model) body["model"] = this.model;
|
|
504
|
+
if (this.ctx.config?.modelVariant) body["variant"] = this.ctx.config.modelVariant;
|
|
505
|
+
const agent = this.ctx.config?.planMode ? "plan" : (this.ctx.config?.modeId ?? this.ctx.config?.mode);
|
|
506
|
+
if (agent) body["agent"] = agent;
|
|
301
507
|
|
|
508
|
+
let outcome: TurnResult;
|
|
509
|
+
let terminalRaw: Record<string, unknown> = {};
|
|
302
510
|
try {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
headers: { "content-type": "application/json" },
|
|
306
|
-
body: JSON.stringify(body),
|
|
307
|
-
signal: ac.signal,
|
|
308
|
-
});
|
|
309
|
-
if (!res.ok) {
|
|
310
|
-
const text = await res.text().catch(() => "");
|
|
311
|
-
return {
|
|
511
|
+
if (!this.server?.isCurrent()) {
|
|
512
|
+
outcome = {
|
|
312
513
|
summary: null,
|
|
313
514
|
costUsd: null,
|
|
314
515
|
status: "failed",
|
|
315
|
-
errorCode: "
|
|
316
|
-
errorMessage:
|
|
516
|
+
errorCode: "runtime_reconfigured",
|
|
517
|
+
errorMessage: "OpenCode runtime configuration changed. Start a fresh session.",
|
|
317
518
|
};
|
|
519
|
+
} else {
|
|
520
|
+
this._pendingPoll = setInterval(() => void this.reconcilePendingInput(), 750);
|
|
521
|
+
const res = await this.client!.request(`/session/${this._sessionId}/message`, {
|
|
522
|
+
method: "POST",
|
|
523
|
+
headers: { "content-type": "application/json" },
|
|
524
|
+
body: JSON.stringify(body),
|
|
525
|
+
signal: ac.signal,
|
|
526
|
+
});
|
|
527
|
+
if (!res.ok) {
|
|
528
|
+
outcome = {
|
|
529
|
+
summary: null,
|
|
530
|
+
costUsd: null,
|
|
531
|
+
status: "failed",
|
|
532
|
+
errorCode: "http_error",
|
|
533
|
+
errorMessage: `OpenCode message request failed (${res.status})`,
|
|
534
|
+
};
|
|
535
|
+
terminalRaw = { status: res.status };
|
|
536
|
+
} else {
|
|
537
|
+
const data = (await res.json()) as Record<string, unknown>;
|
|
538
|
+
terminalRaw = data;
|
|
539
|
+
const info = rec(data["info"]);
|
|
540
|
+
const status = turnStatusFromMessage(info);
|
|
541
|
+
const usage = usageFromMessage(info);
|
|
542
|
+
const summary = assistantTextFromParts(data["parts"]) || null;
|
|
543
|
+
const costUsd = info ? num(info["cost"]) : null;
|
|
544
|
+
const errorMessage = status === "failed" ? JSON.stringify(info?.["error"] ?? null) : null;
|
|
545
|
+
outcome = {
|
|
546
|
+
summary,
|
|
547
|
+
...(usage ? { usage } : {}),
|
|
548
|
+
costUsd,
|
|
549
|
+
status,
|
|
550
|
+
errorCode: status === "failed" ? "agent_error" : null,
|
|
551
|
+
errorMessage,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
318
554
|
}
|
|
319
|
-
const data = (await res.json()) as Record<string, unknown>;
|
|
320
|
-
const info = rec(data["info"]);
|
|
321
|
-
const status = turnStatusFromMessage(info);
|
|
322
|
-
const usage = usageFromMessage(info);
|
|
323
|
-
return {
|
|
324
|
-
summary: assistantTextFromParts(data["parts"]) || null,
|
|
325
|
-
...(usage ? { usage } : {}),
|
|
326
|
-
costUsd: info ? num(info["cost"]) : null,
|
|
327
|
-
status,
|
|
328
|
-
errorCode: status === "failed" ? "agent_error" : null,
|
|
329
|
-
errorMessage: status === "failed" ? JSON.stringify(info?.["error"] ?? null) : null,
|
|
330
|
-
};
|
|
331
555
|
} catch (err) {
|
|
332
556
|
if (ac.signal.aborted) {
|
|
333
|
-
|
|
334
|
-
// already killed the server); only interrupt on timeout/caller-abort.
|
|
335
|
-
const reason = ac.signal.reason === "timeout" ? "timeout" : ac.signal.reason === "closed" ? "aborted" : "aborted";
|
|
557
|
+
const reason = ac.signal.reason === "timeout" ? "timeout" : "aborted";
|
|
336
558
|
if (ac.signal.reason !== "closed") await this.interrupt().catch(() => {});
|
|
337
|
-
|
|
559
|
+
outcome = {
|
|
338
560
|
summary: null,
|
|
339
561
|
costUsd: null,
|
|
340
562
|
status: reason,
|
|
341
563
|
errorCode: reason,
|
|
342
564
|
errorMessage: reason === "timeout" ? "Turn exceeded its timeout" : "Turn aborted",
|
|
343
565
|
};
|
|
566
|
+
} else {
|
|
567
|
+
outcome = {
|
|
568
|
+
summary: null,
|
|
569
|
+
costUsd: null,
|
|
570
|
+
status: "failed",
|
|
571
|
+
errorCode: "request_error",
|
|
572
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
573
|
+
};
|
|
344
574
|
}
|
|
345
|
-
return {
|
|
346
|
-
summary: null,
|
|
347
|
-
costUsd: null,
|
|
348
|
-
status: "failed",
|
|
349
|
-
errorCode: "request_error",
|
|
350
|
-
errorMessage: err instanceof Error ? err.message : String(err),
|
|
351
|
-
};
|
|
352
575
|
} finally {
|
|
576
|
+
if (this._pendingPoll) clearInterval(this._pendingPoll);
|
|
577
|
+
this._pendingPoll = null;
|
|
353
578
|
if (timer) clearTimeout(timer);
|
|
354
579
|
options?.signal?.removeEventListener("abort", onAbort);
|
|
355
580
|
if (this._activeController === ac) this._activeController = null;
|
|
356
|
-
this.finishTurn();
|
|
357
581
|
}
|
|
582
|
+
await this.emitTerminal(outcome, terminalRaw, startedAt);
|
|
583
|
+
this.finishTurn();
|
|
584
|
+
return outcome;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
private async emitTerminal(
|
|
588
|
+
outcome: TurnResult,
|
|
589
|
+
raw: Record<string, unknown>,
|
|
590
|
+
startedAt: number,
|
|
591
|
+
): Promise<void> {
|
|
592
|
+
const info = rec(raw["info"]);
|
|
593
|
+
await this.emit({
|
|
594
|
+
type: "result",
|
|
595
|
+
text: outcome.summary ?? outcome.errorMessage ?? "",
|
|
596
|
+
costUsd: outcome.costUsd,
|
|
597
|
+
isError: outcome.status !== "completed",
|
|
598
|
+
stopReason: null,
|
|
599
|
+
terminalReason: outcome.status,
|
|
600
|
+
numTurns: 1,
|
|
601
|
+
durationMs: Date.now() - startedAt,
|
|
602
|
+
timestamp: new Date().toISOString(),
|
|
603
|
+
providerType: "opencode",
|
|
604
|
+
sessionId: this._sessionId,
|
|
605
|
+
messageId: info ? str(info["id"]) : null,
|
|
606
|
+
eventId: info ? str(info["id"]) : null,
|
|
607
|
+
turnId: null,
|
|
608
|
+
parentToolCallId: null,
|
|
609
|
+
raw,
|
|
610
|
+
});
|
|
358
611
|
}
|
|
359
612
|
|
|
360
613
|
async cancel(_uuid: string): Promise<CancelResult> {
|
|
@@ -381,7 +634,7 @@ class OpenCodeSession implements AgentSession {
|
|
|
381
634
|
async interrupt(): Promise<void> {
|
|
382
635
|
if (!this._sessionId) return;
|
|
383
636
|
try {
|
|
384
|
-
await
|
|
637
|
+
await this.client?.ok(`/session/${this._sessionId}/abort`, { method: "POST" });
|
|
385
638
|
} catch {
|
|
386
639
|
/* best effort */
|
|
387
640
|
}
|
|
@@ -401,6 +654,7 @@ class OpenCodeSession implements AgentSession {
|
|
|
401
654
|
|
|
402
655
|
async close(): Promise<void> {
|
|
403
656
|
this._state = "closed";
|
|
657
|
+
this._inputAbort.abort("closed");
|
|
404
658
|
// Abort an in-flight POST /message so close() doesn't leave it racing a
|
|
405
659
|
// killed server (and so the turn settles as aborted, not a confusing error).
|
|
406
660
|
if (this._activeController) {
|
|
@@ -419,9 +673,24 @@ class OpenCodeSession implements AgentSession {
|
|
|
419
673
|
}
|
|
420
674
|
this._sse = null;
|
|
421
675
|
}
|
|
676
|
+
if (this._pendingPoll) clearInterval(this._pendingPoll);
|
|
677
|
+
this._pendingPoll = null;
|
|
422
678
|
if (this.server) {
|
|
423
679
|
this.server.release();
|
|
424
680
|
this.server = null;
|
|
425
681
|
}
|
|
682
|
+
await this._skillConfigCleanup?.();
|
|
683
|
+
this._skillConfigCleanup = null;
|
|
684
|
+
this.client = null;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
describeHistory() {
|
|
688
|
+
if (!this._sessionId) return null;
|
|
689
|
+
return createSessionRecord({
|
|
690
|
+
providerType: "opencode",
|
|
691
|
+
params: { sessionId: this._sessionId, cwd: this.cwd },
|
|
692
|
+
cwd: this.cwd,
|
|
693
|
+
displayId: this._sessionId,
|
|
694
|
+
});
|
|
426
695
|
}
|
|
427
696
|
}
|
|
@@ -7,18 +7,28 @@ export const opencodeProvider: ProviderModule = {
|
|
|
7
7
|
type: "opencode",
|
|
8
8
|
capabilities: {
|
|
9
9
|
sessions: true,
|
|
10
|
-
modelDiscovery:
|
|
10
|
+
modelDiscovery: true,
|
|
11
11
|
quotaProbing: false,
|
|
12
12
|
mcp: false,
|
|
13
13
|
skills: true,
|
|
14
14
|
instructions: true,
|
|
15
15
|
workspace: true,
|
|
16
|
-
planMode:
|
|
16
|
+
planMode: true,
|
|
17
17
|
concurrentSend: false,
|
|
18
18
|
cancelQueuedMessage: false,
|
|
19
19
|
stopTask: false,
|
|
20
|
-
modes:
|
|
20
|
+
modes: true,
|
|
21
21
|
goals: EMULATED_GOAL_CAPABILITY,
|
|
22
|
+
durableHistory: true,
|
|
23
|
+
resume: true,
|
|
24
|
+
modelVariants: true,
|
|
25
|
+
permissionRequests: true,
|
|
26
|
+
questionRequests: true,
|
|
27
|
+
upstreamProviderDisconnect: true,
|
|
28
|
+
sessionModelChange: true,
|
|
29
|
+
sessionVariantChange: true,
|
|
30
|
+
sessionEffortChange: false,
|
|
31
|
+
sessionModeChange: true,
|
|
22
32
|
},
|
|
23
33
|
// One-shot `opencode run` for execute(); live HTTP/SSE sessions via the
|
|
24
34
|
// `opencode serve` daemon for createSession(). Both load lazily on first use.
|
|
@@ -27,4 +37,17 @@ export const opencodeProvider: ProviderModule = {
|
|
|
27
37
|
(await import("./http-session.js")).createOpenCodeSession(ctx),
|
|
28
38
|
resolveAuth: (ctx) => resolveAuthForProvider("opencode", ctx),
|
|
29
39
|
sessionCodec: opencodeSessionCodec,
|
|
40
|
+
listModels: (options) => import("./discovery.js").then((m) => m.listOpenCodeModels(options)),
|
|
41
|
+
listModes: (options) => import("./modes.js").then((m) => m.listOpenCodeModes(options)),
|
|
42
|
+
probeCapabilities: (ctx) => import("./probe.js").then((m) => m.probeOpenCodeCapabilities(ctx)),
|
|
43
|
+
upstreamProviders: {
|
|
44
|
+
list: (ctx) => import("./manager.js").then((m) => m.openCodeUpstreamProviders.list(ctx)),
|
|
45
|
+
authMethods: (providerId, ctx) => import("./manager.js").then((m) => m.openCodeUpstreamProviders.authMethods(providerId, ctx)),
|
|
46
|
+
setApiKey: (providerId, key, ctx) => import("./manager.js").then((m) => m.openCodeUpstreamProviders.setApiKey(providerId, key, ctx)),
|
|
47
|
+
beginOAuth: (providerId, methodId, inputs, ctx) => import("./manager.js").then((m) => m.openCodeUpstreamProviders.beginOAuth(providerId, methodId, inputs, ctx)),
|
|
48
|
+
completeOAuth: (flowId, code, ctx) => import("./manager.js").then((m) => m.openCodeUpstreamProviders.completeOAuth(flowId, code, ctx)),
|
|
49
|
+
canDisconnect: (providerId, ctx) => import("./manager.js").then((m) => m.openCodeUpstreamProviders.canDisconnect(providerId, ctx)),
|
|
50
|
+
disconnect: (providerId, ctx) => import("./manager.js").then((m) => m.openCodeUpstreamProviders.disconnect(providerId, ctx)),
|
|
51
|
+
},
|
|
52
|
+
attachHistory: (record, options) => import("./history.js").then((m) => m.attachOpenCodeHistory(record, options)),
|
|
30
53
|
};
|