@gitterm/sdk 0.1.3 → 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.
@@ -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.type === "oauth") {
26
- if (!credential.refreshToken.trim())
27
- throw new Error("OAuth refreshToken is required");
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 Error("Model credential apiKey is required");
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
- for (const credential of input.modelCredentials ?? []) {
103
- const providerName = credential.providerName.trim();
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 deadline = Date.now() + timeoutMs;
261
- while (Date.now() < deadline) {
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 deadline = Date.now() + 60000;
814
- while (Date.now() < deadline) {
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 directRuntime = await runtime(handle, input.password);
889
- await waitForDirectRuntime(directRuntime);
890
- return { externalId: serializeHandle2(handle), runtime: directRuntime };
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 directRuntime = await runtime(handle, workspace.runtime.password);
926
- await waitForDirectRuntime(directRuntime);
927
- return directRuntime;
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 deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
1104
- while (Date.now() < deadline) {
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 deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
1119
- while (Date.now() < deadline) {
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 createOpencodeClient({
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 authClient(workspace) {
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 Error(`Workspace belongs to ${workspace.provider}, not ${provider.name}`);
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 authClient(workspace).pty.create({
1678
- command: "bash",
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
- if (result.error || !result.data)
1685
- throw new Error(errorMessage(result.error));
1686
- return result.data;
2880
+ return result;
1687
2881
  }
1688
2882
  async function setupFile(workspace, name) {
1689
- const result = await authClient(workspace).file.read({
1690
- directory: workspace.runtime.directory,
1691
- path: `${SETUP_DIR}/${name}`
1692
- });
1693
- if (result.error || !result.data || result.data.type !== "text")
1694
- return null;
1695
- return result.data.content.trim();
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
- const result = await runtimeClient(workspace).auth.set({
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 deadline = Date.now() + timeoutMs;
1818
- while (Date.now() <= deadline) {
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 = deadline - Date.now();
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 deadline = Date.now() + timeoutMs;
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() >= deadline) {
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
- const client = runtimeClient(input.workspace);
1934
- let sessionId = input.sessionId;
1935
- let title = input.title ?? "Agent run";
1936
- if (!sessionId) {
1937
- const created = await client.session.create({
1938
- body: input.title ? { title: input.title } : undefined,
1939
- query: { directory: input.workspace.runtime.directory }
1940
- });
1941
- if (created.error || !created.data)
1942
- throw new Error(errorMessage(created.error));
1943
- sessionId = created.data.id;
1944
- title = created.data.title;
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
- const prompted = await client.session.promptAsync({
1948
- path: { id: sessionId },
1949
- query: { directory: input.workspace.runtime.directory },
1950
- body: {
1951
- messageID: messageId,
1952
- parts: [{ type: "text", text: input.prompt }],
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
- if (prompted.error)
1958
- throw new Error(errorMessage(prompted.error));
1959
- return {
1960
- id: randomUUID(),
1961
- workspaceId: input.workspace.id,
1962
- sessionId,
1963
- messageId,
1964
- title,
1965
- status: "running",
1966
- error: null,
1967
- finalText: null,
1968
- submittedAt: new Date().toISOString()
1969
- };
1970
- },
1971
- get: getRun,
1972
- async wait(run, workspace, wait = {}) {
1973
- const { timeoutMs, pollIntervalMs } = pollTiming(wait, 10 * 60000);
1974
- const deadline = Date.now() + timeoutMs;
1975
- let current = run;
1976
- while (Date.now() < deadline) {
1977
- current = await getRun(current, workspace);
1978
- if (!["running", "retrying"].includes(current.status))
1979
- return current;
1980
- const remaining = deadline - Date.now();
1981
- if (remaining <= 0)
1982
- break;
1983
- await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
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
  };