@gitterm/sdk 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +588 -263
- package/dist/client.d.ts +39 -21
- package/dist/direct/client.d.ts +15 -14
- package/dist/direct/index.js +1377 -179
- package/dist/direct/runs.d.ts +19 -0
- package/dist/direct/types.d.ts +25 -23
- package/dist/errors.d.ts +9 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +375 -82
- package/dist/runs.d.ts +19 -0
- package/dist/types.d.ts +277 -23
- package/package.json +8 -4
package/dist/direct/index.js
CHANGED
|
@@ -1,6 +1,1238 @@
|
|
|
1
1
|
// src/direct/client.ts
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { createOpencodeClient as createOpencodeClient2 } from "@opencode-ai/sdk";
|
|
4
|
+
|
|
5
|
+
// ../agent-runtime/src/opencode.ts
|
|
3
6
|
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
7
|
+
|
|
8
|
+
// ../agent-runtime/src/types.ts
|
|
9
|
+
function parseModelRef(model) {
|
|
10
|
+
if (!model)
|
|
11
|
+
return;
|
|
12
|
+
const separator = model.indexOf("/");
|
|
13
|
+
if (separator <= 0 || separator === model.length - 1) {
|
|
14
|
+
throw new Error('model must use the "provider/model" format');
|
|
15
|
+
}
|
|
16
|
+
return { providerID: model.slice(0, separator), modelID: model.slice(separator + 1) };
|
|
17
|
+
}
|
|
18
|
+
function permissionTitle(permission, patterns) {
|
|
19
|
+
return patterns.length > 0 ? `${permission}: ${patterns.join(", ")}` : permission;
|
|
20
|
+
}
|
|
21
|
+
function sessionStatusOf(status) {
|
|
22
|
+
return status === "busy" ? "busy" : status === "retry" ? "retry" : "idle";
|
|
23
|
+
}
|
|
24
|
+
function missingSessionSnapshot() {
|
|
25
|
+
return {
|
|
26
|
+
sessionExists: false,
|
|
27
|
+
busy: false,
|
|
28
|
+
retry: false,
|
|
29
|
+
messages: [],
|
|
30
|
+
finalText: null,
|
|
31
|
+
assistant: { exists: false, completed: false, error: null },
|
|
32
|
+
pendingInputs: []
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
var TOOL_OUTPUT_LIMIT = 4000;
|
|
36
|
+
function truncateToolOutput(value) {
|
|
37
|
+
return value.length > TOOL_OUTPUT_LIMIT ? `${value.slice(0, TOOL_OUTPUT_LIMIT)}
|
|
38
|
+
…[truncated ${value.length - TOOL_OUTPUT_LIMIT} chars]` : value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ../agent-runtime/src/opencode.ts
|
|
42
|
+
function isOpencodeRunMessage(info, messageId) {
|
|
43
|
+
return info.id === messageId || info.role === "assistant" && info.parentID === messageId;
|
|
44
|
+
}
|
|
45
|
+
function snapshotParts(parts) {
|
|
46
|
+
const result = [];
|
|
47
|
+
for (const part of parts) {
|
|
48
|
+
if (part.type === "text") {
|
|
49
|
+
if (!part.ignored && part.text.trim())
|
|
50
|
+
result.push({ type: "text", text: part.text });
|
|
51
|
+
} else if (part.type === "tool") {
|
|
52
|
+
const state = part.state;
|
|
53
|
+
result.push({
|
|
54
|
+
type: "tool",
|
|
55
|
+
callId: part.callID,
|
|
56
|
+
tool: part.tool,
|
|
57
|
+
status: state.status,
|
|
58
|
+
title: "title" in state && state.title ? state.title : null,
|
|
59
|
+
input: state.input,
|
|
60
|
+
output: state.status === "completed" ? truncateToolOutput(state.output) : null,
|
|
61
|
+
error: state.status === "error" ? state.error : null,
|
|
62
|
+
startedAt: "time" in state ? new Date(state.time.start).toISOString() : null,
|
|
63
|
+
completedAt: (state.status === "completed" || state.status === "error") && state.time.end ? new Date(state.time.end).toISOString() : null
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function findLastOpencodeRunAssistant(messages, messageId) {
|
|
70
|
+
return messages.findLast((message) => message.info.role === "assistant" && isOpencodeRunMessage(message.info, messageId));
|
|
71
|
+
}
|
|
72
|
+
function formatOpencodeError(error, model) {
|
|
73
|
+
if (!error)
|
|
74
|
+
return "OpenCode request failed";
|
|
75
|
+
if (typeof error === "string")
|
|
76
|
+
return error;
|
|
77
|
+
let message;
|
|
78
|
+
if (typeof error === "object" && "data" in error) {
|
|
79
|
+
const data = error.data;
|
|
80
|
+
if (typeof data?.message === "string")
|
|
81
|
+
message = data.message;
|
|
82
|
+
const name = "name" in error ? error.name : undefined;
|
|
83
|
+
const providerID = typeof data?.providerID === "string" ? data.providerID : model?.providerID;
|
|
84
|
+
const authenticationFailed = name === "ProviderAuthError" || (message ? /invalid api key|authentication|unauthorized/i.test(message) : false);
|
|
85
|
+
if (authenticationFailed && providerID) {
|
|
86
|
+
const selectedModel = model ? ` for model "${model.providerID}/${model.modelID}"` : "";
|
|
87
|
+
return `Model provider credential "${providerID}" was rejected${selectedModel}: ${message ?? "Authentication failed"}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return message ?? JSON.stringify(error) ?? "OpenCode request failed";
|
|
91
|
+
}
|
|
92
|
+
function createWorkspaceOpencodeClient(input) {
|
|
93
|
+
const authorization = input.password ? `Basic ${Buffer.from(`opencode:${input.password}`).toString("base64")}` : undefined;
|
|
94
|
+
return createOpencodeClient({
|
|
95
|
+
baseUrl: input.url,
|
|
96
|
+
directory: input.directory,
|
|
97
|
+
headers: { ...input.headers, ...authorization ? { Authorization: authorization } : {} },
|
|
98
|
+
fetch: input.fetch
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ../agent-runtime/src/http.ts
|
|
103
|
+
class RuntimeHttpError extends Error {
|
|
104
|
+
status;
|
|
105
|
+
body;
|
|
106
|
+
tag;
|
|
107
|
+
constructor(status, body, tag) {
|
|
108
|
+
super(`OpenCode request failed with status ${status}${tag ? ` (${tag})` : ""}`);
|
|
109
|
+
this.status = status;
|
|
110
|
+
this.body = body;
|
|
111
|
+
this.tag = tag;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function isSessionNotFound(error) {
|
|
115
|
+
return error instanceof RuntimeHttpError && error.status === 404;
|
|
116
|
+
}
|
|
117
|
+
function authorizationHeader(password) {
|
|
118
|
+
return password ? { authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` } : {};
|
|
119
|
+
}
|
|
120
|
+
function runtimeUrl(target, path) {
|
|
121
|
+
const url = new URL(`${target.url.replace(/\/$/, "")}${path}`);
|
|
122
|
+
if (target.api === "v2" && target.directory && !url.searchParams.has("location")) {
|
|
123
|
+
url.searchParams.set("location", JSON.stringify({ directory: target.directory }));
|
|
124
|
+
}
|
|
125
|
+
return url.toString();
|
|
126
|
+
}
|
|
127
|
+
function createRuntimeHttp(target) {
|
|
128
|
+
const headers = { ...target.headers, ...authorizationHeader(target.password) };
|
|
129
|
+
async function send(path, init = {}) {
|
|
130
|
+
const { json, ...rest } = init;
|
|
131
|
+
const response = await (target.fetch ?? fetch)(runtimeUrl(target, path), {
|
|
132
|
+
...rest,
|
|
133
|
+
headers: {
|
|
134
|
+
accept: "application/json",
|
|
135
|
+
...json === undefined ? {} : { "content-type": "application/json" },
|
|
136
|
+
...headers,
|
|
137
|
+
...rest.headers
|
|
138
|
+
},
|
|
139
|
+
body: json === undefined ? rest.body : JSON.stringify(json),
|
|
140
|
+
signal: rest.signal ?? (target.signal ? AbortSignal.any([target.signal, AbortSignal.timeout(15000)]) : AbortSignal.timeout(15000))
|
|
141
|
+
});
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
const body = await response.text().catch(() => "");
|
|
144
|
+
let tag = null;
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(body);
|
|
147
|
+
tag = typeof parsed._tag === "string" ? parsed._tag : typeof parsed.name === "string" ? parsed.name : null;
|
|
148
|
+
} catch {}
|
|
149
|
+
throw new RuntimeHttpError(response.status, body, tag);
|
|
150
|
+
}
|
|
151
|
+
return response;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
send,
|
|
155
|
+
async json(path, init) {
|
|
156
|
+
const response = await send(path, init);
|
|
157
|
+
const text = await response.text();
|
|
158
|
+
return text ? JSON.parse(text) : undefined;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function* readServerSentEvents(url, init) {
|
|
163
|
+
const response = await (init.fetch ?? fetch)(url, {
|
|
164
|
+
headers: { accept: "text/event-stream", ...init.headers },
|
|
165
|
+
signal: init.signal
|
|
166
|
+
});
|
|
167
|
+
if (!response.ok || !response.body) {
|
|
168
|
+
await response.body?.cancel().catch(() => {
|
|
169
|
+
return;
|
|
170
|
+
});
|
|
171
|
+
throw new RuntimeHttpError(response.status, "", null);
|
|
172
|
+
}
|
|
173
|
+
const reader = response.body.getReader();
|
|
174
|
+
const onAbort = () => {
|
|
175
|
+
reader.cancel().catch(() => {});
|
|
176
|
+
};
|
|
177
|
+
init.signal.addEventListener("abort", onAbort, { once: true });
|
|
178
|
+
const decoder = new TextDecoder;
|
|
179
|
+
let buffer = "";
|
|
180
|
+
let data = [];
|
|
181
|
+
let event = null;
|
|
182
|
+
try {
|
|
183
|
+
init.signal.throwIfAborted();
|
|
184
|
+
yield { event: "open", data: "" };
|
|
185
|
+
while (true) {
|
|
186
|
+
const { value, done } = await reader.read();
|
|
187
|
+
if (done)
|
|
188
|
+
break;
|
|
189
|
+
buffer += decoder.decode(value, { stream: true });
|
|
190
|
+
let newline = buffer.indexOf(`
|
|
191
|
+
`);
|
|
192
|
+
while (newline >= 0) {
|
|
193
|
+
let line = buffer.slice(0, newline);
|
|
194
|
+
buffer = buffer.slice(newline + 1);
|
|
195
|
+
newline = buffer.indexOf(`
|
|
196
|
+
`);
|
|
197
|
+
if (line.endsWith("\r"))
|
|
198
|
+
line = line.slice(0, -1);
|
|
199
|
+
if (line === "") {
|
|
200
|
+
if (data.length > 0)
|
|
201
|
+
yield { event, data: data.join(`
|
|
202
|
+
`) };
|
|
203
|
+
data = [];
|
|
204
|
+
event = null;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (line.startsWith(":"))
|
|
208
|
+
continue;
|
|
209
|
+
const colon = line.indexOf(":");
|
|
210
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
211
|
+
let fieldValue = colon === -1 ? "" : line.slice(colon + 1);
|
|
212
|
+
if (fieldValue.startsWith(" "))
|
|
213
|
+
fieldValue = fieldValue.slice(1);
|
|
214
|
+
if (field === "data")
|
|
215
|
+
data.push(fieldValue);
|
|
216
|
+
else if (field === "event")
|
|
217
|
+
event = fieldValue;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
init.signal.throwIfAborted();
|
|
221
|
+
} finally {
|
|
222
|
+
init.signal.removeEventListener("abort", onAbort);
|
|
223
|
+
await reader.cancel().catch(() => {
|
|
224
|
+
return;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async function* signalStream(target, path, parse, signal) {
|
|
229
|
+
const stream = readServerSentEvents(runtimeUrl(target, path), {
|
|
230
|
+
headers: { ...target.headers, ...authorizationHeader(target.password) },
|
|
231
|
+
signal,
|
|
232
|
+
fetch: target.fetch
|
|
233
|
+
});
|
|
234
|
+
for await (const item of stream) {
|
|
235
|
+
if (item.event === "open") {
|
|
236
|
+
yield { type: "connected" };
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const raw = parseEventData(item.data);
|
|
240
|
+
const parsed = raw && parse(raw);
|
|
241
|
+
if (parsed)
|
|
242
|
+
yield parsed;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function parseEventData(data) {
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(data);
|
|
248
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function asRecord(value) {
|
|
254
|
+
return value && typeof value === "object" ? value : {};
|
|
255
|
+
}
|
|
256
|
+
function asString(value) {
|
|
257
|
+
return typeof value === "string" ? value : null;
|
|
258
|
+
}
|
|
259
|
+
function asStringArray(value) {
|
|
260
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ../agent-runtime/src/v1.ts
|
|
264
|
+
function createV1Runtime(target) {
|
|
265
|
+
const client = () => createWorkspaceOpencodeClient(target);
|
|
266
|
+
const http = createRuntimeHttp(target);
|
|
267
|
+
const directoryQuery = `?directory=${encodeURIComponent(target.directory)}`;
|
|
268
|
+
return {
|
|
269
|
+
api: "v1",
|
|
270
|
+
async createSession(input) {
|
|
271
|
+
const created = await client().session.create({
|
|
272
|
+
body: input.title ? { title: input.title } : undefined,
|
|
273
|
+
query: { directory: target.directory }
|
|
274
|
+
});
|
|
275
|
+
if (created.error || !created.data)
|
|
276
|
+
throw new Error(formatOpencodeError(created.error));
|
|
277
|
+
return { id: created.data.id, title: created.data.title };
|
|
278
|
+
},
|
|
279
|
+
async prompt(input) {
|
|
280
|
+
const model = parseModelRef(input.model);
|
|
281
|
+
const prompted = await client().session.promptAsync({
|
|
282
|
+
path: { id: input.sessionId },
|
|
283
|
+
query: { directory: target.directory },
|
|
284
|
+
body: {
|
|
285
|
+
messageID: input.messageId,
|
|
286
|
+
parts: [{ type: "text", text: input.prompt }],
|
|
287
|
+
agent: input.agent,
|
|
288
|
+
model
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
if (prompted.error)
|
|
292
|
+
throw new Error(formatOpencodeError(prompted.error));
|
|
293
|
+
},
|
|
294
|
+
async abort(sessionId) {
|
|
295
|
+
const result = await client().session.abort({
|
|
296
|
+
path: { id: sessionId },
|
|
297
|
+
query: { directory: target.directory }
|
|
298
|
+
});
|
|
299
|
+
if (result.error)
|
|
300
|
+
throw new Error(formatOpencodeError(result.error));
|
|
301
|
+
},
|
|
302
|
+
async deleteSession(sessionId) {
|
|
303
|
+
const result = await client().session.delete({
|
|
304
|
+
path: { id: sessionId },
|
|
305
|
+
query: { directory: target.directory }
|
|
306
|
+
});
|
|
307
|
+
if (result.error)
|
|
308
|
+
throw new Error(formatOpencodeError(result.error));
|
|
309
|
+
},
|
|
310
|
+
async snapshot(sessionId, messageId) {
|
|
311
|
+
const api = client();
|
|
312
|
+
const [session, statuses, messages, permissions, questions] = await Promise.all([
|
|
313
|
+
api.session.get({
|
|
314
|
+
path: { id: sessionId },
|
|
315
|
+
query: { directory: target.directory },
|
|
316
|
+
signal: target.signal
|
|
317
|
+
}),
|
|
318
|
+
api.session.status({ query: { directory: target.directory }, signal: target.signal }),
|
|
319
|
+
api.session.messages({
|
|
320
|
+
path: { id: sessionId },
|
|
321
|
+
query: { directory: target.directory },
|
|
322
|
+
signal: target.signal
|
|
323
|
+
}),
|
|
324
|
+
http.json(`/permission${directoryQuery}`),
|
|
325
|
+
http.json(`/question${directoryQuery}`)
|
|
326
|
+
]);
|
|
327
|
+
if (session.response.status === 404)
|
|
328
|
+
return missingSessionSnapshot();
|
|
329
|
+
if (session.error || !session.data)
|
|
330
|
+
throw new Error(formatOpencodeError(session.error));
|
|
331
|
+
if (statuses.error || !statuses.data)
|
|
332
|
+
throw new Error(formatOpencodeError(statuses.error));
|
|
333
|
+
if (messages.error || !messages.data)
|
|
334
|
+
throw new Error(formatOpencodeError(messages.error));
|
|
335
|
+
const status = statuses.data[sessionId];
|
|
336
|
+
const userIndex = messages.data.findIndex((message) => message.info.id === messageId);
|
|
337
|
+
const superseded = userIndex >= 0 && messages.data.slice(userIndex + 1).some((message) => message.info.role === "user");
|
|
338
|
+
const assistant = findLastOpencodeRunAssistant(messages.data, messageId);
|
|
339
|
+
const assistantInfo = assistant?.info.role === "assistant" ? assistant.info : undefined;
|
|
340
|
+
const assistantModel = assistantInfo ? { providerID: assistantInfo.providerID, modelID: assistantInfo.modelID } : undefined;
|
|
341
|
+
const finalText = assistant ? textOf(assistant.parts) : "";
|
|
342
|
+
const pendingInputs = [
|
|
343
|
+
...(Array.isArray(permissions) ? permissions : []).map(asRecord).filter((permission) => permission.sessionID === sessionId).map(permissionRequest),
|
|
344
|
+
...(Array.isArray(questions) ? questions : []).map(asRecord).filter((question) => question.sessionID === sessionId).map(questionRequest)
|
|
345
|
+
];
|
|
346
|
+
return {
|
|
347
|
+
sessionExists: true,
|
|
348
|
+
superseded,
|
|
349
|
+
busy: status?.type === "busy",
|
|
350
|
+
retry: status?.type === "retry",
|
|
351
|
+
messages: messages.data.filter((message) => isOpencodeRunMessage(message.info, messageId)).map((message) => ({
|
|
352
|
+
id: message.info.id,
|
|
353
|
+
role: message.info.role,
|
|
354
|
+
createdAt: new Date(message.info.time.created).toISOString(),
|
|
355
|
+
completedAt: message.info.role === "assistant" && message.info.time.completed ? new Date(message.info.time.completed).toISOString() : null,
|
|
356
|
+
text: textOf(message.parts),
|
|
357
|
+
parts: snapshotParts(message.parts),
|
|
358
|
+
error: message.info.role === "assistant" && message.info.error ? formatOpencodeError(message.info.error, {
|
|
359
|
+
providerID: message.info.providerID,
|
|
360
|
+
modelID: message.info.modelID
|
|
361
|
+
}) : null
|
|
362
|
+
})),
|
|
363
|
+
finalText: finalText || null,
|
|
364
|
+
assistant: {
|
|
365
|
+
exists: Boolean(assistant),
|
|
366
|
+
completed: assistantInfo?.time.completed != null,
|
|
367
|
+
error: assistantInfo?.error ? {
|
|
368
|
+
kind: assistantInfo.error.name === "MessageAbortedError" ? "aborted" : "error",
|
|
369
|
+
message: formatOpencodeError(assistantInfo.error, assistantModel)
|
|
370
|
+
} : null
|
|
371
|
+
},
|
|
372
|
+
pendingInputs: superseded ? [] : pendingInputs
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
subscribe: (signal) => signalStream(target, `/event${directoryQuery}`, parseV1Signal, signal),
|
|
376
|
+
async replyPermission(sessionId, requestId, reply) {
|
|
377
|
+
const result = await client().postSessionIdPermissionsPermissionId({
|
|
378
|
+
path: { id: sessionId, permissionID: requestId },
|
|
379
|
+
query: { directory: target.directory },
|
|
380
|
+
body: { response: reply }
|
|
381
|
+
});
|
|
382
|
+
if (result.response.status === 404)
|
|
383
|
+
throw new RuntimeHttpError(404, "", null);
|
|
384
|
+
if (result.error)
|
|
385
|
+
throw new Error(formatOpencodeError(result.error));
|
|
386
|
+
},
|
|
387
|
+
async replyQuestion(_sessionId, request, answers) {
|
|
388
|
+
await http.send(`/question/${encodeURIComponent(request.id)}/reply${directoryQuery}`, {
|
|
389
|
+
method: "POST",
|
|
390
|
+
json: { answers }
|
|
391
|
+
});
|
|
392
|
+
},
|
|
393
|
+
async rejectQuestion(_sessionId, requestId) {
|
|
394
|
+
await http.send(`/question/${encodeURIComponent(requestId)}/reject${directoryQuery}`, {
|
|
395
|
+
method: "POST",
|
|
396
|
+
json: {}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function textOf(parts) {
|
|
402
|
+
return parts.filter((part) => part.type === "text" && !part.ignored).map((part) => part.text ?? "").join(`
|
|
403
|
+
`).trim();
|
|
404
|
+
}
|
|
405
|
+
function permissionRequest(raw) {
|
|
406
|
+
const tool = asRecord(raw.tool);
|
|
407
|
+
const permission = asString(raw.permission) ?? asString(raw.type) ?? "permission";
|
|
408
|
+
const patterns = asStringArray(raw.patterns).length > 0 ? asStringArray(raw.patterns) : asStringArray(Array.isArray(raw.pattern) ? raw.pattern : [raw.pattern]);
|
|
409
|
+
const createdMs = asRecord(raw.time).created;
|
|
410
|
+
return {
|
|
411
|
+
id: asString(raw.id) ?? "",
|
|
412
|
+
kind: "permission",
|
|
413
|
+
createdAt: typeof createdMs === "number" ? new Date(createdMs).toISOString() : null,
|
|
414
|
+
toolCallId: asString(tool.callID) ?? asString(raw.callID),
|
|
415
|
+
permission,
|
|
416
|
+
patterns,
|
|
417
|
+
always: asStringArray(raw.always),
|
|
418
|
+
title: asString(raw.title) ?? permissionTitle(permission, patterns)
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function questionRequest(raw) {
|
|
422
|
+
const tool = asRecord(raw.tool);
|
|
423
|
+
const questions = Array.isArray(raw.questions) ? raw.questions.map(asRecord) : [];
|
|
424
|
+
return {
|
|
425
|
+
id: asString(raw.id) ?? "",
|
|
426
|
+
kind: "question",
|
|
427
|
+
createdAt: null,
|
|
428
|
+
toolCallId: asString(tool.callID),
|
|
429
|
+
questions: questions.map((question, index) => ({
|
|
430
|
+
key: `q${index}`,
|
|
431
|
+
header: asString(question.header) ?? `Question ${index + 1}`,
|
|
432
|
+
question: asString(question.question) ?? "",
|
|
433
|
+
options: (Array.isArray(question.options) ? question.options.map(asRecord) : []).map((option) => ({
|
|
434
|
+
label: asString(option.label) ?? "",
|
|
435
|
+
description: asString(option.description) ?? ""
|
|
436
|
+
})),
|
|
437
|
+
multiple: question.multiple === true,
|
|
438
|
+
custom: question.custom === true
|
|
439
|
+
}))
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function changed(sessionId) {
|
|
443
|
+
return sessionId ? { type: "session.changed", sessionId } : null;
|
|
444
|
+
}
|
|
445
|
+
function parseV1Signal(raw) {
|
|
446
|
+
const properties = asRecord(raw.properties);
|
|
447
|
+
switch (raw.type) {
|
|
448
|
+
case "message.updated":
|
|
449
|
+
case "message.removed":
|
|
450
|
+
return changed(asString(asRecord(properties.info).sessionID) ?? asString(properties.sessionID));
|
|
451
|
+
case "message.part.updated":
|
|
452
|
+
case "message.part.removed":
|
|
453
|
+
return changed(asString(asRecord(properties.part).sessionID) ?? asString(properties.sessionID));
|
|
454
|
+
case "session.status": {
|
|
455
|
+
const sessionId = asString(properties.sessionID);
|
|
456
|
+
const status = sessionStatusOf(asString(asRecord(properties.status).type));
|
|
457
|
+
return sessionId ? { type: "session.status", sessionId, status } : null;
|
|
458
|
+
}
|
|
459
|
+
case "session.idle": {
|
|
460
|
+
const sessionId = asString(properties.sessionID);
|
|
461
|
+
return sessionId ? { type: "session.status", sessionId, status: "idle" } : null;
|
|
462
|
+
}
|
|
463
|
+
case "session.error": {
|
|
464
|
+
const sessionId = asString(properties.sessionID);
|
|
465
|
+
return sessionId ? { type: "session.error", sessionId, message: formatOpencodeError(properties.error) } : null;
|
|
466
|
+
}
|
|
467
|
+
case "session.deleted": {
|
|
468
|
+
const sessionId = asString(asRecord(properties.info).id);
|
|
469
|
+
return sessionId ? { type: "session.deleted", sessionId } : null;
|
|
470
|
+
}
|
|
471
|
+
case "permission.asked":
|
|
472
|
+
case "permission.updated": {
|
|
473
|
+
const sessionId = asString(properties.sessionID);
|
|
474
|
+
return sessionId ? { type: "input.asked", sessionId, request: permissionRequest(properties) } : null;
|
|
475
|
+
}
|
|
476
|
+
case "question.asked": {
|
|
477
|
+
const sessionId = asString(properties.sessionID);
|
|
478
|
+
return sessionId ? { type: "input.asked", sessionId, request: questionRequest(properties) } : null;
|
|
479
|
+
}
|
|
480
|
+
case "permission.replied":
|
|
481
|
+
case "question.replied":
|
|
482
|
+
case "question.rejected": {
|
|
483
|
+
const sessionId = asString(properties.sessionID);
|
|
484
|
+
const requestId = asString(properties.requestID) ?? asString(properties.permissionID);
|
|
485
|
+
return sessionId && requestId ? { type: "input.resolved", sessionId, requestId } : null;
|
|
486
|
+
}
|
|
487
|
+
default:
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ../agent-runtime/src/v2.ts
|
|
493
|
+
function session(sessionId) {
|
|
494
|
+
return `/api/session/${encodeURIComponent(sessionId)}`;
|
|
495
|
+
}
|
|
496
|
+
function createV2Runtime(target) {
|
|
497
|
+
const http = createRuntimeHttp(target);
|
|
498
|
+
async function switchSessionOptions(sessionId, agent, model) {
|
|
499
|
+
const modelRef = parseModelRef(model);
|
|
500
|
+
if (modelRef) {
|
|
501
|
+
await http.send(`${session(sessionId)}/model`, {
|
|
502
|
+
method: "POST",
|
|
503
|
+
json: { model: { providerID: modelRef.providerID, id: modelRef.modelID } }
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (agent) {
|
|
507
|
+
await http.send(`${session(sessionId)}/agent`, { method: "POST", json: { agent } });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
api: "v2",
|
|
512
|
+
async createSession(input) {
|
|
513
|
+
const modelRef = parseModelRef(input.model);
|
|
514
|
+
const created = await http.json("/api/session", {
|
|
515
|
+
method: "POST",
|
|
516
|
+
json: {
|
|
517
|
+
title: input.title,
|
|
518
|
+
agent: input.agent,
|
|
519
|
+
model: modelRef ? { providerID: modelRef.providerID, id: modelRef.modelID } : undefined,
|
|
520
|
+
location: { directory: target.directory }
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
return { id: created.data.id, title: created.data.title ?? input.title ?? "Agent run" };
|
|
524
|
+
},
|
|
525
|
+
async prompt(input) {
|
|
526
|
+
await switchSessionOptions(input.sessionId, input.agent, input.model);
|
|
527
|
+
await http.send(`${session(input.sessionId)}/prompt`, {
|
|
528
|
+
method: "POST",
|
|
529
|
+
json: { id: input.messageId, text: input.prompt }
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
async abort(sessionId) {
|
|
533
|
+
await http.send(`${session(sessionId)}/interrupt`, { method: "POST", json: {} });
|
|
534
|
+
},
|
|
535
|
+
async deleteSession(sessionId) {
|
|
536
|
+
await http.send(session(sessionId), { method: "DELETE" });
|
|
537
|
+
},
|
|
538
|
+
async snapshot(sessionId, messageId) {
|
|
539
|
+
try {
|
|
540
|
+
await http.send(session(sessionId));
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if (isSessionNotFound(error))
|
|
543
|
+
return missingSessionSnapshot();
|
|
544
|
+
throw error;
|
|
545
|
+
}
|
|
546
|
+
const [active, messages, permissions, forms] = await Promise.all([
|
|
547
|
+
http.json("/api/session/active"),
|
|
548
|
+
listMessages(sessionId),
|
|
549
|
+
http.json(`${session(sessionId)}/permission`),
|
|
550
|
+
http.json(`${session(sessionId)}/form`)
|
|
551
|
+
]);
|
|
552
|
+
const runMessages = selectRunMessages(messages, messageId);
|
|
553
|
+
const userIndex = messages.findIndex((message) => message.id === messageId);
|
|
554
|
+
const superseded = userIndex >= 0 && messages.slice(userIndex + 1).some((message) => message.type === "user");
|
|
555
|
+
const assistant = runMessages.findLast((message) => message.type === "assistant");
|
|
556
|
+
const assistantTime = asRecord(assistant?.time);
|
|
557
|
+
const assistantError = asRecord(assistant?.error);
|
|
558
|
+
const finalText = assistant ? assistantText(assistant) : "";
|
|
559
|
+
return {
|
|
560
|
+
sessionExists: true,
|
|
561
|
+
superseded,
|
|
562
|
+
busy: sessionId in asRecord(active.data),
|
|
563
|
+
retry: Boolean(assistant?.retry) && assistantTime.completed == null,
|
|
564
|
+
messages: runMessages.map(normalizeMessage),
|
|
565
|
+
finalText: finalText || null,
|
|
566
|
+
assistant: {
|
|
567
|
+
exists: Boolean(assistant),
|
|
568
|
+
completed: assistantTime.completed != null,
|
|
569
|
+
error: assistant?.error ? {
|
|
570
|
+
kind: assistantError.type === "aborted" ? "aborted" : "error",
|
|
571
|
+
message: asString(assistantError.message) ?? "Agent request failed"
|
|
572
|
+
} : null
|
|
573
|
+
},
|
|
574
|
+
pendingInputs: superseded ? [] : [
|
|
575
|
+
...(permissions.data ?? []).map(asRecord).map(permissionRequest2),
|
|
576
|
+
...(forms.data ?? []).map(asRecord).map(formRequest)
|
|
577
|
+
]
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
subscribe: (signal) => signalStream(target, "/api/event", parseV2Signal, signal),
|
|
581
|
+
async replyPermission(sessionId, requestId, reply) {
|
|
582
|
+
await http.send(`${session(sessionId)}/permission/${encodeURIComponent(requestId)}/reply`, {
|
|
583
|
+
method: "POST",
|
|
584
|
+
json: { reply }
|
|
585
|
+
});
|
|
586
|
+
},
|
|
587
|
+
async replyQuestion(sessionId, request, answers) {
|
|
588
|
+
const answer = {};
|
|
589
|
+
request.questions.forEach((question, index) => {
|
|
590
|
+
const selected = (answers[index] ?? []).map((label) => optionValue(question, label));
|
|
591
|
+
answer[question.key] = question.multiple ? selected : selected[0] ?? "";
|
|
592
|
+
});
|
|
593
|
+
await http.send(`${session(sessionId)}/form/${encodeURIComponent(request.id)}/reply`, {
|
|
594
|
+
method: "POST",
|
|
595
|
+
json: { answer }
|
|
596
|
+
});
|
|
597
|
+
},
|
|
598
|
+
async rejectQuestion(sessionId, requestId) {
|
|
599
|
+
await http.send(`${session(sessionId)}/form/${encodeURIComponent(requestId)}/cancel`, {
|
|
600
|
+
method: "POST",
|
|
601
|
+
json: {}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
async function listMessages(sessionId) {
|
|
606
|
+
const pageSize = 200;
|
|
607
|
+
const messages = [];
|
|
608
|
+
let query = `?order=asc&limit=${pageSize}`;
|
|
609
|
+
while (true) {
|
|
610
|
+
const page = await http.json(`${session(sessionId)}/message${query}`);
|
|
611
|
+
const items = page.data ?? [];
|
|
612
|
+
messages.push(...items.map(asRecord));
|
|
613
|
+
const next = page.cursor?.next ?? null;
|
|
614
|
+
if (!next || items.length < pageSize)
|
|
615
|
+
break;
|
|
616
|
+
query = `?cursor=${encodeURIComponent(next)}&limit=${pageSize}`;
|
|
617
|
+
}
|
|
618
|
+
return messages;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function selectRunMessages(messages, messageId) {
|
|
622
|
+
const start = messages.findIndex((message) => message.id === messageId);
|
|
623
|
+
if (start === -1)
|
|
624
|
+
return [];
|
|
625
|
+
const selected = [messages[start]];
|
|
626
|
+
for (const message of messages.slice(start + 1)) {
|
|
627
|
+
if (message.type === "user")
|
|
628
|
+
break;
|
|
629
|
+
if (message.type === "assistant")
|
|
630
|
+
selected.push(message);
|
|
631
|
+
}
|
|
632
|
+
return selected;
|
|
633
|
+
}
|
|
634
|
+
function assistantText(message) {
|
|
635
|
+
return (Array.isArray(message.content) ? message.content.map(asRecord) : []).filter((item) => item.type === "text").map((item) => asString(item.text) ?? "").join(`
|
|
636
|
+
`).trim();
|
|
637
|
+
}
|
|
638
|
+
function toIso(value) {
|
|
639
|
+
return typeof value === "number" ? new Date(value).toISOString() : null;
|
|
640
|
+
}
|
|
641
|
+
function normalizeMessage(message) {
|
|
642
|
+
const time = asRecord(message.time);
|
|
643
|
+
const id = asString(message.id) ?? "";
|
|
644
|
+
if (message.type === "user") {
|
|
645
|
+
const text = asString(message.text) ?? "";
|
|
646
|
+
return {
|
|
647
|
+
id,
|
|
648
|
+
role: "user",
|
|
649
|
+
createdAt: toIso(time.created) ?? new Date().toISOString(),
|
|
650
|
+
completedAt: null,
|
|
651
|
+
text,
|
|
652
|
+
parts: text ? [{ type: "text", text }] : [],
|
|
653
|
+
error: null
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const parts = [];
|
|
657
|
+
for (const item of Array.isArray(message.content) ? message.content.map(asRecord) : []) {
|
|
658
|
+
if (item.type === "text") {
|
|
659
|
+
const text = asString(item.text) ?? "";
|
|
660
|
+
if (text.trim())
|
|
661
|
+
parts.push({ type: "text", text });
|
|
662
|
+
} else if (item.type === "tool") {
|
|
663
|
+
const state = asRecord(item.state);
|
|
664
|
+
const toolTime = asRecord(item.time);
|
|
665
|
+
const status = asString(state.status);
|
|
666
|
+
const output = (Array.isArray(state.content) ? state.content.map(asRecord) : []).filter((content) => content.type === "text").map((content) => asString(content.text) ?? "").join(`
|
|
667
|
+
`);
|
|
668
|
+
parts.push({
|
|
669
|
+
type: "tool",
|
|
670
|
+
callId: asString(item.id) ?? "",
|
|
671
|
+
tool: asString(item.name) ?? "tool",
|
|
672
|
+
status: status === "running" || status === "completed" || status === "error" ? status : "pending",
|
|
673
|
+
title: asString(asRecord(state.metadata).title),
|
|
674
|
+
input: typeof state.input === "object" && state.input ? asRecord(state.input) : {},
|
|
675
|
+
output: status === "completed" ? truncateToolOutput(output) : null,
|
|
676
|
+
error: status === "error" ? asString(asRecord(state.error).message) ?? "Tool failed" : null,
|
|
677
|
+
startedAt: toIso(toolTime.ran) ?? toIso(toolTime.created),
|
|
678
|
+
completedAt: toIso(toolTime.completed)
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const error = asRecord(message.error);
|
|
683
|
+
return {
|
|
684
|
+
id,
|
|
685
|
+
role: "assistant",
|
|
686
|
+
createdAt: toIso(time.created) ?? new Date().toISOString(),
|
|
687
|
+
completedAt: toIso(time.completed),
|
|
688
|
+
text: assistantText(message),
|
|
689
|
+
parts,
|
|
690
|
+
error: message.error ? asString(error.message) ?? "Agent request failed" : null
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function permissionRequest2(raw) {
|
|
694
|
+
const action = asString(raw.action) ?? "permission";
|
|
695
|
+
const patterns = asStringArray(raw.resources);
|
|
696
|
+
return {
|
|
697
|
+
id: asString(raw.id) ?? "",
|
|
698
|
+
kind: "permission",
|
|
699
|
+
createdAt: toIso(asRecord(raw.time).created),
|
|
700
|
+
toolCallId: asString(asRecord(raw.source).id),
|
|
701
|
+
permission: action,
|
|
702
|
+
patterns,
|
|
703
|
+
always: asStringArray(raw.save),
|
|
704
|
+
title: permissionTitle(action, patterns)
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function formRequest(raw) {
|
|
708
|
+
const fields = Array.isArray(raw.fields) ? raw.fields.map(asRecord) : [];
|
|
709
|
+
const tool = asRecord(asRecord(raw.metadata).tool);
|
|
710
|
+
return {
|
|
711
|
+
id: asString(raw.id) ?? "",
|
|
712
|
+
kind: "question",
|
|
713
|
+
createdAt: toIso(asRecord(raw.time).created),
|
|
714
|
+
toolCallId: asString(tool.id) ?? asString(tool.callID),
|
|
715
|
+
questions: fields.map((field, index) => {
|
|
716
|
+
const key = asString(field.key) ?? `q${index}`;
|
|
717
|
+
return {
|
|
718
|
+
key,
|
|
719
|
+
header: asString(field.title) ?? key,
|
|
720
|
+
question: asString(field.description) ?? asString(field.title) ?? key,
|
|
721
|
+
options: (Array.isArray(field.options) ? field.options.map(asRecord) : []).map((option) => {
|
|
722
|
+
const value = asString(option.value);
|
|
723
|
+
const label = asString(option.label) ?? value ?? "";
|
|
724
|
+
return {
|
|
725
|
+
label,
|
|
726
|
+
description: asString(option.description) ?? "",
|
|
727
|
+
...value && value !== label ? { value } : {}
|
|
728
|
+
};
|
|
729
|
+
}),
|
|
730
|
+
multiple: field.type === "multiselect",
|
|
731
|
+
custom: field.custom === true
|
|
732
|
+
};
|
|
733
|
+
})
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function optionValue(question, label) {
|
|
737
|
+
const option = question.options.find((candidate) => candidate.label === label);
|
|
738
|
+
return option?.value ?? label;
|
|
739
|
+
}
|
|
740
|
+
function parseV2Signal(raw) {
|
|
741
|
+
const data = asRecord(raw.data);
|
|
742
|
+
const type = asString(raw.type) ?? "";
|
|
743
|
+
const sessionId = asString(data.sessionID);
|
|
744
|
+
const changed2 = () => sessionId ? { type: "session.changed", sessionId } : null;
|
|
745
|
+
if (type.startsWith("session.step.") || type.startsWith("session.tool."))
|
|
746
|
+
return changed2();
|
|
747
|
+
switch (type) {
|
|
748
|
+
case "session.text.ended":
|
|
749
|
+
case "session.reasoning.ended":
|
|
750
|
+
case "session.message.content.updated":
|
|
751
|
+
case "session.inbox.delivered":
|
|
752
|
+
return changed2();
|
|
753
|
+
case "session.execution.started":
|
|
754
|
+
return sessionId ? { type: "session.status", sessionId, status: "busy" } : null;
|
|
755
|
+
case "session.execution.succeeded":
|
|
756
|
+
case "session.execution.failed":
|
|
757
|
+
case "session.execution.interrupted":
|
|
758
|
+
case "session.idle":
|
|
759
|
+
return sessionId ? { type: "session.status", sessionId, status: "idle" } : null;
|
|
760
|
+
case "session.retry.scheduled":
|
|
761
|
+
return sessionId ? { type: "session.status", sessionId, status: "retry" } : null;
|
|
762
|
+
case "session.status": {
|
|
763
|
+
const status = sessionStatusOf(asString(asRecord(data.status).type));
|
|
764
|
+
return sessionId ? { type: "session.status", sessionId, status } : null;
|
|
765
|
+
}
|
|
766
|
+
case "session.error":
|
|
767
|
+
return sessionId ? {
|
|
768
|
+
type: "session.error",
|
|
769
|
+
sessionId,
|
|
770
|
+
message: asString(asRecord(data.error).message) ?? "Agent request failed"
|
|
771
|
+
} : null;
|
|
772
|
+
case "session.deleted": {
|
|
773
|
+
const deleted = sessionId ?? asString(data.id) ?? asString(asRecord(data.info).id);
|
|
774
|
+
return deleted ? { type: "session.deleted", sessionId: deleted } : null;
|
|
775
|
+
}
|
|
776
|
+
case "permission.asked":
|
|
777
|
+
return sessionId ? { type: "input.asked", sessionId, request: permissionRequest2(data) } : null;
|
|
778
|
+
case "form.created": {
|
|
779
|
+
const form = asRecord(data.form);
|
|
780
|
+
const formSession = asString(form.sessionID);
|
|
781
|
+
return formSession ? { type: "input.asked", sessionId: formSession, request: formRequest(form) } : null;
|
|
782
|
+
}
|
|
783
|
+
case "permission.replied": {
|
|
784
|
+
const requestId = asString(data.requestID);
|
|
785
|
+
return sessionId && requestId ? { type: "input.resolved", sessionId, requestId } : null;
|
|
786
|
+
}
|
|
787
|
+
case "form.replied":
|
|
788
|
+
case "form.cancelled": {
|
|
789
|
+
const requestId = asString(data.id);
|
|
790
|
+
return sessionId && requestId ? { type: "input.resolved", sessionId, requestId } : null;
|
|
791
|
+
}
|
|
792
|
+
default:
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
// ../agent-runtime/src/derive.ts
|
|
797
|
+
var MISSING_ASSISTANT_GRACE_MS = 1e4;
|
|
798
|
+
function deriveRunState(snapshot, context) {
|
|
799
|
+
if (!snapshot.sessionExists) {
|
|
800
|
+
return { status: "cancelled", errorMessage: "Session was deleted before the run completed" };
|
|
801
|
+
}
|
|
802
|
+
const assistantError = snapshot.assistant.error;
|
|
803
|
+
if (assistantError?.kind === "aborted") {
|
|
804
|
+
return { status: "cancelled", errorMessage: assistantError.message };
|
|
805
|
+
}
|
|
806
|
+
if (assistantError)
|
|
807
|
+
return { status: "failed", errorMessage: assistantError.message };
|
|
808
|
+
if (!snapshot.superseded) {
|
|
809
|
+
if (snapshot.pendingInputs.length > 0)
|
|
810
|
+
return { status: "awaiting_input", errorMessage: null };
|
|
811
|
+
if (snapshot.retry)
|
|
812
|
+
return { status: "retrying", errorMessage: null };
|
|
813
|
+
if (snapshot.busy)
|
|
814
|
+
return { status: "running", errorMessage: null };
|
|
815
|
+
}
|
|
816
|
+
if (!snapshot.assistant.exists) {
|
|
817
|
+
if (context.sessionError)
|
|
818
|
+
return { status: "failed", errorMessage: context.sessionError };
|
|
819
|
+
const now = context.now ?? Date.now();
|
|
820
|
+
const waited = context.submittedAt ? now - context.submittedAt.getTime() : 0;
|
|
821
|
+
if (context.submittedAt && waited >= MISSING_ASSISTANT_GRACE_MS) {
|
|
822
|
+
return {
|
|
823
|
+
status: "failed",
|
|
824
|
+
errorMessage: "OpenCode stopped before producing an assistant response"
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
return { status: "running", errorMessage: null };
|
|
828
|
+
}
|
|
829
|
+
if (!snapshot.assistant.completed)
|
|
830
|
+
return { status: "running", errorMessage: null };
|
|
831
|
+
return { status: "completed", errorMessage: null };
|
|
832
|
+
}
|
|
833
|
+
// ../agent-runtime/src/index.ts
|
|
834
|
+
function getRuntime(target) {
|
|
835
|
+
return target.api === "v2" ? createV2Runtime(target) : createV1Runtime(target);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ../agent-runtime/src/replies.ts
|
|
839
|
+
function questionAnswers(request, answers) {
|
|
840
|
+
const keys = request.questions.map((question) => question.key);
|
|
841
|
+
if (Object.keys(answers).some((key) => !keys.includes(key)) || keys.some((key) => !Object.hasOwn(answers, key))) {
|
|
842
|
+
throw new Error(`Answers must cover exactly the question keys ${JSON.stringify(keys)}`);
|
|
843
|
+
}
|
|
844
|
+
return request.questions.map((question) => {
|
|
845
|
+
const selected = answers[question.key];
|
|
846
|
+
if (!Array.isArray(selected) || !selected.length || !question.multiple && selected.length !== 1) {
|
|
847
|
+
throw new Error(`Question "${question.key}" requires ${question.multiple ? "at least one answer" : "one answer"}`);
|
|
848
|
+
}
|
|
849
|
+
if (selected.some((answer) => typeof answer !== "string" || !answer.trim() || !question.custom && !question.options.some((option) => option.label === answer))) {
|
|
850
|
+
throw new Error(`Invalid answer for question "${question.key}"`);
|
|
851
|
+
}
|
|
852
|
+
if (new Set(selected).size !== selected.length)
|
|
853
|
+
throw new Error(`Duplicate answer for question "${question.key}"`);
|
|
854
|
+
return selected;
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/errors.ts
|
|
859
|
+
var WORKSPACE_LIFECYCLE_ERROR_CODES = [
|
|
860
|
+
"WORKSPACE_NOT_RUNNING",
|
|
861
|
+
"WORKSPACE_TERMINATED",
|
|
862
|
+
"WORKSPACE_NON_RECOVERABLE",
|
|
863
|
+
"WORKSPACE_START_TIMEOUT",
|
|
864
|
+
"WORKSPACE_RESTART_FAILED"
|
|
865
|
+
];
|
|
866
|
+
|
|
867
|
+
class GittermError extends Error {
|
|
868
|
+
code;
|
|
869
|
+
cause;
|
|
870
|
+
constructor(code, message, options = {}) {
|
|
871
|
+
super(message);
|
|
872
|
+
this.name = "GittermError";
|
|
873
|
+
this.code = code;
|
|
874
|
+
this.cause = options.cause;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
class WorkspaceLifecycleError extends GittermError {
|
|
879
|
+
constructor(code, message, options = {}) {
|
|
880
|
+
super(code, message, options);
|
|
881
|
+
this.name = "WorkspaceLifecycleError";
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
class AgentRunError extends GittermError {
|
|
886
|
+
run;
|
|
887
|
+
constructor(run, code) {
|
|
888
|
+
super(code, run.error ?? (code === "INPUT_REQUIRED" ? "The agent needs input; supply a handler or use runs.respond()" : `Agent run ${run.status}`));
|
|
889
|
+
this.run = run;
|
|
890
|
+
this.name = "AgentRunError";
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// src/runs.ts
|
|
895
|
+
function terminal(status) {
|
|
896
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
897
|
+
}
|
|
898
|
+
function aborted(signal) {
|
|
899
|
+
if (signal?.aborted)
|
|
900
|
+
throw new GittermError("ABORTED", "Run observation was aborted; the agent was not cancelled");
|
|
901
|
+
}
|
|
902
|
+
async function abortable(operation, signal) {
|
|
903
|
+
if (signal.aborted)
|
|
904
|
+
operation.catch(() => {});
|
|
905
|
+
aborted(signal);
|
|
906
|
+
let onAbort;
|
|
907
|
+
try {
|
|
908
|
+
return await Promise.race([
|
|
909
|
+
operation,
|
|
910
|
+
new Promise((_, reject) => {
|
|
911
|
+
onAbort = () => reject(new GittermError("ABORTED", "Run observation was aborted; the agent was not cancelled"));
|
|
912
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
913
|
+
})
|
|
914
|
+
]);
|
|
915
|
+
} finally {
|
|
916
|
+
if (onAbort)
|
|
917
|
+
signal.removeEventListener("abort", onAbort);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function delay(ms, signal) {
|
|
921
|
+
aborted(signal);
|
|
922
|
+
return new Promise((resolve, reject) => {
|
|
923
|
+
const onAbort = () => {
|
|
924
|
+
clearTimeout(timer);
|
|
925
|
+
reject(new GittermError("ABORTED", "Operation aborted"));
|
|
926
|
+
};
|
|
927
|
+
const timer = setTimeout(() => {
|
|
928
|
+
signal.removeEventListener("abort", onAbort);
|
|
929
|
+
resolve();
|
|
930
|
+
}, ms);
|
|
931
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
async function deadline(options, operation) {
|
|
935
|
+
aborted(options?.signal);
|
|
936
|
+
const timeoutMs = options?.timeoutMs ?? 30 * 60000;
|
|
937
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
938
|
+
throw new GittermError("BAD_REQUEST", "timeoutMs must be finite and non-negative");
|
|
939
|
+
const controller = new AbortController;
|
|
940
|
+
const onAbort = () => controller.abort();
|
|
941
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
942
|
+
let timedOut = false;
|
|
943
|
+
const timer = setTimeout(() => {
|
|
944
|
+
timedOut = true;
|
|
945
|
+
controller.abort();
|
|
946
|
+
}, timeoutMs);
|
|
947
|
+
try {
|
|
948
|
+
return await operation(controller.signal);
|
|
949
|
+
} catch (error) {
|
|
950
|
+
if (timedOut)
|
|
951
|
+
throw new GittermError("TIMEOUT", "Run observation timed out; the agent was not cancelled", {
|
|
952
|
+
cause: error
|
|
953
|
+
});
|
|
954
|
+
aborted(options?.signal);
|
|
955
|
+
throw error;
|
|
956
|
+
} finally {
|
|
957
|
+
clearTimeout(timer);
|
|
958
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
959
|
+
controller.abort();
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
async function* runEvents(states) {
|
|
963
|
+
const seen = new Set;
|
|
964
|
+
let pending = new Set;
|
|
965
|
+
let status;
|
|
966
|
+
for await (const run of states) {
|
|
967
|
+
if (run.status !== status) {
|
|
968
|
+
status = run.status;
|
|
969
|
+
yield { type: "run.status", run };
|
|
970
|
+
}
|
|
971
|
+
const current = new Set(run.pendingInputs.map((request) => request.id));
|
|
972
|
+
for (const requestId of pending) {
|
|
973
|
+
if (!current.has(requestId))
|
|
974
|
+
yield { type: "input.resolved", run, requestId };
|
|
975
|
+
}
|
|
976
|
+
pending = current;
|
|
977
|
+
for (const request of run.pendingInputs) {
|
|
978
|
+
if (seen.has(request.id))
|
|
979
|
+
continue;
|
|
980
|
+
seen.add(request.id);
|
|
981
|
+
yield { type: "input.required", run, request };
|
|
982
|
+
}
|
|
983
|
+
if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
|
|
984
|
+
yield { type: `run.${run.status}`, run };
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
function runHelpers(operations) {
|
|
990
|
+
return {
|
|
991
|
+
events: (ref, options) => runEvents(operations.watch(ref, options)),
|
|
992
|
+
wait: (ref, options) => deadline(options, async (signal) => {
|
|
993
|
+
for await (const run of operations.watch(ref, { signal })) {
|
|
994
|
+
if (terminal(run.status) || run.status === "awaiting_input" && options?.until !== "terminal")
|
|
995
|
+
return run;
|
|
996
|
+
}
|
|
997
|
+
throw new GittermError("NETWORK", "Run stream ended before a terminal state");
|
|
998
|
+
}),
|
|
999
|
+
result: (ref, options) => deadline(options, async (signal) => {
|
|
1000
|
+
for await (const event of runEvents(operations.watch(ref, { signal }))) {
|
|
1001
|
+
aborted(signal);
|
|
1002
|
+
if (event.type === "run.completed")
|
|
1003
|
+
return event.run;
|
|
1004
|
+
if (event.type === "run.failed")
|
|
1005
|
+
throw new AgentRunError(event.run, "RUN_FAILED");
|
|
1006
|
+
if (event.type === "run.cancelled")
|
|
1007
|
+
throw new AgentRunError(event.run, "RUN_CANCELLED");
|
|
1008
|
+
if (event.type !== "input.required")
|
|
1009
|
+
continue;
|
|
1010
|
+
const { request } = event;
|
|
1011
|
+
let reply;
|
|
1012
|
+
if (request.kind === "permission") {
|
|
1013
|
+
if (!options?.onPermission)
|
|
1014
|
+
throw new AgentRunError(event.run, "INPUT_REQUIRED");
|
|
1015
|
+
reply = {
|
|
1016
|
+
type: "permission",
|
|
1017
|
+
response: await abortable(Promise.resolve(options.onPermission(request, { signal })), signal)
|
|
1018
|
+
};
|
|
1019
|
+
} else {
|
|
1020
|
+
if (!options?.onQuestion)
|
|
1021
|
+
throw new AgentRunError(event.run, "INPUT_REQUIRED");
|
|
1022
|
+
reply = {
|
|
1023
|
+
type: "question",
|
|
1024
|
+
...await abortable(Promise.resolve(options.onQuestion(request, { signal })), signal)
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
aborted(signal);
|
|
1028
|
+
try {
|
|
1029
|
+
await abortable(operations.respond(ref, { requestId: request.id, reply }), signal);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
if (!(error instanceof GittermError) || error.code !== "INPUT_NOT_PENDING")
|
|
1032
|
+
throw error;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
throw new GittermError("NETWORK", "Run stream ended before a terminal state");
|
|
1036
|
+
})
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// src/direct/runs.ts
|
|
1041
|
+
function directRuntime(workspace, fetchImpl, signal) {
|
|
1042
|
+
return getRuntime({
|
|
1043
|
+
url: workspace.runtime.url,
|
|
1044
|
+
directory: workspace.runtime.directory,
|
|
1045
|
+
password: workspace.runtime.password ?? null,
|
|
1046
|
+
headers: workspace.runtime.headers,
|
|
1047
|
+
api: workspace.opencodeApi,
|
|
1048
|
+
fetch: fetchImpl,
|
|
1049
|
+
signal
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function directError(error) {
|
|
1053
|
+
if (error instanceof GittermError)
|
|
1054
|
+
return error;
|
|
1055
|
+
if (error instanceof RuntimeHttpError) {
|
|
1056
|
+
return new GittermError(error.status === 404 ? "NOT_FOUND" : error.status === 401 ? "UNAUTHORIZED" : error.status === 403 ? "FORBIDDEN" : error.status >= 500 ? "SERVER_ERROR" : "BAD_REQUEST", error.message, { cause: error });
|
|
1057
|
+
}
|
|
1058
|
+
return new GittermError("NETWORK", error instanceof Error ? error.message : "OpenCode request failed", { cause: error });
|
|
1059
|
+
}
|
|
1060
|
+
function directRunOperations(assertWorkspace, fetchImpl) {
|
|
1061
|
+
function assertRun(run) {
|
|
1062
|
+
assertWorkspace(run.workspace);
|
|
1063
|
+
if (run.workspaceId !== run.workspace.id)
|
|
1064
|
+
throw new GittermError("BAD_REQUEST", "Run belongs to another workspace");
|
|
1065
|
+
}
|
|
1066
|
+
async function get(run, sessionError, signal) {
|
|
1067
|
+
assertRun(run);
|
|
1068
|
+
if (terminal(run.status))
|
|
1069
|
+
return run;
|
|
1070
|
+
try {
|
|
1071
|
+
const snapshot = await directRuntime(run.workspace, fetchImpl, signal).snapshot(run.sessionId, run.messageId);
|
|
1072
|
+
const derived = deriveRunState(snapshot, {
|
|
1073
|
+
submittedAt: run.submittedAt ? new Date(run.submittedAt) : null,
|
|
1074
|
+
sessionError
|
|
1075
|
+
});
|
|
1076
|
+
return {
|
|
1077
|
+
...run,
|
|
1078
|
+
status: derived.status,
|
|
1079
|
+
error: derived.errorMessage,
|
|
1080
|
+
finalText: snapshot.finalText,
|
|
1081
|
+
pendingInputs: terminal(derived.status) ? [] : snapshot.pendingInputs,
|
|
1082
|
+
completedAt: terminal(derived.status) ? new Date().toISOString() : null
|
|
1083
|
+
};
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
throw directError(error);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
async function respond(run, input) {
|
|
1089
|
+
const current = await get(run);
|
|
1090
|
+
const request = current.pendingInputs.find((candidate) => candidate.id === input.requestId);
|
|
1091
|
+
if (current.status !== "awaiting_input" || !request)
|
|
1092
|
+
throw new GittermError("INPUT_NOT_PENDING", "Input request is no longer pending");
|
|
1093
|
+
const runtime = directRuntime(run.workspace, fetchImpl);
|
|
1094
|
+
if (request.kind !== input.reply.type)
|
|
1095
|
+
throw new GittermError("BAD_REQUEST", "Reply type does not match the input request");
|
|
1096
|
+
if (input.reply.type === "question" && ("answers" in input.reply ? "reject" in input.reply : input.reply.reject !== true)) {
|
|
1097
|
+
throw new GittermError("BAD_REQUEST", "Question replies require answers or an explicit rejection, not both");
|
|
1098
|
+
}
|
|
1099
|
+
let answers;
|
|
1100
|
+
if (request.kind === "question" && input.reply.type === "question" && "answers" in input.reply) {
|
|
1101
|
+
try {
|
|
1102
|
+
answers = questionAnswers(request, input.reply.answers);
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
throw new GittermError("BAD_REQUEST", error instanceof Error ? error.message : "Invalid answers");
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
if (request.kind === "permission" && input.reply.type === "permission") {
|
|
1109
|
+
if (!["once", "always", "reject"].includes(input.reply.response))
|
|
1110
|
+
throw new GittermError("BAD_REQUEST", "Invalid permission response");
|
|
1111
|
+
await runtime.replyPermission(run.sessionId, request.id, input.reply.response);
|
|
1112
|
+
} else if (request.kind === "question") {
|
|
1113
|
+
if (answers)
|
|
1114
|
+
await runtime.replyQuestion(run.sessionId, request, answers);
|
|
1115
|
+
else
|
|
1116
|
+
await runtime.rejectQuestion(run.sessionId, request.id);
|
|
1117
|
+
}
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
if (error instanceof RuntimeHttpError && error.status === 404)
|
|
1120
|
+
throw new GittermError("INPUT_NOT_PENDING", "Input request was resolved by another responder");
|
|
1121
|
+
throw directError(error);
|
|
1122
|
+
}
|
|
1123
|
+
return get(current);
|
|
1124
|
+
}
|
|
1125
|
+
async function* watch(run, options) {
|
|
1126
|
+
assertRun(run);
|
|
1127
|
+
aborted(options?.signal);
|
|
1128
|
+
if (terminal(run.status)) {
|
|
1129
|
+
yield run;
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
const controller = new AbortController;
|
|
1133
|
+
const onAbort = () => controller.abort();
|
|
1134
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1135
|
+
const runtime = directRuntime(run.workspace, fetchImpl);
|
|
1136
|
+
let dirty = false;
|
|
1137
|
+
let wake;
|
|
1138
|
+
let sessionError = null;
|
|
1139
|
+
let streamError;
|
|
1140
|
+
const notify = () => {
|
|
1141
|
+
dirty = true;
|
|
1142
|
+
wake?.();
|
|
1143
|
+
wake = undefined;
|
|
1144
|
+
};
|
|
1145
|
+
const probe = setInterval(notify, 60000);
|
|
1146
|
+
const pump = (async () => {
|
|
1147
|
+
let backoff = 1000;
|
|
1148
|
+
while (!controller.signal.aborted) {
|
|
1149
|
+
try {
|
|
1150
|
+
for await (const event of runtime.subscribe(controller.signal)) {
|
|
1151
|
+
if (event.type === "connected") {
|
|
1152
|
+
backoff = 1000;
|
|
1153
|
+
notify();
|
|
1154
|
+
} else if (event.sessionId === run.sessionId) {
|
|
1155
|
+
if (event.type === "session.error")
|
|
1156
|
+
sessionError = event.message;
|
|
1157
|
+
notify();
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
if (controller.signal.aborted)
|
|
1162
|
+
return;
|
|
1163
|
+
if (error instanceof RuntimeHttpError && [400, 401, 403, 404].includes(error.status)) {
|
|
1164
|
+
streamError = error;
|
|
1165
|
+
notify();
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
await delay(backoff, controller.signal).catch(() => {});
|
|
1170
|
+
backoff = Math.min(backoff * 2, 30000);
|
|
1171
|
+
}
|
|
1172
|
+
})();
|
|
1173
|
+
let current = run;
|
|
1174
|
+
let previous;
|
|
1175
|
+
try {
|
|
1176
|
+
while (true) {
|
|
1177
|
+
aborted(options?.signal);
|
|
1178
|
+
if (!dirty)
|
|
1179
|
+
await abortable(new Promise((resolve) => {
|
|
1180
|
+
wake = resolve;
|
|
1181
|
+
}), controller.signal);
|
|
1182
|
+
dirty = false;
|
|
1183
|
+
if (streamError)
|
|
1184
|
+
throw directError(streamError);
|
|
1185
|
+
current = await abortable(get(current, sessionError, controller.signal), controller.signal);
|
|
1186
|
+
const key = JSON.stringify([current.status, current.pendingInputs]);
|
|
1187
|
+
if (key !== previous) {
|
|
1188
|
+
previous = key;
|
|
1189
|
+
yield current;
|
|
1190
|
+
}
|
|
1191
|
+
if (terminal(current.status))
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
} finally {
|
|
1195
|
+
clearInterval(probe);
|
|
1196
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
1197
|
+
controller.abort();
|
|
1198
|
+
await pump;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
get: (run) => get(run),
|
|
1203
|
+
watch,
|
|
1204
|
+
respond,
|
|
1205
|
+
...runHelpers({ watch, respond }),
|
|
1206
|
+
async messages(run) {
|
|
1207
|
+
assertRun(run);
|
|
1208
|
+
try {
|
|
1209
|
+
return (await directRuntime(run.workspace, fetchImpl).snapshot(run.sessionId, run.messageId)).messages;
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
throw directError(error);
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
async cancel(run) {
|
|
1215
|
+
const current = await get(run);
|
|
1216
|
+
if (terminal(current.status))
|
|
1217
|
+
return { cancelled: current.status === "cancelled" };
|
|
1218
|
+
const runtime = directRuntime(run.workspace, fetchImpl);
|
|
1219
|
+
try {
|
|
1220
|
+
for (const request of current.pendingInputs) {
|
|
1221
|
+
if (request.kind === "permission")
|
|
1222
|
+
await runtime.replyPermission(run.sessionId, request.id, "reject");
|
|
1223
|
+
else
|
|
1224
|
+
await runtime.rejectQuestion(run.sessionId, request.id);
|
|
1225
|
+
}
|
|
1226
|
+
await runtime.abort(run.sessionId);
|
|
1227
|
+
return { cancelled: true };
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
throw directError(error);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/direct/client.ts
|
|
4
1236
|
import {
|
|
5
1237
|
createOpencodeClient as createOpencodeV2Client
|
|
6
1238
|
} from "@opencode-ai/sdk/v2";
|
|
@@ -22,9 +1254,9 @@ function resolveDirectImage(image) {
|
|
|
22
1254
|
return image?.trim() || DIRECT_OPENCODE_SERVER_IMAGE;
|
|
23
1255
|
}
|
|
24
1256
|
function directModelAuth(credential) {
|
|
25
|
-
if (credential.
|
|
26
|
-
if (!credential.refreshToken.trim())
|
|
27
|
-
throw new
|
|
1257
|
+
if (credential.source === "oauth") {
|
|
1258
|
+
if (typeof credential.refreshToken !== "string" || !credential.refreshToken.trim())
|
|
1259
|
+
throw new GittermError("MODEL_CREDENTIAL_INVALID", "OAuth refreshToken is required");
|
|
28
1260
|
if (credential.expiresAt != null && (!Number.isFinite(credential.expiresAt) || credential.expiresAt < 0)) {
|
|
29
1261
|
throw new Error("OAuth expiresAt must be a non-negative Unix epoch time in milliseconds");
|
|
30
1262
|
}
|
|
@@ -37,8 +1269,9 @@ function directModelAuth(credential) {
|
|
|
37
1269
|
...credential.enterpriseUrl ? { enterpriseUrl: credential.enterpriseUrl } : {}
|
|
38
1270
|
};
|
|
39
1271
|
}
|
|
40
|
-
if (!credential.apiKey.trim())
|
|
41
|
-
throw new
|
|
1272
|
+
if (credential.source !== "apiKey" || typeof credential.apiKey !== "string" || !credential.apiKey.trim()) {
|
|
1273
|
+
throw new GittermError("MODEL_CREDENTIAL_INVALID", "Inline model credential apiKey is required");
|
|
1274
|
+
}
|
|
42
1275
|
return {
|
|
43
1276
|
type: "api",
|
|
44
1277
|
key: credential.apiKey,
|
|
@@ -99,8 +1332,15 @@ function buildDirectProvisioningPlan(input) {
|
|
|
99
1332
|
}
|
|
100
1333
|
}
|
|
101
1334
|
const credentials = new Map;
|
|
102
|
-
|
|
103
|
-
|
|
1335
|
+
if (input.models?.default && !/^[^/]+\/.+$/.test(input.models.default)) {
|
|
1336
|
+
throw new GittermError("BAD_REQUEST", "Model must use provider/model format");
|
|
1337
|
+
}
|
|
1338
|
+
if (input.models?.inherit && input.models.inherit !== "none") {
|
|
1339
|
+
throw new GittermError("MODEL_CREDENTIAL_INVALID", "Direct mode cannot inherit dashboard credentials");
|
|
1340
|
+
}
|
|
1341
|
+
for (const [key, source] of Object.entries(input.models?.providers ?? {})) {
|
|
1342
|
+
const credential = { ...source, providerName: key };
|
|
1343
|
+
const providerName = key.trim();
|
|
104
1344
|
if (!providerName)
|
|
105
1345
|
throw new Error("Model credential providerName is required");
|
|
106
1346
|
if (credentials.has(providerName)) {
|
|
@@ -117,6 +1357,7 @@ function buildDirectProvisioningPlan(input) {
|
|
|
117
1357
|
const config = {
|
|
118
1358
|
$schema: "https://opencode.ai/config.json",
|
|
119
1359
|
...input.opencode?.config,
|
|
1360
|
+
...input.models?.default ? { model: input.models.default } : {},
|
|
120
1361
|
username: "Gitterm direct",
|
|
121
1362
|
permission: {
|
|
122
1363
|
external_directory: "allow",
|
|
@@ -257,8 +1498,8 @@ function basicAuthHeader(password) {
|
|
|
257
1498
|
return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
|
|
258
1499
|
}
|
|
259
1500
|
async function waitForDirectRuntime(runtime, timeoutMs = 60000) {
|
|
260
|
-
const
|
|
261
|
-
while (Date.now() <
|
|
1501
|
+
const deadline2 = Date.now() + timeoutMs;
|
|
1502
|
+
while (Date.now() < deadline2) {
|
|
262
1503
|
const controller = new AbortController;
|
|
263
1504
|
const timer = setTimeout(() => controller.abort(), 5000);
|
|
264
1505
|
const ready = await fetch(runtime.url, {
|
|
@@ -810,8 +2051,8 @@ function createExeDevDirectProvider(config) {
|
|
|
810
2051
|
return token;
|
|
811
2052
|
}
|
|
812
2053
|
async function waitUntilRunning(vmName) {
|
|
813
|
-
const
|
|
814
|
-
while (Date.now() <
|
|
2054
|
+
const deadline2 = Date.now() + 60000;
|
|
2055
|
+
while (Date.now() < deadline2) {
|
|
815
2056
|
const status = await execute(`ls ${vmName}`).then(resultStatus).catch(() => "pending");
|
|
816
2057
|
if (status === "running")
|
|
817
2058
|
return;
|
|
@@ -885,9 +2126,9 @@ function createExeDevDirectProvider(config) {
|
|
|
885
2126
|
await startRuntime(handle);
|
|
886
2127
|
await execute(`share port ${vmName} ${handle.serve.port}`);
|
|
887
2128
|
await execute(`share set-private ${vmName}`);
|
|
888
|
-
const
|
|
889
|
-
await waitForDirectRuntime(
|
|
890
|
-
return { externalId: serializeHandle2(handle), runtime:
|
|
2129
|
+
const directRuntime2 = await runtime(handle, input.password);
|
|
2130
|
+
await waitForDirectRuntime(directRuntime2);
|
|
2131
|
+
return { externalId: serializeHandle2(handle), runtime: directRuntime2 };
|
|
891
2132
|
} catch (error) {
|
|
892
2133
|
if (handle.owned) {
|
|
893
2134
|
await execute(`rm ${vmName}`).catch(() => {
|
|
@@ -922,9 +2163,9 @@ function createExeDevDirectProvider(config) {
|
|
|
922
2163
|
await execute(`share set-private ${handle.vmName}`);
|
|
923
2164
|
if (!workspace.runtime.password)
|
|
924
2165
|
throw new Error("exe.dev runtime password is missing");
|
|
925
|
-
const
|
|
926
|
-
await waitForDirectRuntime(
|
|
927
|
-
return
|
|
2166
|
+
const directRuntime2 = await runtime(handle, workspace.runtime.password);
|
|
2167
|
+
await waitForDirectRuntime(directRuntime2);
|
|
2168
|
+
return directRuntime2;
|
|
928
2169
|
},
|
|
929
2170
|
async terminate(workspace) {
|
|
930
2171
|
const handle = parseHandle3(workspace.externalId);
|
|
@@ -1100,8 +2341,8 @@ function createRailwayDirectProvider(config) {
|
|
|
1100
2341
|
};
|
|
1101
2342
|
const latestDeployment = async (serviceId) => (await serviceSnapshot(serviceId)).serviceInstance?.latestDeployment ?? undefined;
|
|
1102
2343
|
const waitForDeployment = async (serviceId, previousDeploymentId) => {
|
|
1103
|
-
const
|
|
1104
|
-
while (Date.now() <
|
|
2344
|
+
const deadline2 = Date.now() + DEPLOYMENT_TIMEOUT_MS;
|
|
2345
|
+
while (Date.now() < deadline2) {
|
|
1105
2346
|
const deployment = await latestDeployment(serviceId);
|
|
1106
2347
|
if (deployment && deployment.id !== previousDeploymentId) {
|
|
1107
2348
|
if (deployment.status === "SUCCESS")
|
|
@@ -1115,8 +2356,8 @@ function createRailwayDirectProvider(config) {
|
|
|
1115
2356
|
throw new Error("Timed out waiting for Railway deployment");
|
|
1116
2357
|
};
|
|
1117
2358
|
const waitForDeploymentRemoval = async (serviceId, deploymentId) => {
|
|
1118
|
-
const
|
|
1119
|
-
while (Date.now() <
|
|
2359
|
+
const deadline2 = Date.now() + DEPLOYMENT_TIMEOUT_MS;
|
|
2360
|
+
while (Date.now() < deadline2) {
|
|
1120
2361
|
const deployment = await latestDeployment(serviceId);
|
|
1121
2362
|
if (!deployment || deployment.id !== deploymentId || deployment.status === "REMOVED")
|
|
1122
2363
|
return;
|
|
@@ -1505,9 +2746,10 @@ function resolveProvider(provider) {
|
|
|
1505
2746
|
return createVercelDirectProvider(provider);
|
|
1506
2747
|
}
|
|
1507
2748
|
}
|
|
1508
|
-
function runtimeClient(workspace) {
|
|
2749
|
+
function runtimeClient(workspace, fetchImpl) {
|
|
1509
2750
|
const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
|
|
1510
|
-
return
|
|
2751
|
+
return createOpencodeClient2({
|
|
2752
|
+
fetch: fetchImpl,
|
|
1511
2753
|
baseUrl: workspace.runtime.url,
|
|
1512
2754
|
directory: workspace.runtime.directory,
|
|
1513
2755
|
headers: {
|
|
@@ -1516,9 +2758,12 @@ function runtimeClient(workspace) {
|
|
|
1516
2758
|
}
|
|
1517
2759
|
});
|
|
1518
2760
|
}
|
|
1519
|
-
function
|
|
2761
|
+
function createAuthClient(workspace, fetchImpl) {
|
|
2762
|
+
if (workspace.opencodeApi !== "v2")
|
|
2763
|
+
throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2; use inline credentials or a managed workspace with v1");
|
|
1520
2764
|
const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
|
|
1521
2765
|
return createOpencodeV2Client({
|
|
2766
|
+
fetch: fetchImpl,
|
|
1522
2767
|
baseUrl: workspace.runtime.url,
|
|
1523
2768
|
directory: workspace.runtime.directory,
|
|
1524
2769
|
headers: {
|
|
@@ -1553,17 +2798,6 @@ function authStatus(status) {
|
|
|
1553
2798
|
return { status: "failed", message: status.message, ...common };
|
|
1554
2799
|
return { status: status.status, ...common };
|
|
1555
2800
|
}
|
|
1556
|
-
function mapStatus(status, errorName, assistantCompleted = true) {
|
|
1557
|
-
if (errorName === "MessageAbortedError")
|
|
1558
|
-
return "cancelled";
|
|
1559
|
-
if (errorName)
|
|
1560
|
-
return "failed";
|
|
1561
|
-
if (assistantCompleted)
|
|
1562
|
-
return "completed";
|
|
1563
|
-
if (status?.type === "retry")
|
|
1564
|
-
return "retrying";
|
|
1565
|
-
return "running";
|
|
1566
|
-
}
|
|
1567
2801
|
function pollTiming(wait, fallbackTimeoutMs) {
|
|
1568
2802
|
const timeoutMs = wait.timeoutMs ?? fallbackTimeoutMs;
|
|
1569
2803
|
const pollIntervalMs = wait.pollIntervalMs ?? 1000;
|
|
@@ -1575,15 +2809,6 @@ function pollTiming(wait, fallbackTimeoutMs) {
|
|
|
1575
2809
|
}
|
|
1576
2810
|
return { timeoutMs, pollIntervalMs };
|
|
1577
2811
|
}
|
|
1578
|
-
function modelParts(model) {
|
|
1579
|
-
if (!model)
|
|
1580
|
-
return;
|
|
1581
|
-
const separator = model.indexOf("/");
|
|
1582
|
-
if (separator <= 0 || separator === model.length - 1) {
|
|
1583
|
-
throw new Error('model must use the "provider/model" format');
|
|
1584
|
-
}
|
|
1585
|
-
return { providerID: model.slice(0, separator), modelID: model.slice(separator + 1) };
|
|
1586
|
-
}
|
|
1587
2812
|
function validateEnvironmentVariables(values) {
|
|
1588
2813
|
for (const key of Object.keys(values)) {
|
|
1589
2814
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
@@ -1618,15 +2843,18 @@ function setupRunner(commands) {
|
|
|
1618
2843
|
}
|
|
1619
2844
|
function createDirectGittermClient(options) {
|
|
1620
2845
|
const provider = resolveProvider(options.provider);
|
|
2846
|
+
const authClient = (workspace) => createAuthClient(workspace, options.fetch);
|
|
2847
|
+
const http = (workspace) => createRuntimeHttp({
|
|
2848
|
+
url: workspace.runtime.url,
|
|
2849
|
+
directory: workspace.runtime.directory,
|
|
2850
|
+
password: workspace.runtime.password ?? null,
|
|
2851
|
+
api: workspace.opencodeApi,
|
|
2852
|
+
headers: workspace.runtime.headers,
|
|
2853
|
+
fetch: options.fetch
|
|
2854
|
+
});
|
|
1621
2855
|
function assertWorkspace(workspace) {
|
|
1622
2856
|
if (workspace.provider !== provider.name) {
|
|
1623
|
-
throw new
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
function assertRunWorkspace(run, workspace) {
|
|
1627
|
-
assertWorkspace(workspace);
|
|
1628
|
-
if (run.workspaceId !== workspace.id) {
|
|
1629
|
-
throw new Error(`Run belongs to workspace ${run.workspaceId}, not ${workspace.id}`);
|
|
2857
|
+
throw new GittermError("BAD_REQUEST", `Workspace belongs to ${workspace.provider}, not ${provider.name}`);
|
|
1630
2858
|
}
|
|
1631
2859
|
}
|
|
1632
2860
|
function assertAuthAttempt(attempt, workspace) {
|
|
@@ -1644,55 +2872,29 @@ function createDirectGittermClient(options) {
|
|
|
1644
2872
|
throw new Error(errorMessage(result.error));
|
|
1645
2873
|
return authStatus(result.data.data);
|
|
1646
2874
|
}
|
|
1647
|
-
async function getRun(run, workspace) {
|
|
1648
|
-
assertRunWorkspace(run, workspace);
|
|
1649
|
-
const client = runtimeClient(workspace);
|
|
1650
|
-
const [statuses, messages] = await Promise.all([
|
|
1651
|
-
client.session.status({ query: { directory: workspace.runtime.directory } }),
|
|
1652
|
-
client.session.messages({
|
|
1653
|
-
path: { id: run.sessionId },
|
|
1654
|
-
query: { directory: workspace.runtime.directory }
|
|
1655
|
-
})
|
|
1656
|
-
]);
|
|
1657
|
-
if (statuses.error || !statuses.data)
|
|
1658
|
-
throw new Error(errorMessage(statuses.error));
|
|
1659
|
-
if (messages.error || !messages.data)
|
|
1660
|
-
throw new Error(errorMessage(messages.error));
|
|
1661
|
-
const related = messages.data.filter((message) => message.info.id === run.messageId || message.info.role === "assistant" && message.info.parentID === run.messageId);
|
|
1662
|
-
const assistant = related.findLast((message) => message.info.role === "assistant");
|
|
1663
|
-
const assistantError = assistant?.info.role === "assistant" ? assistant.info.error : undefined;
|
|
1664
|
-
const assistantCompleted = assistant?.info.role === "assistant" && assistant.info.time.completed != null;
|
|
1665
|
-
const finalText = assistant?.parts.filter((part) => part.type === "text" && !part.ignored).map((part) => part.type === "text" ? part.text : "").join(`
|
|
1666
|
-
`).trim();
|
|
1667
|
-
const sessionStatus = statuses.data[run.sessionId];
|
|
1668
|
-
const status = assistant ? mapStatus(sessionStatus, assistantError?.name, Boolean(assistantCompleted)) : mapStatus(sessionStatus, undefined, false);
|
|
1669
|
-
return {
|
|
1670
|
-
...run,
|
|
1671
|
-
status,
|
|
1672
|
-
error: assistantError ? errorMessage(assistantError) : null,
|
|
1673
|
-
finalText: finalText || null
|
|
1674
|
-
};
|
|
1675
|
-
}
|
|
1676
2875
|
async function startPty(workspace, command, title) {
|
|
1677
|
-
const result = await
|
|
1678
|
-
|
|
1679
|
-
args: ["-lc", command],
|
|
1680
|
-
cwd: workspace.runtime.directory,
|
|
1681
|
-
directory: workspace.runtime.directory,
|
|
1682
|
-
title
|
|
2876
|
+
const result = await http(workspace).json(workspace.opencodeApi === "v2" ? "/api/pty" : `/pty?directory=${encodeURIComponent(workspace.runtime.directory)}`, {
|
|
2877
|
+
method: "POST",
|
|
2878
|
+
json: { command: "bash", args: ["-lc", command], cwd: workspace.runtime.directory, title }
|
|
1683
2879
|
});
|
|
1684
|
-
|
|
1685
|
-
throw new Error(errorMessage(result.error));
|
|
1686
|
-
return result.data;
|
|
2880
|
+
return result;
|
|
1687
2881
|
}
|
|
1688
2882
|
async function setupFile(workspace, name) {
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2883
|
+
try {
|
|
2884
|
+
if (workspace.opencodeApi === "v2") {
|
|
2885
|
+
return (await (await http(workspace).send(`/api/fs/read/${SETUP_DIR}/${name}`)).text()).trim();
|
|
2886
|
+
}
|
|
2887
|
+
const result = await runtimeClient(workspace, options.fetch).file.read({
|
|
2888
|
+
query: { directory: workspace.runtime.directory, path: `${SETUP_DIR}/${name}` }
|
|
2889
|
+
});
|
|
2890
|
+
if (result.error || !result.data || result.data.type !== "text")
|
|
2891
|
+
return null;
|
|
2892
|
+
return result.data.content.trim();
|
|
2893
|
+
} catch (error) {
|
|
2894
|
+
if (error instanceof RuntimeHttpError && error.status === 404)
|
|
2895
|
+
return null;
|
|
2896
|
+
throw directError(error);
|
|
2897
|
+
}
|
|
1696
2898
|
}
|
|
1697
2899
|
async function getSetupStatus(workspace) {
|
|
1698
2900
|
assertWorkspace(workspace);
|
|
@@ -1739,7 +2941,14 @@ function createDirectGittermClient(options) {
|
|
|
1739
2941
|
if (!providerName) {
|
|
1740
2942
|
throw new Error("Model credential providerName is required");
|
|
1741
2943
|
}
|
|
1742
|
-
|
|
2944
|
+
if (workspace.opencodeApi === "v2") {
|
|
2945
|
+
if (credential.source !== "apiKey")
|
|
2946
|
+
throw new GittermError("BAD_REQUEST", "Use connectOAuth() for v2 credential rotation, or inject an OAuth bundle when creating the workspace");
|
|
2947
|
+
directModelAuth(credential);
|
|
2948
|
+
await http(workspace).send(`/api/integration/${encodeURIComponent(providerName)}/connect/key`, { method: "POST", json: { key: credential.apiKey } });
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
const result = await runtimeClient(workspace, options.fetch).auth.set({
|
|
1743
2952
|
path: { id: providerName },
|
|
1744
2953
|
query: { directory: workspace.runtime.directory },
|
|
1745
2954
|
body: directModelAuth(credential)
|
|
@@ -1814,8 +3023,8 @@ function createDirectGittermClient(options) {
|
|
|
1814
3023
|
async wait(attempt, workspace, wait = {}) {
|
|
1815
3024
|
assertAuthAttempt(attempt, workspace);
|
|
1816
3025
|
const { timeoutMs, pollIntervalMs } = pollTiming(wait, Math.max(0, attempt.expiresAt - Date.now()));
|
|
1817
|
-
const
|
|
1818
|
-
while (Date.now() <=
|
|
3026
|
+
const deadline2 = Date.now() + timeoutMs;
|
|
3027
|
+
while (Date.now() <= deadline2) {
|
|
1819
3028
|
const status = await getAuthStatus(attempt, workspace);
|
|
1820
3029
|
if (status.status === "complete")
|
|
1821
3030
|
return status;
|
|
@@ -1823,7 +3032,7 @@ function createDirectGittermClient(options) {
|
|
|
1823
3032
|
throw new Error(status.message);
|
|
1824
3033
|
if (status.status === "expired")
|
|
1825
3034
|
throw new Error("OAuth attempt expired");
|
|
1826
|
-
const remaining =
|
|
3035
|
+
const remaining = deadline2 - Date.now();
|
|
1827
3036
|
if (remaining <= 0)
|
|
1828
3037
|
break;
|
|
1829
3038
|
await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
|
|
@@ -1841,6 +3050,8 @@ function createDirectGittermClient(options) {
|
|
|
1841
3050
|
},
|
|
1842
3051
|
workspaces: {
|
|
1843
3052
|
async create(input = {}) {
|
|
3053
|
+
if (input.opencode?.api && !["v1", "v2"].includes(input.opencode.api))
|
|
3054
|
+
throw new GittermError("BAD_REQUEST", "opencode.api must be v1 or v2");
|
|
1844
3055
|
const lifecycle = input.lifecycle ?? provider.capabilities.recommendedLifecycle;
|
|
1845
3056
|
if (lifecycle === "persistent" && provider.capabilities.persistence === "unsupported") {
|
|
1846
3057
|
throw new Error(`${provider.name} does not support persistent direct workspaces`);
|
|
@@ -1857,6 +3068,7 @@ function createDirectGittermClient(options) {
|
|
|
1857
3068
|
status: "running",
|
|
1858
3069
|
lifecycle,
|
|
1859
3070
|
runtime: created.runtime,
|
|
3071
|
+
opencodeApi: input.opencode?.api ?? "v1",
|
|
1860
3072
|
setup: provisioning.setup.afterAgent.length ? "after_agent" : provisioning.setup.beforeAgent.length ? "before_agent_complete" : "not_requested",
|
|
1861
3073
|
createdAt: new Date().toISOString()
|
|
1862
3074
|
};
|
|
@@ -1911,7 +3123,7 @@ function createDirectGittermClient(options) {
|
|
|
1911
3123
|
setupStatus: getSetupStatus,
|
|
1912
3124
|
async waitForSetup(workspace, wait = {}) {
|
|
1913
3125
|
const { timeoutMs, pollIntervalMs } = pollTiming(wait, 10 * 60000);
|
|
1914
|
-
const
|
|
3126
|
+
const deadline2 = Date.now() + timeoutMs;
|
|
1915
3127
|
while (true) {
|
|
1916
3128
|
const status = await getSetupStatus(workspace);
|
|
1917
3129
|
if (status.status === "not_requested" || status.status === "succeeded")
|
|
@@ -1920,7 +3132,7 @@ function createDirectGittermClient(options) {
|
|
|
1920
3132
|
throw new Error(`Workspace setup failed${status.exitCode == null ? "" : ` with exit code ${status.exitCode}`}${status.log ? `
|
|
1921
3133
|
${status.log}` : ""}`);
|
|
1922
3134
|
}
|
|
1923
|
-
if (Date.now() >=
|
|
3135
|
+
if (Date.now() >= deadline2) {
|
|
1924
3136
|
throw new Error(`Workspace setup timed out after ${timeoutMs}ms`);
|
|
1925
3137
|
}
|
|
1926
3138
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
@@ -1928,89 +3140,75 @@ ${status.log}` : ""}`);
|
|
|
1928
3140
|
}
|
|
1929
3141
|
},
|
|
1930
3142
|
runs: {
|
|
3143
|
+
...directRunOperations(assertWorkspace, options.fetch),
|
|
1931
3144
|
async create(input) {
|
|
1932
3145
|
assertWorkspace(input.workspace);
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
if (!
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
3146
|
+
aborted(input.signal);
|
|
3147
|
+
if (input.workspace.status !== "running")
|
|
3148
|
+
throw new GittermError("WORKSPACE_NOT_RUNNING", "Resume the workspace before creating a run");
|
|
3149
|
+
if (!input.prompt.trim())
|
|
3150
|
+
throw new GittermError("BAD_REQUEST", "Prompt is required");
|
|
3151
|
+
if (input.model && !/^[^/]+\/.+$/.test(input.model))
|
|
3152
|
+
throw new GittermError("BAD_REQUEST", "Model must use provider/model format");
|
|
3153
|
+
if (input.waitForSetup) {
|
|
3154
|
+
const deadline2 = Date.now() + (input.setupTimeoutMs ?? 10 * 60000);
|
|
3155
|
+
while (true) {
|
|
3156
|
+
aborted(input.signal);
|
|
3157
|
+
const status = await getSetupStatus(input.workspace);
|
|
3158
|
+
if (status.status === "succeeded" || status.status === "not_requested")
|
|
3159
|
+
break;
|
|
3160
|
+
if (status.status === "failed")
|
|
3161
|
+
throw new GittermError("BAD_REQUEST", `Workspace setup failed: ${status.log ?? ""}`);
|
|
3162
|
+
if (Date.now() >= deadline2)
|
|
3163
|
+
throw new GittermError("TIMEOUT", "Workspace setup timed out");
|
|
3164
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
const runtime = directRuntime(input.workspace, options.fetch);
|
|
3168
|
+
const previous = input.context?.type === "continue" ? input.context.run : undefined;
|
|
3169
|
+
if (previous && (previous.workspaceId !== input.workspace.id || !terminal(previous.status))) {
|
|
3170
|
+
throw new GittermError("CONFLICT", "Continue a terminal run from the same workspace");
|
|
1945
3171
|
}
|
|
3172
|
+
let sessionId = previous?.sessionId;
|
|
3173
|
+
let title = input.title ?? "Agent run";
|
|
3174
|
+
const now = new Date().toISOString();
|
|
1946
3175
|
const messageId = `msg_${randomUUID().replaceAll("-", "")}`;
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
agent: input.agent,
|
|
1954
|
-
model: modelParts(input.model)
|
|
3176
|
+
try {
|
|
3177
|
+
aborted(input.signal);
|
|
3178
|
+
if (!sessionId) {
|
|
3179
|
+
const session2 = await runtime.createSession(input);
|
|
3180
|
+
sessionId = session2.id;
|
|
3181
|
+
title = session2.title;
|
|
1955
3182
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
if (
|
|
1982
|
-
|
|
1983
|
-
|
|
3183
|
+
aborted(input.signal);
|
|
3184
|
+
await runtime.prompt({
|
|
3185
|
+
sessionId,
|
|
3186
|
+
messageId,
|
|
3187
|
+
prompt: input.prompt,
|
|
3188
|
+
agent: input.agent,
|
|
3189
|
+
model: input.model
|
|
3190
|
+
});
|
|
3191
|
+
return {
|
|
3192
|
+
id: randomUUID(),
|
|
3193
|
+
workspaceId: input.workspace.id,
|
|
3194
|
+
workspace: input.workspace,
|
|
3195
|
+
sessionId,
|
|
3196
|
+
messageId,
|
|
3197
|
+
title,
|
|
3198
|
+
status: "running",
|
|
3199
|
+
error: null,
|
|
3200
|
+
finalText: null,
|
|
3201
|
+
pendingInputs: [],
|
|
3202
|
+
context: previous ? { type: "continued", runId: previous.id } : { type: "isolated" },
|
|
3203
|
+
createdAt: now,
|
|
3204
|
+
submittedAt: now,
|
|
3205
|
+
completedAt: null
|
|
3206
|
+
};
|
|
3207
|
+
} catch (error) {
|
|
3208
|
+
if (sessionId && !previous)
|
|
3209
|
+
await runtime.deleteSession(sessionId).catch(() => {});
|
|
3210
|
+
throw directError(error);
|
|
1984
3211
|
}
|
|
1985
|
-
throw new Error(`Agent run timed out after ${timeoutMs}ms`);
|
|
1986
|
-
},
|
|
1987
|
-
async messages(run, workspace) {
|
|
1988
|
-
assertRunWorkspace(run, workspace);
|
|
1989
|
-
const result = await runtimeClient(workspace).session.messages({
|
|
1990
|
-
path: { id: run.sessionId },
|
|
1991
|
-
query: { directory: workspace.runtime.directory }
|
|
1992
|
-
});
|
|
1993
|
-
if (result.error || !result.data)
|
|
1994
|
-
throw new Error(errorMessage(result.error));
|
|
1995
|
-
return result.data.filter((message) => message.info.id === run.messageId || message.info.role === "assistant" && message.info.parentID === run.messageId).map((message) => ({
|
|
1996
|
-
id: message.info.id,
|
|
1997
|
-
role: message.info.role,
|
|
1998
|
-
text: message.parts.filter((part) => part.type === "text" && !part.ignored).map((part) => part.type === "text" ? part.text : "").join(`
|
|
1999
|
-
`).trim(),
|
|
2000
|
-
error: message.info.role === "assistant" && message.info.error ? errorMessage(message.info.error) : null
|
|
2001
|
-
}));
|
|
2002
|
-
},
|
|
2003
|
-
async cancel(run, workspace) {
|
|
2004
|
-
const current = await getRun(run, workspace);
|
|
2005
|
-
if (!["running", "retrying"].includes(current.status))
|
|
2006
|
-
return false;
|
|
2007
|
-
const result = await runtimeClient(workspace).session.abort({
|
|
2008
|
-
path: { id: run.sessionId },
|
|
2009
|
-
query: { directory: workspace.runtime.directory }
|
|
2010
|
-
});
|
|
2011
|
-
if (result.error)
|
|
2012
|
-
throw new Error(errorMessage(result.error));
|
|
2013
|
-
return result.data === true;
|
|
2014
3212
|
}
|
|
2015
3213
|
}
|
|
2016
3214
|
};
|