@gitterm/sdk 0.1.3 → 0.2.1
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 +443 -341
- package/dist/client.d.ts +19 -9
- package/dist/direct/client.d.ts +15 -14
- package/dist/direct/index.js +1380 -179
- package/dist/direct/runs.d.ts +19 -0
- package/dist/direct/types.d.ts +25 -23
- package/dist/errors.d.ts +7 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +267 -88
- package/dist/runs.d.ts +19 -0
- package/dist/types.d.ts +122 -59
- package/package.json +2 -1
package/dist/direct/index.js
CHANGED
|
@@ -1,6 +1,1241 @@
|
|
|
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 !== false
|
|
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 !== false
|
|
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
|
+
if (!question.options.length && !question.custom) {
|
|
846
|
+
throw new Error(`Question "${question.key}" has no options and does not accept custom answers`);
|
|
847
|
+
}
|
|
848
|
+
const selected = answers[question.key];
|
|
849
|
+
if (!Array.isArray(selected) || !selected.length || !question.multiple && selected.length !== 1) {
|
|
850
|
+
throw new Error(`Question "${question.key}" requires ${question.multiple ? "at least one answer" : "one answer"}`);
|
|
851
|
+
}
|
|
852
|
+
if (selected.some((answer) => typeof answer !== "string" || !answer.trim() || !question.custom && !question.options.some((option) => option.label === answer))) {
|
|
853
|
+
throw new Error(`Invalid answer for question "${question.key}"`);
|
|
854
|
+
}
|
|
855
|
+
if (new Set(selected).size !== selected.length)
|
|
856
|
+
throw new Error(`Duplicate answer for question "${question.key}"`);
|
|
857
|
+
return selected;
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// src/errors.ts
|
|
862
|
+
var WORKSPACE_LIFECYCLE_ERROR_CODES = [
|
|
863
|
+
"WORKSPACE_NOT_RUNNING",
|
|
864
|
+
"WORKSPACE_TERMINATED",
|
|
865
|
+
"WORKSPACE_NON_RECOVERABLE",
|
|
866
|
+
"WORKSPACE_START_TIMEOUT",
|
|
867
|
+
"WORKSPACE_RESTART_FAILED"
|
|
868
|
+
];
|
|
869
|
+
|
|
870
|
+
class GittermError extends Error {
|
|
871
|
+
code;
|
|
872
|
+
cause;
|
|
873
|
+
constructor(code, message, options = {}) {
|
|
874
|
+
super(message);
|
|
875
|
+
this.name = "GittermError";
|
|
876
|
+
this.code = code;
|
|
877
|
+
this.cause = options.cause;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
class WorkspaceLifecycleError extends GittermError {
|
|
882
|
+
constructor(code, message, options = {}) {
|
|
883
|
+
super(code, message, options);
|
|
884
|
+
this.name = "WorkspaceLifecycleError";
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
class AgentRunError extends GittermError {
|
|
889
|
+
run;
|
|
890
|
+
constructor(run, code) {
|
|
891
|
+
super(code, run.error ?? (code === "INPUT_REQUIRED" ? "The agent needs input; supply a handler or use runs.respond()" : `Agent run ${run.status}`));
|
|
892
|
+
this.run = run;
|
|
893
|
+
this.name = "AgentRunError";
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/runs.ts
|
|
898
|
+
function terminal(status) {
|
|
899
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
900
|
+
}
|
|
901
|
+
function aborted(signal) {
|
|
902
|
+
if (signal?.aborted)
|
|
903
|
+
throw new GittermError("ABORTED", "Run observation was aborted; the agent was not cancelled");
|
|
904
|
+
}
|
|
905
|
+
async function abortable(operation, signal) {
|
|
906
|
+
if (signal.aborted)
|
|
907
|
+
operation.catch(() => {});
|
|
908
|
+
aborted(signal);
|
|
909
|
+
let onAbort;
|
|
910
|
+
try {
|
|
911
|
+
return await Promise.race([
|
|
912
|
+
operation,
|
|
913
|
+
new Promise((_, reject) => {
|
|
914
|
+
onAbort = () => reject(new GittermError("ABORTED", "Run observation was aborted; the agent was not cancelled"));
|
|
915
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
916
|
+
})
|
|
917
|
+
]);
|
|
918
|
+
} finally {
|
|
919
|
+
if (onAbort)
|
|
920
|
+
signal.removeEventListener("abort", onAbort);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function delay(ms, signal) {
|
|
924
|
+
aborted(signal);
|
|
925
|
+
return new Promise((resolve, reject) => {
|
|
926
|
+
const onAbort = () => {
|
|
927
|
+
clearTimeout(timer);
|
|
928
|
+
reject(new GittermError("ABORTED", "Operation aborted"));
|
|
929
|
+
};
|
|
930
|
+
const timer = setTimeout(() => {
|
|
931
|
+
signal.removeEventListener("abort", onAbort);
|
|
932
|
+
resolve();
|
|
933
|
+
}, ms);
|
|
934
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
async function deadline(options, operation) {
|
|
938
|
+
aborted(options?.signal);
|
|
939
|
+
const timeoutMs = options?.timeoutMs ?? 30 * 60000;
|
|
940
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0)
|
|
941
|
+
throw new GittermError("BAD_REQUEST", "timeoutMs must be finite and non-negative");
|
|
942
|
+
const controller = new AbortController;
|
|
943
|
+
const onAbort = () => controller.abort();
|
|
944
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
945
|
+
let timedOut = false;
|
|
946
|
+
const timer = setTimeout(() => {
|
|
947
|
+
timedOut = true;
|
|
948
|
+
controller.abort();
|
|
949
|
+
}, timeoutMs);
|
|
950
|
+
try {
|
|
951
|
+
return await operation(controller.signal);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
if (timedOut)
|
|
954
|
+
throw new GittermError("TIMEOUT", "Run observation timed out; the agent was not cancelled", {
|
|
955
|
+
cause: error
|
|
956
|
+
});
|
|
957
|
+
aborted(options?.signal);
|
|
958
|
+
throw error;
|
|
959
|
+
} finally {
|
|
960
|
+
clearTimeout(timer);
|
|
961
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
962
|
+
controller.abort();
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async function* runEvents(states) {
|
|
966
|
+
const seen = new Set;
|
|
967
|
+
let pending = new Set;
|
|
968
|
+
let status;
|
|
969
|
+
for await (const run of states) {
|
|
970
|
+
if (run.status !== status) {
|
|
971
|
+
status = run.status;
|
|
972
|
+
yield { type: "run.status", run };
|
|
973
|
+
}
|
|
974
|
+
const current = new Set(run.pendingInputs.map((request) => request.id));
|
|
975
|
+
for (const requestId of pending) {
|
|
976
|
+
if (!current.has(requestId))
|
|
977
|
+
yield { type: "input.resolved", run, requestId };
|
|
978
|
+
}
|
|
979
|
+
pending = current;
|
|
980
|
+
for (const request of run.pendingInputs) {
|
|
981
|
+
if (seen.has(request.id))
|
|
982
|
+
continue;
|
|
983
|
+
seen.add(request.id);
|
|
984
|
+
yield { type: "input.required", run, request };
|
|
985
|
+
}
|
|
986
|
+
if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
|
|
987
|
+
yield { type: `run.${run.status}`, run };
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
function runHelpers(operations) {
|
|
993
|
+
return {
|
|
994
|
+
events: (ref, options) => runEvents(operations.watch(ref, options)),
|
|
995
|
+
wait: (ref, options) => deadline(options, async (signal) => {
|
|
996
|
+
for await (const run of operations.watch(ref, { signal })) {
|
|
997
|
+
if (terminal(run.status) || run.status === "awaiting_input" && options?.until !== "terminal")
|
|
998
|
+
return run;
|
|
999
|
+
}
|
|
1000
|
+
throw new GittermError("NETWORK", "Run stream ended before a terminal state");
|
|
1001
|
+
}),
|
|
1002
|
+
result: (ref, options) => deadline(options, async (signal) => {
|
|
1003
|
+
for await (const event of runEvents(operations.watch(ref, { signal }))) {
|
|
1004
|
+
aborted(signal);
|
|
1005
|
+
if (event.type === "run.completed")
|
|
1006
|
+
return event.run;
|
|
1007
|
+
if (event.type === "run.failed")
|
|
1008
|
+
throw new AgentRunError(event.run, "RUN_FAILED");
|
|
1009
|
+
if (event.type === "run.cancelled")
|
|
1010
|
+
throw new AgentRunError(event.run, "RUN_CANCELLED");
|
|
1011
|
+
if (event.type !== "input.required")
|
|
1012
|
+
continue;
|
|
1013
|
+
const { request } = event;
|
|
1014
|
+
let reply;
|
|
1015
|
+
if (request.kind === "permission") {
|
|
1016
|
+
if (!options?.onPermission)
|
|
1017
|
+
throw new AgentRunError(event.run, "INPUT_REQUIRED");
|
|
1018
|
+
reply = {
|
|
1019
|
+
type: "permission",
|
|
1020
|
+
response: await abortable(Promise.resolve(options.onPermission(request, { signal })), signal)
|
|
1021
|
+
};
|
|
1022
|
+
} else {
|
|
1023
|
+
if (!options?.onQuestion)
|
|
1024
|
+
throw new AgentRunError(event.run, "INPUT_REQUIRED");
|
|
1025
|
+
reply = {
|
|
1026
|
+
type: "question",
|
|
1027
|
+
...await abortable(Promise.resolve(options.onQuestion(request, { signal })), signal)
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
aborted(signal);
|
|
1031
|
+
try {
|
|
1032
|
+
await abortable(operations.respond(ref, { requestId: request.id, reply }), signal);
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
if (!(error instanceof GittermError) || error.code !== "INPUT_NOT_PENDING")
|
|
1035
|
+
throw error;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
throw new GittermError("NETWORK", "Run stream ended before a terminal state");
|
|
1039
|
+
})
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/direct/runs.ts
|
|
1044
|
+
function directRuntime(workspace, fetchImpl, signal) {
|
|
1045
|
+
return getRuntime({
|
|
1046
|
+
url: workspace.runtime.url,
|
|
1047
|
+
directory: workspace.runtime.directory,
|
|
1048
|
+
password: workspace.runtime.password ?? null,
|
|
1049
|
+
headers: workspace.runtime.headers,
|
|
1050
|
+
api: workspace.opencodeApi,
|
|
1051
|
+
fetch: fetchImpl,
|
|
1052
|
+
signal
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
function directError(error) {
|
|
1056
|
+
if (error instanceof GittermError)
|
|
1057
|
+
return error;
|
|
1058
|
+
if (error instanceof RuntimeHttpError) {
|
|
1059
|
+
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 });
|
|
1060
|
+
}
|
|
1061
|
+
return new GittermError("NETWORK", error instanceof Error ? error.message : "OpenCode request failed", { cause: error });
|
|
1062
|
+
}
|
|
1063
|
+
function directRunOperations(assertWorkspace, fetchImpl) {
|
|
1064
|
+
function assertRun(run) {
|
|
1065
|
+
assertWorkspace(run.workspace);
|
|
1066
|
+
if (run.workspaceId !== run.workspace.id)
|
|
1067
|
+
throw new GittermError("BAD_REQUEST", "Run belongs to another workspace");
|
|
1068
|
+
}
|
|
1069
|
+
async function get(run, sessionError, signal) {
|
|
1070
|
+
assertRun(run);
|
|
1071
|
+
if (terminal(run.status))
|
|
1072
|
+
return run;
|
|
1073
|
+
try {
|
|
1074
|
+
const snapshot = await directRuntime(run.workspace, fetchImpl, signal).snapshot(run.sessionId, run.messageId);
|
|
1075
|
+
const derived = deriveRunState(snapshot, {
|
|
1076
|
+
submittedAt: run.submittedAt ? new Date(run.submittedAt) : null,
|
|
1077
|
+
sessionError
|
|
1078
|
+
});
|
|
1079
|
+
return {
|
|
1080
|
+
...run,
|
|
1081
|
+
status: derived.status,
|
|
1082
|
+
error: derived.errorMessage,
|
|
1083
|
+
finalText: snapshot.finalText,
|
|
1084
|
+
pendingInputs: terminal(derived.status) ? [] : snapshot.pendingInputs,
|
|
1085
|
+
completedAt: terminal(derived.status) ? new Date().toISOString() : null
|
|
1086
|
+
};
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
throw directError(error);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
async function respond(run, input) {
|
|
1092
|
+
const current = await get(run);
|
|
1093
|
+
const request = current.pendingInputs.find((candidate) => candidate.id === input.requestId);
|
|
1094
|
+
if (current.status !== "awaiting_input" || !request)
|
|
1095
|
+
throw new GittermError("INPUT_NOT_PENDING", "Input request is no longer pending");
|
|
1096
|
+
const runtime = directRuntime(run.workspace, fetchImpl);
|
|
1097
|
+
if (request.kind !== input.reply.type)
|
|
1098
|
+
throw new GittermError("BAD_REQUEST", "Reply type does not match the input request");
|
|
1099
|
+
if (input.reply.type === "question" && ("answers" in input.reply ? "reject" in input.reply : input.reply.reject !== true)) {
|
|
1100
|
+
throw new GittermError("BAD_REQUEST", "Question replies require answers or an explicit rejection, not both");
|
|
1101
|
+
}
|
|
1102
|
+
let answers;
|
|
1103
|
+
if (request.kind === "question" && input.reply.type === "question" && "answers" in input.reply) {
|
|
1104
|
+
try {
|
|
1105
|
+
answers = questionAnswers(request, input.reply.answers);
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
throw new GittermError("BAD_REQUEST", error instanceof Error ? error.message : "Invalid answers");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
try {
|
|
1111
|
+
if (request.kind === "permission" && input.reply.type === "permission") {
|
|
1112
|
+
if (!["once", "always", "reject"].includes(input.reply.response))
|
|
1113
|
+
throw new GittermError("BAD_REQUEST", "Invalid permission response");
|
|
1114
|
+
await runtime.replyPermission(run.sessionId, request.id, input.reply.response);
|
|
1115
|
+
} else if (request.kind === "question") {
|
|
1116
|
+
if (answers)
|
|
1117
|
+
await runtime.replyQuestion(run.sessionId, request, answers);
|
|
1118
|
+
else
|
|
1119
|
+
await runtime.rejectQuestion(run.sessionId, request.id);
|
|
1120
|
+
}
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (error instanceof RuntimeHttpError && error.status === 404)
|
|
1123
|
+
throw new GittermError("INPUT_NOT_PENDING", "Input request was resolved by another responder");
|
|
1124
|
+
throw directError(error);
|
|
1125
|
+
}
|
|
1126
|
+
return get(current);
|
|
1127
|
+
}
|
|
1128
|
+
async function* watch(run, options) {
|
|
1129
|
+
assertRun(run);
|
|
1130
|
+
aborted(options?.signal);
|
|
1131
|
+
if (terminal(run.status)) {
|
|
1132
|
+
yield run;
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const controller = new AbortController;
|
|
1136
|
+
const onAbort = () => controller.abort();
|
|
1137
|
+
options?.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1138
|
+
const runtime = directRuntime(run.workspace, fetchImpl);
|
|
1139
|
+
let dirty = false;
|
|
1140
|
+
let wake;
|
|
1141
|
+
let sessionError = null;
|
|
1142
|
+
let streamError;
|
|
1143
|
+
const notify = () => {
|
|
1144
|
+
dirty = true;
|
|
1145
|
+
wake?.();
|
|
1146
|
+
wake = undefined;
|
|
1147
|
+
};
|
|
1148
|
+
const probe = setInterval(notify, 60000);
|
|
1149
|
+
const pump = (async () => {
|
|
1150
|
+
let backoff = 1000;
|
|
1151
|
+
while (!controller.signal.aborted) {
|
|
1152
|
+
try {
|
|
1153
|
+
for await (const event of runtime.subscribe(controller.signal)) {
|
|
1154
|
+
if (event.type === "connected") {
|
|
1155
|
+
backoff = 1000;
|
|
1156
|
+
notify();
|
|
1157
|
+
} else if (event.sessionId === run.sessionId) {
|
|
1158
|
+
if (event.type === "session.error")
|
|
1159
|
+
sessionError = event.message;
|
|
1160
|
+
notify();
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
if (controller.signal.aborted)
|
|
1165
|
+
return;
|
|
1166
|
+
if (error instanceof RuntimeHttpError && [400, 401, 403, 404].includes(error.status)) {
|
|
1167
|
+
streamError = error;
|
|
1168
|
+
notify();
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
await delay(backoff, controller.signal).catch(() => {});
|
|
1173
|
+
backoff = Math.min(backoff * 2, 30000);
|
|
1174
|
+
}
|
|
1175
|
+
})();
|
|
1176
|
+
let current = run;
|
|
1177
|
+
let previous;
|
|
1178
|
+
try {
|
|
1179
|
+
while (true) {
|
|
1180
|
+
aborted(options?.signal);
|
|
1181
|
+
if (!dirty)
|
|
1182
|
+
await abortable(new Promise((resolve) => {
|
|
1183
|
+
wake = resolve;
|
|
1184
|
+
}), controller.signal);
|
|
1185
|
+
dirty = false;
|
|
1186
|
+
if (streamError)
|
|
1187
|
+
throw directError(streamError);
|
|
1188
|
+
current = await abortable(get(current, sessionError, controller.signal), controller.signal);
|
|
1189
|
+
const key = JSON.stringify([current.status, current.pendingInputs]);
|
|
1190
|
+
if (key !== previous) {
|
|
1191
|
+
previous = key;
|
|
1192
|
+
yield current;
|
|
1193
|
+
}
|
|
1194
|
+
if (terminal(current.status))
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
} finally {
|
|
1198
|
+
clearInterval(probe);
|
|
1199
|
+
options?.signal?.removeEventListener("abort", onAbort);
|
|
1200
|
+
controller.abort();
|
|
1201
|
+
await pump;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return {
|
|
1205
|
+
get: (run) => get(run),
|
|
1206
|
+
watch,
|
|
1207
|
+
respond,
|
|
1208
|
+
...runHelpers({ watch, respond }),
|
|
1209
|
+
async messages(run) {
|
|
1210
|
+
assertRun(run);
|
|
1211
|
+
try {
|
|
1212
|
+
return (await directRuntime(run.workspace, fetchImpl).snapshot(run.sessionId, run.messageId)).messages;
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
throw directError(error);
|
|
1215
|
+
}
|
|
1216
|
+
},
|
|
1217
|
+
async cancel(run) {
|
|
1218
|
+
const current = await get(run);
|
|
1219
|
+
if (terminal(current.status))
|
|
1220
|
+
return { cancelled: current.status === "cancelled" };
|
|
1221
|
+
const runtime = directRuntime(run.workspace, fetchImpl);
|
|
1222
|
+
try {
|
|
1223
|
+
for (const request of current.pendingInputs) {
|
|
1224
|
+
if (request.kind === "permission")
|
|
1225
|
+
await runtime.replyPermission(run.sessionId, request.id, "reject");
|
|
1226
|
+
else
|
|
1227
|
+
await runtime.rejectQuestion(run.sessionId, request.id);
|
|
1228
|
+
}
|
|
1229
|
+
await runtime.abort(run.sessionId);
|
|
1230
|
+
return { cancelled: true };
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
throw directError(error);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/direct/client.ts
|
|
4
1239
|
import {
|
|
5
1240
|
createOpencodeClient as createOpencodeV2Client
|
|
6
1241
|
} from "@opencode-ai/sdk/v2";
|
|
@@ -22,9 +1257,9 @@ function resolveDirectImage(image) {
|
|
|
22
1257
|
return image?.trim() || DIRECT_OPENCODE_SERVER_IMAGE;
|
|
23
1258
|
}
|
|
24
1259
|
function directModelAuth(credential) {
|
|
25
|
-
if (credential.
|
|
26
|
-
if (!credential.refreshToken.trim())
|
|
27
|
-
throw new
|
|
1260
|
+
if (credential.source === "oauth") {
|
|
1261
|
+
if (typeof credential.refreshToken !== "string" || !credential.refreshToken.trim())
|
|
1262
|
+
throw new GittermError("MODEL_CREDENTIAL_INVALID", "OAuth refreshToken is required");
|
|
28
1263
|
if (credential.expiresAt != null && (!Number.isFinite(credential.expiresAt) || credential.expiresAt < 0)) {
|
|
29
1264
|
throw new Error("OAuth expiresAt must be a non-negative Unix epoch time in milliseconds");
|
|
30
1265
|
}
|
|
@@ -37,8 +1272,9 @@ function directModelAuth(credential) {
|
|
|
37
1272
|
...credential.enterpriseUrl ? { enterpriseUrl: credential.enterpriseUrl } : {}
|
|
38
1273
|
};
|
|
39
1274
|
}
|
|
40
|
-
if (!credential.apiKey.trim())
|
|
41
|
-
throw new
|
|
1275
|
+
if (credential.source !== "apiKey" || typeof credential.apiKey !== "string" || !credential.apiKey.trim()) {
|
|
1276
|
+
throw new GittermError("MODEL_CREDENTIAL_INVALID", "Inline model credential apiKey is required");
|
|
1277
|
+
}
|
|
42
1278
|
return {
|
|
43
1279
|
type: "api",
|
|
44
1280
|
key: credential.apiKey,
|
|
@@ -99,8 +1335,15 @@ function buildDirectProvisioningPlan(input) {
|
|
|
99
1335
|
}
|
|
100
1336
|
}
|
|
101
1337
|
const credentials = new Map;
|
|
102
|
-
|
|
103
|
-
|
|
1338
|
+
if (input.models?.default && !/^[^/]+\/.+$/.test(input.models.default)) {
|
|
1339
|
+
throw new GittermError("BAD_REQUEST", "Model must use provider/model format");
|
|
1340
|
+
}
|
|
1341
|
+
if (input.models?.inherit && input.models.inherit !== "none") {
|
|
1342
|
+
throw new GittermError("MODEL_CREDENTIAL_INVALID", "Direct mode cannot inherit dashboard credentials");
|
|
1343
|
+
}
|
|
1344
|
+
for (const [key, source] of Object.entries(input.models?.providers ?? {})) {
|
|
1345
|
+
const credential = { ...source, providerName: key };
|
|
1346
|
+
const providerName = key.trim();
|
|
104
1347
|
if (!providerName)
|
|
105
1348
|
throw new Error("Model credential providerName is required");
|
|
106
1349
|
if (credentials.has(providerName)) {
|
|
@@ -117,6 +1360,7 @@ function buildDirectProvisioningPlan(input) {
|
|
|
117
1360
|
const config = {
|
|
118
1361
|
$schema: "https://opencode.ai/config.json",
|
|
119
1362
|
...input.opencode?.config,
|
|
1363
|
+
...input.models?.default ? { model: input.models.default } : {},
|
|
120
1364
|
username: "Gitterm direct",
|
|
121
1365
|
permission: {
|
|
122
1366
|
external_directory: "allow",
|
|
@@ -257,8 +1501,8 @@ function basicAuthHeader(password) {
|
|
|
257
1501
|
return `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
|
|
258
1502
|
}
|
|
259
1503
|
async function waitForDirectRuntime(runtime, timeoutMs = 60000) {
|
|
260
|
-
const
|
|
261
|
-
while (Date.now() <
|
|
1504
|
+
const deadline2 = Date.now() + timeoutMs;
|
|
1505
|
+
while (Date.now() < deadline2) {
|
|
262
1506
|
const controller = new AbortController;
|
|
263
1507
|
const timer = setTimeout(() => controller.abort(), 5000);
|
|
264
1508
|
const ready = await fetch(runtime.url, {
|
|
@@ -810,8 +2054,8 @@ function createExeDevDirectProvider(config) {
|
|
|
810
2054
|
return token;
|
|
811
2055
|
}
|
|
812
2056
|
async function waitUntilRunning(vmName) {
|
|
813
|
-
const
|
|
814
|
-
while (Date.now() <
|
|
2057
|
+
const deadline2 = Date.now() + 60000;
|
|
2058
|
+
while (Date.now() < deadline2) {
|
|
815
2059
|
const status = await execute(`ls ${vmName}`).then(resultStatus).catch(() => "pending");
|
|
816
2060
|
if (status === "running")
|
|
817
2061
|
return;
|
|
@@ -885,9 +2129,9 @@ function createExeDevDirectProvider(config) {
|
|
|
885
2129
|
await startRuntime(handle);
|
|
886
2130
|
await execute(`share port ${vmName} ${handle.serve.port}`);
|
|
887
2131
|
await execute(`share set-private ${vmName}`);
|
|
888
|
-
const
|
|
889
|
-
await waitForDirectRuntime(
|
|
890
|
-
return { externalId: serializeHandle2(handle), runtime:
|
|
2132
|
+
const directRuntime2 = await runtime(handle, input.password);
|
|
2133
|
+
await waitForDirectRuntime(directRuntime2);
|
|
2134
|
+
return { externalId: serializeHandle2(handle), runtime: directRuntime2 };
|
|
891
2135
|
} catch (error) {
|
|
892
2136
|
if (handle.owned) {
|
|
893
2137
|
await execute(`rm ${vmName}`).catch(() => {
|
|
@@ -922,9 +2166,9 @@ function createExeDevDirectProvider(config) {
|
|
|
922
2166
|
await execute(`share set-private ${handle.vmName}`);
|
|
923
2167
|
if (!workspace.runtime.password)
|
|
924
2168
|
throw new Error("exe.dev runtime password is missing");
|
|
925
|
-
const
|
|
926
|
-
await waitForDirectRuntime(
|
|
927
|
-
return
|
|
2169
|
+
const directRuntime2 = await runtime(handle, workspace.runtime.password);
|
|
2170
|
+
await waitForDirectRuntime(directRuntime2);
|
|
2171
|
+
return directRuntime2;
|
|
928
2172
|
},
|
|
929
2173
|
async terminate(workspace) {
|
|
930
2174
|
const handle = parseHandle3(workspace.externalId);
|
|
@@ -1100,8 +2344,8 @@ function createRailwayDirectProvider(config) {
|
|
|
1100
2344
|
};
|
|
1101
2345
|
const latestDeployment = async (serviceId) => (await serviceSnapshot(serviceId)).serviceInstance?.latestDeployment ?? undefined;
|
|
1102
2346
|
const waitForDeployment = async (serviceId, previousDeploymentId) => {
|
|
1103
|
-
const
|
|
1104
|
-
while (Date.now() <
|
|
2347
|
+
const deadline2 = Date.now() + DEPLOYMENT_TIMEOUT_MS;
|
|
2348
|
+
while (Date.now() < deadline2) {
|
|
1105
2349
|
const deployment = await latestDeployment(serviceId);
|
|
1106
2350
|
if (deployment && deployment.id !== previousDeploymentId) {
|
|
1107
2351
|
if (deployment.status === "SUCCESS")
|
|
@@ -1115,8 +2359,8 @@ function createRailwayDirectProvider(config) {
|
|
|
1115
2359
|
throw new Error("Timed out waiting for Railway deployment");
|
|
1116
2360
|
};
|
|
1117
2361
|
const waitForDeploymentRemoval = async (serviceId, deploymentId) => {
|
|
1118
|
-
const
|
|
1119
|
-
while (Date.now() <
|
|
2362
|
+
const deadline2 = Date.now() + DEPLOYMENT_TIMEOUT_MS;
|
|
2363
|
+
while (Date.now() < deadline2) {
|
|
1120
2364
|
const deployment = await latestDeployment(serviceId);
|
|
1121
2365
|
if (!deployment || deployment.id !== deploymentId || deployment.status === "REMOVED")
|
|
1122
2366
|
return;
|
|
@@ -1505,9 +2749,10 @@ function resolveProvider(provider) {
|
|
|
1505
2749
|
return createVercelDirectProvider(provider);
|
|
1506
2750
|
}
|
|
1507
2751
|
}
|
|
1508
|
-
function runtimeClient(workspace) {
|
|
2752
|
+
function runtimeClient(workspace, fetchImpl) {
|
|
1509
2753
|
const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
|
|
1510
|
-
return
|
|
2754
|
+
return createOpencodeClient2({
|
|
2755
|
+
fetch: fetchImpl,
|
|
1511
2756
|
baseUrl: workspace.runtime.url,
|
|
1512
2757
|
directory: workspace.runtime.directory,
|
|
1513
2758
|
headers: {
|
|
@@ -1516,9 +2761,12 @@ function runtimeClient(workspace) {
|
|
|
1516
2761
|
}
|
|
1517
2762
|
});
|
|
1518
2763
|
}
|
|
1519
|
-
function
|
|
2764
|
+
function createAuthClient(workspace, fetchImpl) {
|
|
2765
|
+
if (workspace.opencodeApi !== "v2")
|
|
2766
|
+
throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2; use inline credentials or a managed workspace with v1");
|
|
1520
2767
|
const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
|
|
1521
2768
|
return createOpencodeV2Client({
|
|
2769
|
+
fetch: fetchImpl,
|
|
1522
2770
|
baseUrl: workspace.runtime.url,
|
|
1523
2771
|
directory: workspace.runtime.directory,
|
|
1524
2772
|
headers: {
|
|
@@ -1553,17 +2801,6 @@ function authStatus(status) {
|
|
|
1553
2801
|
return { status: "failed", message: status.message, ...common };
|
|
1554
2802
|
return { status: status.status, ...common };
|
|
1555
2803
|
}
|
|
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
2804
|
function pollTiming(wait, fallbackTimeoutMs) {
|
|
1568
2805
|
const timeoutMs = wait.timeoutMs ?? fallbackTimeoutMs;
|
|
1569
2806
|
const pollIntervalMs = wait.pollIntervalMs ?? 1000;
|
|
@@ -1575,15 +2812,6 @@ function pollTiming(wait, fallbackTimeoutMs) {
|
|
|
1575
2812
|
}
|
|
1576
2813
|
return { timeoutMs, pollIntervalMs };
|
|
1577
2814
|
}
|
|
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
2815
|
function validateEnvironmentVariables(values) {
|
|
1588
2816
|
for (const key of Object.keys(values)) {
|
|
1589
2817
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
@@ -1618,15 +2846,18 @@ function setupRunner(commands) {
|
|
|
1618
2846
|
}
|
|
1619
2847
|
function createDirectGittermClient(options) {
|
|
1620
2848
|
const provider = resolveProvider(options.provider);
|
|
2849
|
+
const authClient = (workspace) => createAuthClient(workspace, options.fetch);
|
|
2850
|
+
const http = (workspace) => createRuntimeHttp({
|
|
2851
|
+
url: workspace.runtime.url,
|
|
2852
|
+
directory: workspace.runtime.directory,
|
|
2853
|
+
password: workspace.runtime.password ?? null,
|
|
2854
|
+
api: workspace.opencodeApi,
|
|
2855
|
+
headers: workspace.runtime.headers,
|
|
2856
|
+
fetch: options.fetch
|
|
2857
|
+
});
|
|
1621
2858
|
function assertWorkspace(workspace) {
|
|
1622
2859
|
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}`);
|
|
2860
|
+
throw new GittermError("BAD_REQUEST", `Workspace belongs to ${workspace.provider}, not ${provider.name}`);
|
|
1630
2861
|
}
|
|
1631
2862
|
}
|
|
1632
2863
|
function assertAuthAttempt(attempt, workspace) {
|
|
@@ -1644,55 +2875,29 @@ function createDirectGittermClient(options) {
|
|
|
1644
2875
|
throw new Error(errorMessage(result.error));
|
|
1645
2876
|
return authStatus(result.data.data);
|
|
1646
2877
|
}
|
|
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
2878
|
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
|
|
2879
|
+
const result = await http(workspace).json(workspace.opencodeApi === "v2" ? "/api/pty" : `/pty?directory=${encodeURIComponent(workspace.runtime.directory)}`, {
|
|
2880
|
+
method: "POST",
|
|
2881
|
+
json: { command: "bash", args: ["-lc", command], cwd: workspace.runtime.directory, title }
|
|
1683
2882
|
});
|
|
1684
|
-
|
|
1685
|
-
throw new Error(errorMessage(result.error));
|
|
1686
|
-
return result.data;
|
|
2883
|
+
return result;
|
|
1687
2884
|
}
|
|
1688
2885
|
async function setupFile(workspace, name) {
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2886
|
+
try {
|
|
2887
|
+
if (workspace.opencodeApi === "v2") {
|
|
2888
|
+
return (await (await http(workspace).send(`/api/fs/read/${SETUP_DIR}/${name}`)).text()).trim();
|
|
2889
|
+
}
|
|
2890
|
+
const result = await runtimeClient(workspace, options.fetch).file.read({
|
|
2891
|
+
query: { directory: workspace.runtime.directory, path: `${SETUP_DIR}/${name}` }
|
|
2892
|
+
});
|
|
2893
|
+
if (result.error || !result.data || result.data.type !== "text")
|
|
2894
|
+
return null;
|
|
2895
|
+
return result.data.content.trim();
|
|
2896
|
+
} catch (error) {
|
|
2897
|
+
if (error instanceof RuntimeHttpError && error.status === 404)
|
|
2898
|
+
return null;
|
|
2899
|
+
throw directError(error);
|
|
2900
|
+
}
|
|
1696
2901
|
}
|
|
1697
2902
|
async function getSetupStatus(workspace) {
|
|
1698
2903
|
assertWorkspace(workspace);
|
|
@@ -1739,7 +2944,14 @@ function createDirectGittermClient(options) {
|
|
|
1739
2944
|
if (!providerName) {
|
|
1740
2945
|
throw new Error("Model credential providerName is required");
|
|
1741
2946
|
}
|
|
1742
|
-
|
|
2947
|
+
if (workspace.opencodeApi === "v2") {
|
|
2948
|
+
if (credential.source !== "apiKey")
|
|
2949
|
+
throw new GittermError("BAD_REQUEST", "Use connectOAuth() for v2 credential rotation, or inject an OAuth bundle when creating the workspace");
|
|
2950
|
+
directModelAuth(credential);
|
|
2951
|
+
await http(workspace).send(`/api/integration/${encodeURIComponent(providerName)}/connect/key`, { method: "POST", json: { key: credential.apiKey } });
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
const result = await runtimeClient(workspace, options.fetch).auth.set({
|
|
1743
2955
|
path: { id: providerName },
|
|
1744
2956
|
query: { directory: workspace.runtime.directory },
|
|
1745
2957
|
body: directModelAuth(credential)
|
|
@@ -1814,8 +3026,8 @@ function createDirectGittermClient(options) {
|
|
|
1814
3026
|
async wait(attempt, workspace, wait = {}) {
|
|
1815
3027
|
assertAuthAttempt(attempt, workspace);
|
|
1816
3028
|
const { timeoutMs, pollIntervalMs } = pollTiming(wait, Math.max(0, attempt.expiresAt - Date.now()));
|
|
1817
|
-
const
|
|
1818
|
-
while (Date.now() <=
|
|
3029
|
+
const deadline2 = Date.now() + timeoutMs;
|
|
3030
|
+
while (Date.now() <= deadline2) {
|
|
1819
3031
|
const status = await getAuthStatus(attempt, workspace);
|
|
1820
3032
|
if (status.status === "complete")
|
|
1821
3033
|
return status;
|
|
@@ -1823,7 +3035,7 @@ function createDirectGittermClient(options) {
|
|
|
1823
3035
|
throw new Error(status.message);
|
|
1824
3036
|
if (status.status === "expired")
|
|
1825
3037
|
throw new Error("OAuth attempt expired");
|
|
1826
|
-
const remaining =
|
|
3038
|
+
const remaining = deadline2 - Date.now();
|
|
1827
3039
|
if (remaining <= 0)
|
|
1828
3040
|
break;
|
|
1829
3041
|
await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
|
|
@@ -1841,6 +3053,8 @@ function createDirectGittermClient(options) {
|
|
|
1841
3053
|
},
|
|
1842
3054
|
workspaces: {
|
|
1843
3055
|
async create(input = {}) {
|
|
3056
|
+
if (input.opencode?.api && !["v1", "v2"].includes(input.opencode.api))
|
|
3057
|
+
throw new GittermError("BAD_REQUEST", "opencode.api must be v1 or v2");
|
|
1844
3058
|
const lifecycle = input.lifecycle ?? provider.capabilities.recommendedLifecycle;
|
|
1845
3059
|
if (lifecycle === "persistent" && provider.capabilities.persistence === "unsupported") {
|
|
1846
3060
|
throw new Error(`${provider.name} does not support persistent direct workspaces`);
|
|
@@ -1857,6 +3071,7 @@ function createDirectGittermClient(options) {
|
|
|
1857
3071
|
status: "running",
|
|
1858
3072
|
lifecycle,
|
|
1859
3073
|
runtime: created.runtime,
|
|
3074
|
+
opencodeApi: input.opencode?.api ?? "v1",
|
|
1860
3075
|
setup: provisioning.setup.afterAgent.length ? "after_agent" : provisioning.setup.beforeAgent.length ? "before_agent_complete" : "not_requested",
|
|
1861
3076
|
createdAt: new Date().toISOString()
|
|
1862
3077
|
};
|
|
@@ -1911,7 +3126,7 @@ function createDirectGittermClient(options) {
|
|
|
1911
3126
|
setupStatus: getSetupStatus,
|
|
1912
3127
|
async waitForSetup(workspace, wait = {}) {
|
|
1913
3128
|
const { timeoutMs, pollIntervalMs } = pollTiming(wait, 10 * 60000);
|
|
1914
|
-
const
|
|
3129
|
+
const deadline2 = Date.now() + timeoutMs;
|
|
1915
3130
|
while (true) {
|
|
1916
3131
|
const status = await getSetupStatus(workspace);
|
|
1917
3132
|
if (status.status === "not_requested" || status.status === "succeeded")
|
|
@@ -1920,7 +3135,7 @@ function createDirectGittermClient(options) {
|
|
|
1920
3135
|
throw new Error(`Workspace setup failed${status.exitCode == null ? "" : ` with exit code ${status.exitCode}`}${status.log ? `
|
|
1921
3136
|
${status.log}` : ""}`);
|
|
1922
3137
|
}
|
|
1923
|
-
if (Date.now() >=
|
|
3138
|
+
if (Date.now() >= deadline2) {
|
|
1924
3139
|
throw new Error(`Workspace setup timed out after ${timeoutMs}ms`);
|
|
1925
3140
|
}
|
|
1926
3141
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
@@ -1928,89 +3143,75 @@ ${status.log}` : ""}`);
|
|
|
1928
3143
|
}
|
|
1929
3144
|
},
|
|
1930
3145
|
runs: {
|
|
3146
|
+
...directRunOperations(assertWorkspace, options.fetch),
|
|
1931
3147
|
async create(input) {
|
|
1932
3148
|
assertWorkspace(input.workspace);
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
if (!
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
3149
|
+
aborted(input.signal);
|
|
3150
|
+
if (input.workspace.status !== "running")
|
|
3151
|
+
throw new GittermError("WORKSPACE_NOT_RUNNING", "Resume the workspace before creating a run");
|
|
3152
|
+
if (!input.prompt.trim())
|
|
3153
|
+
throw new GittermError("BAD_REQUEST", "Prompt is required");
|
|
3154
|
+
if (input.model && !/^[^/]+\/.+$/.test(input.model))
|
|
3155
|
+
throw new GittermError("BAD_REQUEST", "Model must use provider/model format");
|
|
3156
|
+
if (input.waitForSetup) {
|
|
3157
|
+
const deadline2 = Date.now() + (input.setupTimeoutMs ?? 10 * 60000);
|
|
3158
|
+
while (true) {
|
|
3159
|
+
aborted(input.signal);
|
|
3160
|
+
const status = await getSetupStatus(input.workspace);
|
|
3161
|
+
if (status.status === "succeeded" || status.status === "not_requested")
|
|
3162
|
+
break;
|
|
3163
|
+
if (status.status === "failed")
|
|
3164
|
+
throw new GittermError("BAD_REQUEST", `Workspace setup failed: ${status.log ?? ""}`);
|
|
3165
|
+
if (Date.now() >= deadline2)
|
|
3166
|
+
throw new GittermError("TIMEOUT", "Workspace setup timed out");
|
|
3167
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
const runtime = directRuntime(input.workspace, options.fetch);
|
|
3171
|
+
const previous = input.context?.type === "continue" ? input.context.run : undefined;
|
|
3172
|
+
if (previous && (previous.workspaceId !== input.workspace.id || !terminal(previous.status))) {
|
|
3173
|
+
throw new GittermError("CONFLICT", "Continue a terminal run from the same workspace");
|
|
1945
3174
|
}
|
|
3175
|
+
let sessionId = previous?.sessionId;
|
|
3176
|
+
let title = input.title ?? "Agent run";
|
|
3177
|
+
const now = new Date().toISOString();
|
|
1946
3178
|
const messageId = `msg_${randomUUID().replaceAll("-", "")}`;
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
agent: input.agent,
|
|
1954
|
-
model: modelParts(input.model)
|
|
3179
|
+
try {
|
|
3180
|
+
aborted(input.signal);
|
|
3181
|
+
if (!sessionId) {
|
|
3182
|
+
const session2 = await runtime.createSession(input);
|
|
3183
|
+
sessionId = session2.id;
|
|
3184
|
+
title = session2.title;
|
|
1955
3185
|
}
|
|
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
|
-
|
|
3186
|
+
aborted(input.signal);
|
|
3187
|
+
await runtime.prompt({
|
|
3188
|
+
sessionId,
|
|
3189
|
+
messageId,
|
|
3190
|
+
prompt: input.prompt,
|
|
3191
|
+
agent: input.agent,
|
|
3192
|
+
model: input.model
|
|
3193
|
+
});
|
|
3194
|
+
return {
|
|
3195
|
+
id: randomUUID(),
|
|
3196
|
+
workspaceId: input.workspace.id,
|
|
3197
|
+
workspace: input.workspace,
|
|
3198
|
+
sessionId,
|
|
3199
|
+
messageId,
|
|
3200
|
+
title,
|
|
3201
|
+
status: "running",
|
|
3202
|
+
error: null,
|
|
3203
|
+
finalText: null,
|
|
3204
|
+
pendingInputs: [],
|
|
3205
|
+
context: previous ? { type: "continued", runId: previous.id } : { type: "isolated" },
|
|
3206
|
+
createdAt: now,
|
|
3207
|
+
submittedAt: now,
|
|
3208
|
+
completedAt: null
|
|
3209
|
+
};
|
|
3210
|
+
} catch (error) {
|
|
3211
|
+
if (sessionId && !previous)
|
|
3212
|
+
await runtime.deleteSession(sessionId).catch(() => {});
|
|
3213
|
+
throw directError(error);
|
|
1984
3214
|
}
|
|
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
3215
|
}
|
|
2015
3216
|
}
|
|
2016
3217
|
};
|