@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.
@@ -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.type === "oauth") {
26
- if (!credential.refreshToken.trim())
27
- throw new Error("OAuth refreshToken is required");
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 Error("Model credential apiKey is required");
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
- for (const credential of input.modelCredentials ?? []) {
103
- const providerName = credential.providerName.trim();
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 deadline = Date.now() + timeoutMs;
261
- while (Date.now() < deadline) {
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 deadline = Date.now() + 60000;
814
- while (Date.now() < deadline) {
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 directRuntime = await runtime(handle, input.password);
889
- await waitForDirectRuntime(directRuntime);
890
- return { externalId: serializeHandle2(handle), runtime: directRuntime };
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 directRuntime = await runtime(handle, workspace.runtime.password);
926
- await waitForDirectRuntime(directRuntime);
927
- return directRuntime;
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 deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
1104
- while (Date.now() < deadline) {
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 deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
1119
- while (Date.now() < deadline) {
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 createOpencodeClient({
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 authClient(workspace) {
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 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}`);
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 authClient(workspace).pty.create({
1678
- command: "bash",
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
- if (result.error || !result.data)
1685
- throw new Error(errorMessage(result.error));
1686
- return result.data;
2883
+ return result;
1687
2884
  }
1688
2885
  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();
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
- const result = await runtimeClient(workspace).auth.set({
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 deadline = Date.now() + timeoutMs;
1818
- while (Date.now() <= deadline) {
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 = deadline - Date.now();
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 deadline = Date.now() + timeoutMs;
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() >= deadline) {
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
- 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;
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
- 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)
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
- 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)));
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
  };