@dudousxd/nestjs-agent-opencode 0.0.0-stage → 0.1.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.
@@ -0,0 +1,3109 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/engine.ts
5
+ import { Logger as Logger3 } from "@nestjs/common";
6
+
7
+ // src/host.ts
8
+ var InMemoryOpenCodeSessionStore = class {
9
+ static {
10
+ __name(this, "InMemoryOpenCodeSessionStore");
11
+ }
12
+ refs = /* @__PURE__ */ new Map();
13
+ async get(threadId) {
14
+ return this.refs.get(threadId) ?? null;
15
+ }
16
+ async set(threadId, ref) {
17
+ this.refs.set(threadId, ref);
18
+ }
19
+ };
20
+
21
+ // src/mcp.controller.ts
22
+ import { Body, Controller, Delete, Get, Inject, Post, Req, Res } from "@nestjs/common";
23
+
24
+ // src/tokens.ts
25
+ var OPENCODE_HOST = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-opencode:host");
26
+ var OPENCODE_SESSIONS = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-opencode:sessions");
27
+ var OPENCODE_OPTIONS = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-opencode:options");
28
+ var OPENCODE_TURNS = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-opencode:turns");
29
+ var OPENCODE_MCP_ENDPOINT = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-opencode:mcp-endpoint");
30
+ var OPENCODE_TOOLS_TOKENS = /* @__PURE__ */ Symbol.for("@dudousxd/nestjs-agent-opencode:tools-tokens");
31
+
32
+ // src/mcp.controller.ts
33
+ function _ts_decorate(decorators, target, key, desc) {
34
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
35
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
36
+ r = Reflect.decorate(decorators, target, key, desc);
37
+ } else {
38
+ for (var i = decorators.length - 1; i >= 0; i--) {
39
+ if (d = decorators[i]) {
40
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
41
+ }
42
+ }
43
+ }
44
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
45
+ }
46
+ __name(_ts_decorate, "_ts_decorate");
47
+ function _ts_metadata(metadataKey, metadataValue) {
48
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
49
+ return Reflect.metadata(metadataKey, metadataValue);
50
+ }
51
+ }
52
+ __name(_ts_metadata, "_ts_metadata");
53
+ function _ts_param(paramIndex, decorator) {
54
+ return function(target, key) {
55
+ decorator(target, key, paramIndex);
56
+ };
57
+ }
58
+ __name(_ts_param, "_ts_param");
59
+ function notAllowed(res) {
60
+ res.statusCode = 405;
61
+ res.setHeader("allow", "POST");
62
+ res.setHeader("content-type", "application/json");
63
+ res.end(JSON.stringify({
64
+ error: "method not allowed"
65
+ }));
66
+ }
67
+ __name(notAllowed, "notAllowed");
68
+ var OpenCodeMcpController = class {
69
+ static {
70
+ __name(this, "OpenCodeMcpController");
71
+ }
72
+ endpoint;
73
+ constructor(endpoint) {
74
+ this.endpoint = endpoint;
75
+ }
76
+ async post(req, res, body) {
77
+ await this.endpoint.handle(req, res, body);
78
+ }
79
+ get(res) {
80
+ notAllowed(res);
81
+ }
82
+ delete(res) {
83
+ notAllowed(res);
84
+ }
85
+ };
86
+ _ts_decorate([
87
+ Post(),
88
+ _ts_param(0, Req()),
89
+ _ts_param(1, Res()),
90
+ _ts_param(2, Body()),
91
+ _ts_metadata("design:type", Function),
92
+ _ts_metadata("design:paramtypes", [
93
+ typeof IncomingMessage === "undefined" ? Object : IncomingMessage,
94
+ typeof ServerResponse === "undefined" ? Object : ServerResponse,
95
+ Object
96
+ ]),
97
+ _ts_metadata("design:returntype", Promise)
98
+ ], OpenCodeMcpController.prototype, "post", null);
99
+ _ts_decorate([
100
+ Get(),
101
+ _ts_param(0, Res()),
102
+ _ts_metadata("design:type", Function),
103
+ _ts_metadata("design:paramtypes", [
104
+ typeof ServerResponse === "undefined" ? Object : ServerResponse
105
+ ]),
106
+ _ts_metadata("design:returntype", void 0)
107
+ ], OpenCodeMcpController.prototype, "get", null);
108
+ _ts_decorate([
109
+ Delete(),
110
+ _ts_param(0, Res()),
111
+ _ts_metadata("design:type", Function),
112
+ _ts_metadata("design:paramtypes", [
113
+ typeof ServerResponse === "undefined" ? Object : ServerResponse
114
+ ]),
115
+ _ts_metadata("design:returntype", void 0)
116
+ ], OpenCodeMcpController.prototype, "delete", null);
117
+ OpenCodeMcpController = _ts_decorate([
118
+ Controller("opencode/mcp"),
119
+ _ts_param(0, Inject(OPENCODE_MCP_ENDPOINT)),
120
+ _ts_metadata("design:type", Function),
121
+ _ts_metadata("design:paramtypes", [
122
+ typeof OpenCodeMcpEndpoint === "undefined" ? Object : OpenCodeMcpEndpoint
123
+ ])
124
+ ], OpenCodeMcpController);
125
+
126
+ // src/mcp.ts
127
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
128
+ import { REMEMBER_TOOL_DESCRIPTION, REMEMBER_TOOL_NAME, ToolNotFoundError, rememberInputSchema, schemaExtensionOf } from "@dudousxd/nestjs-agent-core";
129
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
130
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
131
+ import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
132
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
133
+
134
+ // src/turn.ts
135
+ import { encodeStreamEvent, settleElicitation } from "@dudousxd/nestjs-agent-core";
136
+ import { ConflictException } from "@nestjs/common";
137
+
138
+ // src/forms.ts
139
+ import { validateElicitationAnswer } from "@dudousxd/nestjs-agent-core";
140
+ function toQuestion(field) {
141
+ const required = field.required === true ? {
142
+ required: true
143
+ } : {};
144
+ const base = {
145
+ id: field.key,
146
+ prompt: field.title || field.key,
147
+ ...field.description && field.description !== field.title ? {
148
+ description: field.description
149
+ } : {}
150
+ };
151
+ if (field.options !== void 0 && field.options.length > 0) {
152
+ return {
153
+ ...base,
154
+ options: field.options.map((o) => ({
155
+ value: String(o.value),
156
+ label: String(o.label ?? o.value)
157
+ })),
158
+ input: {
159
+ type: "select",
160
+ ...required
161
+ },
162
+ ...field.type === "array" ? {
163
+ multiple: true
164
+ } : {},
165
+ ...field.custom === true ? {
166
+ allowFreeText: true
167
+ } : {}
168
+ };
169
+ }
170
+ const type = field.type === "number" || field.type === "integer" ? "number" : field.type === "boolean" ? "boolean" : "text";
171
+ return {
172
+ ...base,
173
+ input: {
174
+ type,
175
+ ...required
176
+ }
177
+ };
178
+ }
179
+ __name(toQuestion, "toQuestion");
180
+ function formFields(form) {
181
+ return (form.fields ?? []).filter((f) => typeof f === "object" && f !== null && typeof f.key === "string");
182
+ }
183
+ __name(formFields, "formFields");
184
+ function toElicitation(id, form) {
185
+ const questions = formFields(form).map(toQuestion);
186
+ const first = questions[0];
187
+ const title = form.title;
188
+ const preamble = title && title !== "Questions" && title !== first?.prompt && title !== first?.description ? title : void 0;
189
+ return {
190
+ id,
191
+ source: "ask",
192
+ ...preamble ? {
193
+ preamble
194
+ } : {},
195
+ questions
196
+ };
197
+ }
198
+ __name(toElicitation, "toElicitation");
199
+ var FormAnswerError = class extends Error {
200
+ static {
201
+ __name(this, "FormAnswerError");
202
+ }
203
+ };
204
+ function toFormAnswer(form, answers = {}) {
205
+ const out = {};
206
+ for (const field of formFields(form)) {
207
+ const question = toQuestion(field);
208
+ const values = answers[field.key] ?? question.defaults ?? [];
209
+ const problem = validateElicitationAnswer(question, values);
210
+ if (problem) throw new FormAnswerError(`answers["${field.key}"] ${problem}`);
211
+ const present = values.filter((v) => v !== "");
212
+ const [value] = present;
213
+ if (value === void 0) continue;
214
+ if (question.multiple) out[field.key] = present;
215
+ else if (question.input?.type === "number") out[field.key] = Number(value);
216
+ else if (question.input?.type === "boolean") out[field.key] = value === "true";
217
+ else out[field.key] = value;
218
+ }
219
+ return out;
220
+ }
221
+ __name(toFormAnswer, "toFormAnswer");
222
+
223
+ // src/turn.ts
224
+ var ASK_TOOL_NAME = "ask";
225
+ var FAILED_STATUSES = /* @__PURE__ */ new Set([
226
+ "error",
227
+ "failed",
228
+ "denied",
229
+ "cancelled"
230
+ ]);
231
+ function emptySegment() {
232
+ return {
233
+ text: "",
234
+ reasoning: "",
235
+ reasoningMs: 0,
236
+ calls: [],
237
+ results: [],
238
+ ui: []
239
+ };
240
+ }
241
+ __name(emptySegment, "emptySegment");
242
+ function isDecision(reply) {
243
+ return typeof reply.approved === "boolean";
244
+ }
245
+ __name(isDecision, "isDecision");
246
+ var OpenCodeReplyMismatchError = class extends ConflictException {
247
+ static {
248
+ __name(this, "OpenCodeReplyMismatchError");
249
+ }
250
+ runId;
251
+ toolCallId;
252
+ constructor(runId, toolCallId) {
253
+ super(`Tool call "${toolCallId}" on run "${runId}" is waiting for an approve/reject, not for answers`), this.runId = runId, this.toolCallId = toolCallId;
254
+ this.name = "OpenCodeReplyMismatchError";
255
+ }
256
+ };
257
+ function permissionArgs(req) {
258
+ const metadata = req.metadata ?? {};
259
+ const nested = metadata.input ?? metadata.args;
260
+ if (nested !== null && typeof nested === "object") return nested;
261
+ return metadata;
262
+ }
263
+ __name(permissionArgs, "permissionArgs");
264
+ var HIDDEN_TOOLS = /* @__PURE__ */ new Set([
265
+ "question"
266
+ ]);
267
+ function errorText(error, fallback) {
268
+ if (typeof error === "string" && error.length > 0) return error;
269
+ const message = error?.message;
270
+ return typeof message === "string" && message.length > 0 ? message : fallback;
271
+ }
272
+ __name(errorText, "errorText");
273
+ var OpenCodeTurn = class {
274
+ static {
275
+ __name(this, "OpenCodeTurn");
276
+ }
277
+ a;
278
+ /** Serializes the handling of events, so frames and rows land in the order OpenCode sent them. */
279
+ chain = Promise.resolve();
280
+ segment = emptySegment();
281
+ tools = /* @__PURE__ */ new Map();
282
+ /** Results already persisted per message, so a late settlement adds to them rather than replacing. */
283
+ messageResults = /* @__PURE__ */ new Map();
284
+ asked = /* @__PURE__ */ new Set();
285
+ /** Calls of {@link HIDDEN_TOOLS}, by id. */
286
+ hidden = /* @__PURE__ */ new Set();
287
+ milestones = [];
288
+ waiting;
289
+ usage = {
290
+ inputTokens: 0,
291
+ outputTokens: 0
292
+ };
293
+ costUsd;
294
+ stepOpen = false;
295
+ sawText = false;
296
+ separator = false;
297
+ reasoningSince;
298
+ wroteMessage = false;
299
+ ended = false;
300
+ stepError;
301
+ constructor(a) {
302
+ this.a = a;
303
+ }
304
+ get sessionId() {
305
+ return this.a.sessionId;
306
+ }
307
+ get finished() {
308
+ return this.ended;
309
+ }
310
+ /** Feed one of the session's events. */
311
+ handle(event) {
312
+ this.enqueue(() => this.onEvent(event));
313
+ }
314
+ /** End the turn as failed without an event (the prompt was refused, the stream is gone). */
315
+ fail(error) {
316
+ this.enqueue(() => this.end({
317
+ status: "failed",
318
+ error
319
+ }));
320
+ }
321
+ /**
322
+ * A component a tool pushed (`ctx.emitUi`, through the MCP surface): streamed as a `ui` frame and
323
+ * persisted on the message being written, in the order it arrived. Pushing the same id again
324
+ * replaces it.
325
+ */
326
+ pushUi(component) {
327
+ return new Promise((resolve, reject) => {
328
+ this.chain = this.chain.then(async () => {
329
+ await this.step();
330
+ const ui = this.segment.ui;
331
+ const at = ui.findIndex((c) => c.id === component.id);
332
+ if (at >= 0) ui[at] = component;
333
+ else ui.push(component);
334
+ await this.write({
335
+ kind: "ui",
336
+ ...component
337
+ });
338
+ }).then(resolve, reject);
339
+ });
340
+ }
341
+ /** The next milestone: one already reached, or the next one to come. */
342
+ next() {
343
+ const ready = this.milestones.shift();
344
+ if (ready !== void 0) return Promise.resolve(ready);
345
+ return new Promise((resolve) => {
346
+ this.waiting = resolve;
347
+ });
348
+ }
349
+ /** Whether a milestone is waiting to be taken. */
350
+ hasMilestone() {
351
+ return this.milestones.length > 0;
352
+ }
353
+ /**
354
+ * Requests OpenCode raised while nobody listened (this process started after them, or the event
355
+ * stream dropped): its open permissions and forms, handled as if their events had just arrived.
356
+ * One already recorded in the store is reported without being recorded or streamed again.
357
+ */
358
+ async catchUp() {
359
+ const { client, sessionId } = this.a;
360
+ const permissions = await client.permission.list?.({
361
+ sessionID: sessionId
362
+ }) ?? [];
363
+ const forms = await client.session.form.list?.({
364
+ sessionID: sessionId
365
+ }) ?? [];
366
+ await new Promise((resolve) => {
367
+ this.enqueue(async () => {
368
+ for (const p of permissions) await this.onPermission(p, true);
369
+ for (const f of forms) await this.onForm(f, true);
370
+ resolve();
371
+ });
372
+ });
373
+ }
374
+ /**
375
+ * Hand a person's answer to what the turn asked, and let OpenCode go on. `tellOpenCode: false`
376
+ * only settles the call (stream and store): the request died with a restarted server, and the
377
+ * new session is told the answer in its prompt instead.
378
+ */
379
+ decide(ask, reply, opts = {}) {
380
+ const tell = opts.tellOpenCode ?? true;
381
+ return new Promise((resolve, reject) => {
382
+ this.chain = this.chain.then(() => ask.kind === "approval" ? this.decided(ask, reply, tell) : this.answered(ask, reply, tell)).then(resolve, reject);
383
+ });
384
+ }
385
+ reach(milestone) {
386
+ const waiting = this.waiting;
387
+ if (waiting !== void 0) {
388
+ this.waiting = void 0;
389
+ waiting(milestone);
390
+ } else {
391
+ this.milestones.push(milestone);
392
+ }
393
+ }
394
+ enqueue(work) {
395
+ this.chain = this.chain.then(work).catch((error) => {
396
+ this.a.logger.error(`run ${this.a.runId}: ${errorText(error, "event handling failed")}`);
397
+ if (!this.ended) {
398
+ this.ended = true;
399
+ this.reach({
400
+ kind: "finished",
401
+ outcome: {
402
+ status: "failed",
403
+ error: errorText(error, "event handling failed")
404
+ }
405
+ });
406
+ }
407
+ });
408
+ }
409
+ async write(event) {
410
+ await this.a.writer.write(encodeStreamEvent(event));
411
+ }
412
+ async step() {
413
+ if (this.stepOpen) return;
414
+ this.stepOpen = true;
415
+ await this.write({
416
+ kind: "step-start"
417
+ });
418
+ }
419
+ endReasoning() {
420
+ if (this.reasoningSince === void 0) return;
421
+ this.segment.reasoningMs += Date.now() - this.reasoningSince;
422
+ this.reasoningSince = void 0;
423
+ }
424
+ tool(id, name = "tool") {
425
+ let state = this.tools.get(id);
426
+ if (state === void 0) {
427
+ state = {
428
+ name,
429
+ announced: false,
430
+ available: false,
431
+ settled: false,
432
+ input: {},
433
+ innerAnnounced: /* @__PURE__ */ new Set(),
434
+ innerSettled: /* @__PURE__ */ new Set()
435
+ };
436
+ this.tools.set(id, state);
437
+ this.segment.calls.push({
438
+ id,
439
+ name,
440
+ input: state.input,
441
+ kind: "read"
442
+ });
443
+ }
444
+ return state;
445
+ }
446
+ async announce(id, name) {
447
+ const state = this.tool(id, name);
448
+ if (!state.announced) {
449
+ state.announced = true;
450
+ await this.write({
451
+ kind: "tool-input-start",
452
+ id,
453
+ name: state.name,
454
+ toolKind: "read"
455
+ });
456
+ }
457
+ return state;
458
+ }
459
+ async makeAvailable(id) {
460
+ const state = await this.announce(id, this.tools.get(id)?.name ?? "tool");
461
+ if (!state.available) {
462
+ state.available = true;
463
+ await this.write({
464
+ kind: "tool-input-available",
465
+ id,
466
+ name: state.name,
467
+ input: state.input,
468
+ toolKind: "read"
469
+ });
470
+ }
471
+ return state;
472
+ }
473
+ setInput(id, input, name) {
474
+ const state = this.tool(id, name);
475
+ if (input === null || typeof input !== "object") return;
476
+ Object.assign(state.input, input);
477
+ }
478
+ async innerCalls(parentId, raw) {
479
+ if (!Array.isArray(raw)) return;
480
+ const parent = this.tool(parentId);
481
+ for (const [index, call] of raw.entries()) {
482
+ const id = `${parentId}.${index}`;
483
+ const name = String(call?.tool ?? "tool");
484
+ const input = call?.input !== null && typeof call?.input === "object" ? call.input : {};
485
+ if (!parent.innerAnnounced.has(index)) {
486
+ parent.innerAnnounced.add(index);
487
+ this.segment.calls.push({
488
+ id,
489
+ name,
490
+ input,
491
+ kind: "read",
492
+ parentId
493
+ });
494
+ await this.write({
495
+ kind: "tool-input-available",
496
+ id,
497
+ name,
498
+ input,
499
+ toolKind: "read",
500
+ parentId
501
+ });
502
+ }
503
+ const status = String(call?.status ?? "").toLowerCase();
504
+ if (parent.innerSettled.has(index) || status === "running" || status === "pending" || !status) {
505
+ continue;
506
+ }
507
+ parent.innerSettled.add(index);
508
+ if (FAILED_STATUSES.has(status)) {
509
+ this.segment.results.push({
510
+ id,
511
+ name,
512
+ output: {
513
+ status
514
+ },
515
+ error: status
516
+ });
517
+ await this.write({
518
+ kind: "tool-output-error",
519
+ id,
520
+ error: status
521
+ });
522
+ } else {
523
+ this.segment.results.push({
524
+ id,
525
+ name,
526
+ output: {
527
+ status
528
+ }
529
+ });
530
+ await this.write({
531
+ kind: "tool-output",
532
+ id,
533
+ output: {
534
+ status
535
+ }
536
+ });
537
+ }
538
+ }
539
+ }
540
+ async onEvent(e) {
541
+ if (this.ended) return;
542
+ const d = e.data ?? {};
543
+ switch (e.type) {
544
+ case "session.text.delta": {
545
+ const delta = typeof d.delta === "string" ? d.delta : "";
546
+ if (!delta) return;
547
+ this.endReasoning();
548
+ if (!this.sawText && !delta.trim()) return;
549
+ await this.step();
550
+ const text = this.separator && this.sawText ? `
551
+
552
+ ${delta}` : delta;
553
+ this.separator = false;
554
+ this.sawText = true;
555
+ this.segment.text += text;
556
+ await this.write({
557
+ kind: "text",
558
+ text
559
+ });
560
+ return;
561
+ }
562
+ case "session.text.ended":
563
+ this.separator = true;
564
+ return;
565
+ case "session.reasoning.delta": {
566
+ const delta = typeof d.delta === "string" ? d.delta : "";
567
+ if (!delta) return;
568
+ this.reasoningSince ??= Date.now();
569
+ await this.step();
570
+ this.segment.reasoning += delta;
571
+ await this.write({
572
+ kind: "reasoning",
573
+ text: delta
574
+ });
575
+ return;
576
+ }
577
+ case "session.tool.input.started":
578
+ this.endReasoning();
579
+ if (HIDDEN_TOOLS.has(String(d.name))) {
580
+ this.hidden.add(String(d.id));
581
+ return;
582
+ }
583
+ await this.step();
584
+ await this.announce(String(d.id), String(d.name ?? "tool"));
585
+ return;
586
+ case "session.tool.called":
587
+ if (this.hidden.has(String(d.id))) return;
588
+ await this.step();
589
+ this.setInput(String(d.id), d.input, typeof d.name === "string" ? d.name : void 0);
590
+ await this.makeAvailable(String(d.id));
591
+ return;
592
+ case "session.skill.activated": {
593
+ const name = String(d.id ?? d.name ?? "skill");
594
+ const id = `skill:${name}:${this.tools.size}`;
595
+ await this.step();
596
+ await this.announce(id, "skill");
597
+ this.setInput(id, {
598
+ name
599
+ });
600
+ await this.makeAvailable(id);
601
+ await this.settleTool(id, true);
602
+ return;
603
+ }
604
+ case "session.tool.progress":
605
+ if (this.hidden.has(String(d.id))) return;
606
+ await this.step();
607
+ await this.makeAvailable(String(d.id));
608
+ await this.innerCalls(String(d.id), d.metadata?.toolCalls);
609
+ return;
610
+ case "session.tool.success":
611
+ case "session.tool.failed": {
612
+ const id = String(d.id);
613
+ if (this.hidden.has(id)) return;
614
+ await this.step();
615
+ await this.makeAvailable(id);
616
+ await this.innerCalls(id, d.metadata?.toolCalls);
617
+ await this.settleTool(id, e.type === "session.tool.success", errorText(d.error, "tool failed"));
618
+ return;
619
+ }
620
+ case "session.step.ended":
621
+ this.endReasoning();
622
+ await this.stepEnded(d);
623
+ return;
624
+ case "session.step.failed":
625
+ this.stepError = errorText(d.error, "the model call failed");
626
+ return;
627
+ case "session.renamed":
628
+ if (typeof d.title === "string" && d.title.trim()) {
629
+ await this.a.store.setTitle(this.a.input.threadId, d.title.trim());
630
+ await this.write({
631
+ kind: "title",
632
+ title: d.title.trim()
633
+ });
634
+ }
635
+ return;
636
+ case "permission.asked":
637
+ await this.onPermission(d, false);
638
+ return;
639
+ case "form.created":
640
+ await this.onForm(d.form ?? d, false);
641
+ return;
642
+ case "session.execution.succeeded":
643
+ await this.end({
644
+ status: "succeeded"
645
+ });
646
+ return;
647
+ case "session.execution.failed":
648
+ await this.end({
649
+ status: "failed",
650
+ error: errorText(d.error, this.stepError ?? "the execution failed")
651
+ });
652
+ return;
653
+ case "session.execution.interrupted":
654
+ await this.end({
655
+ status: "interrupted"
656
+ });
657
+ return;
658
+ default:
659
+ return;
660
+ }
661
+ }
662
+ async settleTool(id, ok, error = "failed") {
663
+ const state = this.tool(id);
664
+ if (state.settled) return;
665
+ state.settled = true;
666
+ if (ok) {
667
+ this.segment.results.push({
668
+ id,
669
+ name: state.name,
670
+ output: {
671
+ ok: true
672
+ }
673
+ });
674
+ await this.write({
675
+ kind: "tool-output",
676
+ id,
677
+ output: {
678
+ ok: true
679
+ }
680
+ });
681
+ } else {
682
+ this.segment.results.push({
683
+ id,
684
+ name: state.name,
685
+ output: {
686
+ ok: false
687
+ },
688
+ error
689
+ });
690
+ await this.write({
691
+ kind: "tool-output-error",
692
+ id,
693
+ error
694
+ });
695
+ }
696
+ }
697
+ async stepEnded(d) {
698
+ const usage = {
699
+ inputTokens: Number(d.tokens?.input) || 0,
700
+ outputTokens: Number(d.tokens?.output) || 0,
701
+ ...Number(d.tokens?.cache?.read) ? {
702
+ cacheReadTokens: Number(d.tokens.cache.read)
703
+ } : {},
704
+ ...Number(d.tokens?.cache?.write) ? {
705
+ cacheWriteTokens: Number(d.tokens.cache.write)
706
+ } : {}
707
+ };
708
+ const costUsd = typeof d.cost === "number" ? d.cost : void 0;
709
+ this.usage.inputTokens += usage.inputTokens;
710
+ this.usage.outputTokens += usage.outputTokens;
711
+ if (costUsd !== void 0) this.costUsd = (this.costUsd ?? 0) + costUsd;
712
+ await this.a.store.recordUsage({
713
+ threadId: this.a.input.threadId,
714
+ actorRef: this.a.input.actor.id,
715
+ modelId: this.a.modelLabel,
716
+ purpose: "chat",
717
+ usage,
718
+ ...costUsd !== void 0 ? {
719
+ costUsd
720
+ } : {}
721
+ });
722
+ if (!this.stepOpen) return;
723
+ this.stepOpen = false;
724
+ await this.write({
725
+ kind: "step-finish",
726
+ usage,
727
+ costUsd: costUsd ?? null,
728
+ ...this.segment.reasoningMs > 0 ? {
729
+ reasoningMs: this.segment.reasoningMs
730
+ } : {},
731
+ model: this.a.modelLabel
732
+ });
733
+ }
734
+ /**
735
+ * Persist what the turn produced since the last flush as one assistant message — the message a
736
+ * call put to a person hangs on, or the turn's last one. Returns its id.
737
+ */
738
+ async flush(final) {
739
+ this.endReasoning();
740
+ const s = this.segment;
741
+ const empty = !s.text && !s.reasoning && s.calls.length === 0 && s.ui.length === 0;
742
+ if (empty && !(final && !this.wroteMessage)) return void 0;
743
+ this.segment = emptySegment();
744
+ const message = await this.a.store.appendMessage({
745
+ threadId: this.a.input.threadId,
746
+ role: "assistant",
747
+ content: s.text,
748
+ runId: this.a.runId,
749
+ ...final ? {
750
+ usage: {
751
+ ...this.usage,
752
+ ...this.costUsd !== void 0 ? {
753
+ costUsd: this.costUsd
754
+ } : {}
755
+ }
756
+ } : {},
757
+ ...this.a.input.agentName !== void 0 ? {
758
+ agentName: this.a.input.agentName
759
+ } : {},
760
+ ...this.a.input.persona !== void 0 ? {
761
+ persona: this.a.input.persona
762
+ } : {},
763
+ ...s.reasoning ? {
764
+ reasoning: s.reasoning,
765
+ reasoningMs: s.reasoningMs
766
+ } : {},
767
+ ...s.calls.length > 0 ? {
768
+ toolCalls: s.calls
769
+ } : {},
770
+ ...s.results.length > 0 ? {
771
+ toolResults: s.results
772
+ } : {},
773
+ ...s.ui.length > 0 ? {
774
+ ui: s.ui
775
+ } : {}
776
+ });
777
+ this.wroteMessage = true;
778
+ this.messageResults.set(message.id, [
779
+ ...s.results
780
+ ]);
781
+ return message.id;
782
+ }
783
+ /** A request recorded by an earlier process (a turn resumed after a restart). */
784
+ async alreadyRecorded(id) {
785
+ return (await this.a.store.toolCallApproval?.(id) ?? null) !== null;
786
+ }
787
+ /** The persisted assistant message carrying call `id`, so its result lands on the same message. */
788
+ async messageOf(id) {
789
+ const thread = await this.a.store.getThread(this.a.input.threadId);
790
+ return thread?.messages.find((m) => m.toolCalls?.some((call) => call.id === id))?.id;
791
+ }
792
+ async onPermission(req, recovering) {
793
+ if (this.asked.has(req.id)) return;
794
+ this.asked.add(req.id);
795
+ const { client, sessionId, input, store } = this.a;
796
+ const action = String(req.action);
797
+ const args = permissionArgs(req);
798
+ if (recovering && await this.alreadyRecorded(req.id)) {
799
+ const state = await store.toolCallApproval?.(req.id);
800
+ const messageId2 = await this.messageOf(req.id);
801
+ this.reach({
802
+ kind: "ask",
803
+ ask: {
804
+ kind: "approval",
805
+ id: req.id,
806
+ action,
807
+ approver: state?.approver ?? "requester",
808
+ ...state?.expiresAt ? {
809
+ expiresAt: state.expiresAt
810
+ } : {},
811
+ ...messageId2 !== void 0 ? {
812
+ messageId: messageId2
813
+ } : {}
814
+ },
815
+ ...state?.expiresAt ? {
816
+ timeoutMs: Math.max(0, Date.parse(state.expiresAt) - Date.now())
817
+ } : {}
818
+ });
819
+ return;
820
+ }
821
+ await this.step();
822
+ const requirement = await this.a.approvalFor(action);
823
+ const remembered = requirement.required ? (await store.rememberedApprovals?.(input.threadId) ?? []).includes(action) : false;
824
+ if (!requirement.required || remembered) {
825
+ this.a.onGranted?.(action, req.id);
826
+ await client.permission.reply({
827
+ sessionID: sessionId,
828
+ requestID: req.id,
829
+ decision: "once"
830
+ });
831
+ this.segment.calls.push({
832
+ id: req.id,
833
+ name: action,
834
+ input: args,
835
+ kind: "action"
836
+ });
837
+ this.segment.results.push({
838
+ id: req.id,
839
+ name: action,
840
+ output: {
841
+ approved: true
842
+ }
843
+ });
844
+ await this.write({
845
+ kind: "tool-input-available",
846
+ id: req.id,
847
+ name: action,
848
+ input: args,
849
+ toolKind: "action"
850
+ });
851
+ await this.write({
852
+ kind: "approval-settled",
853
+ id: req.id,
854
+ status: "approved",
855
+ approver: requirement.approver,
856
+ decidedVia: remembered ? "remembered" : "policy",
857
+ ...remembered ? {
858
+ remember: true
859
+ } : {}
860
+ });
861
+ await this.write({
862
+ kind: "tool-output",
863
+ id: req.id,
864
+ output: {
865
+ approved: true
866
+ }
867
+ });
868
+ return;
869
+ }
870
+ const ttlMs = requirement.ttlMs !== void 0 && Number.isFinite(requirement.ttlMs) && requirement.ttlMs > 0 ? Math.floor(requirement.ttlMs) : void 0;
871
+ const expiresAt = ttlMs !== void 0 ? new Date(Date.now() + ttlMs).toISOString() : void 0;
872
+ this.segment.calls.push({
873
+ id: req.id,
874
+ name: action,
875
+ input: args,
876
+ kind: "action"
877
+ });
878
+ const messageId = await this.flush(false);
879
+ await store.recordToolCall({
880
+ toolCallId: req.id,
881
+ messageId,
882
+ toolName: action,
883
+ toolType: "action",
884
+ input: args,
885
+ status: "pending_approval",
886
+ runId: this.a.runId,
887
+ approver: requirement.approver,
888
+ ...expiresAt !== void 0 ? {
889
+ expiresAt
890
+ } : {}
891
+ });
892
+ await this.write({
893
+ kind: "tool-input-available",
894
+ id: req.id,
895
+ name: action,
896
+ input: args,
897
+ toolKind: "action"
898
+ });
899
+ await this.write({
900
+ kind: "approval-requested",
901
+ id: req.id,
902
+ approver: requirement.approver,
903
+ ...expiresAt !== void 0 ? {
904
+ expiresAt
905
+ } : {}
906
+ });
907
+ this.reach({
908
+ kind: "ask",
909
+ ask: {
910
+ kind: "approval",
911
+ id: req.id,
912
+ action,
913
+ approver: requirement.approver,
914
+ messageId,
915
+ ...expiresAt !== void 0 ? {
916
+ expiresAt
917
+ } : {}
918
+ },
919
+ ...ttlMs !== void 0 ? {
920
+ timeoutMs: ttlMs
921
+ } : {}
922
+ });
923
+ }
924
+ async decided(ask, reply, tell) {
925
+ if (!isDecision(reply)) throw new OpenCodeReplyMismatchError(this.a.runId, ask.id);
926
+ const decision = reply;
927
+ const { client, sessionId, store } = this.a;
928
+ const { id, action } = ask;
929
+ const status = decision.approved ? "approved" : decision.expired ? "expired" : "rejected";
930
+ if (decision.approved) this.a.onGranted?.(action, id);
931
+ if (tell) await client.permission.reply({
932
+ sessionID: sessionId,
933
+ requestID: id,
934
+ decision: decision.approved ? "once" : "reject",
935
+ ...decision.approved ? {} : {
936
+ message: decision.expired ? "Nobody approved this action in time, so it was not run." : `The user denied this action${decision.reason ? `: ${decision.reason}` : ""}. Do not retry it; tell the user what you would have done.`
937
+ }
938
+ });
939
+ const result = decision.approved ? {
940
+ id,
941
+ name: action,
942
+ output: {
943
+ approved: true
944
+ }
945
+ } : {
946
+ id,
947
+ name: action,
948
+ output: {
949
+ rejected: true,
950
+ ...decision.reason ? {
951
+ reason: decision.reason
952
+ } : {}
953
+ },
954
+ error: decision.expired ? "approval expired" : "rejected by the user",
955
+ denied: true,
956
+ ...decision.expired ? {
957
+ expired: true
958
+ } : {}
959
+ };
960
+ await store.updateToolCall({
961
+ toolCallId: id,
962
+ status: decision.approved ? "executed" : status === "expired" ? "expired" : "rejected",
963
+ output: result.output,
964
+ ...result.error !== void 0 ? {
965
+ error: result.error
966
+ } : {},
967
+ ...decision.executedByRef !== void 0 ? {
968
+ executedByRef: decision.executedByRef
969
+ } : {},
970
+ ...decision.remember === true ? {
971
+ remember: true
972
+ } : {},
973
+ ...decision.decidedVia !== void 0 ? {
974
+ decidedVia: decision.decidedVia
975
+ } : {}
976
+ });
977
+ await this.addResult(ask.messageId, result);
978
+ await this.write({
979
+ kind: "approval-settled",
980
+ id,
981
+ status,
982
+ approver: ask.approver,
983
+ ...decision.executedByRef !== void 0 ? {
984
+ decidedBy: decision.executedByRef
985
+ } : {},
986
+ ...decision.decidedVia !== void 0 ? {
987
+ decidedVia: decision.decidedVia
988
+ } : {},
989
+ ...decision.remember === true ? {
990
+ remember: true
991
+ } : {},
992
+ ...decision.reason !== void 0 ? {
993
+ reason: decision.reason
994
+ } : {}
995
+ });
996
+ if (decision.approved) await this.write({
997
+ kind: "tool-output",
998
+ id,
999
+ output: {
1000
+ approved: true
1001
+ }
1002
+ });
1003
+ else await this.write({
1004
+ kind: "tool-output-denied",
1005
+ id,
1006
+ ...decision.expired ? {
1007
+ reason: "approval expired"
1008
+ } : decision.reason ? {
1009
+ reason: decision.reason
1010
+ } : {}
1011
+ });
1012
+ }
1013
+ async onForm(form, recovering) {
1014
+ if (this.asked.has(form.id)) return;
1015
+ this.asked.add(form.id);
1016
+ const request = toElicitation(form.id, form);
1017
+ if (recovering && await this.alreadyRecorded(form.id)) {
1018
+ const messageId2 = await this.messageOf(form.id);
1019
+ this.reach({
1020
+ kind: "ask",
1021
+ ask: {
1022
+ kind: "form",
1023
+ id: form.id,
1024
+ form,
1025
+ request,
1026
+ ...messageId2 !== void 0 ? {
1027
+ messageId: messageId2
1028
+ } : {}
1029
+ }
1030
+ });
1031
+ return;
1032
+ }
1033
+ const input = {
1034
+ ...request.preamble !== void 0 ? {
1035
+ preamble: request.preamble
1036
+ } : {},
1037
+ questions: request.questions
1038
+ };
1039
+ await this.step();
1040
+ this.segment.calls.push({
1041
+ id: form.id,
1042
+ name: ASK_TOOL_NAME,
1043
+ input,
1044
+ kind: "ask"
1045
+ });
1046
+ const messageId = await this.flush(false);
1047
+ await this.a.store.recordToolCall({
1048
+ toolCallId: form.id,
1049
+ messageId,
1050
+ toolName: ASK_TOOL_NAME,
1051
+ toolType: "action",
1052
+ input,
1053
+ status: "pending_approval",
1054
+ runId: this.a.runId
1055
+ });
1056
+ await this.write({
1057
+ kind: "elicitation",
1058
+ id: form.id,
1059
+ request
1060
+ });
1061
+ this.reach({
1062
+ kind: "ask",
1063
+ ask: {
1064
+ kind: "form",
1065
+ id: form.id,
1066
+ form,
1067
+ request,
1068
+ messageId
1069
+ }
1070
+ });
1071
+ }
1072
+ async answered(ask, reply, tell) {
1073
+ const { client, sessionId } = this.a;
1074
+ const { form, request } = ask;
1075
+ const answer = isDecision(reply) ? {
1076
+ answers: {},
1077
+ skipped: true
1078
+ } : reply;
1079
+ if (!tell) {
1080
+ } else if (answer.skipped === true) {
1081
+ await client.session.form.cancel({
1082
+ sessionID: sessionId,
1083
+ formID: form.id
1084
+ });
1085
+ } else {
1086
+ await client.session.form.reply({
1087
+ sessionID: sessionId,
1088
+ formID: form.id,
1089
+ answer: toFormAnswer(form, answer.answers)
1090
+ });
1091
+ }
1092
+ const result = settleElicitation(request, answer);
1093
+ await this.a.store.updateToolCall({
1094
+ toolCallId: form.id,
1095
+ status: result.skipped ? "rejected" : "executed",
1096
+ output: result,
1097
+ ...result.skipped ? {
1098
+ error: "skipped by the user"
1099
+ } : {},
1100
+ ...answer.answeredByRef !== void 0 ? {
1101
+ executedByRef: answer.answeredByRef
1102
+ } : {},
1103
+ ...answer.answeredVia !== void 0 ? {
1104
+ decidedVia: answer.answeredVia
1105
+ } : {}
1106
+ });
1107
+ await this.addResult(ask.messageId, {
1108
+ id: form.id,
1109
+ name: ASK_TOOL_NAME,
1110
+ output: result
1111
+ });
1112
+ await this.write({
1113
+ kind: "tool-output",
1114
+ id: form.id,
1115
+ output: result
1116
+ });
1117
+ }
1118
+ async addResult(messageId, result) {
1119
+ if (messageId === void 0) return;
1120
+ let known = this.messageResults.get(messageId);
1121
+ if (known === void 0) {
1122
+ const thread = await this.a.store.getThread(this.a.input.threadId);
1123
+ known = [
1124
+ ...thread?.messages.find((m) => m.id === messageId)?.toolResults ?? []
1125
+ ];
1126
+ }
1127
+ const results = [
1128
+ ...known.filter((r) => r.id !== result.id),
1129
+ result
1130
+ ];
1131
+ this.messageResults.set(messageId, results);
1132
+ await this.a.store.setMessageToolResults(messageId, results);
1133
+ }
1134
+ async end(outcome) {
1135
+ if (this.ended) return;
1136
+ this.ended = true;
1137
+ if (outcome.status !== "failed" || this.segment.text || this.segment.calls.length > 0 || this.segment.ui.length > 0) {
1138
+ await this.flush(true);
1139
+ }
1140
+ if (this.stepOpen) {
1141
+ this.stepOpen = false;
1142
+ await this.write({
1143
+ kind: "step-finish"
1144
+ });
1145
+ }
1146
+ this.reach({
1147
+ kind: "finished",
1148
+ outcome
1149
+ });
1150
+ }
1151
+ };
1152
+
1153
+ // src/mcp.ts
1154
+ var LOOP_SERVED_KINDS = /* @__PURE__ */ new Set([
1155
+ "agent",
1156
+ "ask",
1157
+ "skill",
1158
+ "memory"
1159
+ ]);
1160
+ var b64url = /* @__PURE__ */ __name((data) => Buffer.from(data).toString("base64url"), "b64url");
1161
+ var OpenCodeToolsTokens = class {
1162
+ static {
1163
+ __name(this, "OpenCodeToolsTokens");
1164
+ }
1165
+ ttlMs;
1166
+ key;
1167
+ constructor(secret, ttlMs) {
1168
+ this.ttlMs = ttlMs;
1169
+ this.key = createHash("sha256").update("aviary:opencode:tools\0").update(secret ?? randomBytes(32).toString("hex")).digest();
1170
+ }
1171
+ mint(actor, server, now = Date.now()) {
1172
+ const claims = {
1173
+ v: 1,
1174
+ actor,
1175
+ server,
1176
+ exp: now + this.ttlMs
1177
+ };
1178
+ const body = b64url(JSON.stringify(claims));
1179
+ return `${body}.${this.sign(body)}`;
1180
+ }
1181
+ verify(token, now = Date.now()) {
1182
+ const [body, signature, extra] = token.split(".");
1183
+ if (body === void 0 || signature === void 0 || extra !== void 0) return null;
1184
+ const expected = Buffer.from(this.sign(body));
1185
+ const given = Buffer.from(signature);
1186
+ if (expected.length !== given.length || !timingSafeEqual(expected, given)) return null;
1187
+ try {
1188
+ const claims = JSON.parse(Buffer.from(body, "base64url").toString("utf8"));
1189
+ if (claims?.v !== 1 || typeof claims.server !== "string" || typeof claims.exp !== "number" || typeof claims.actor?.id !== "string" || claims.exp <= now) {
1190
+ return null;
1191
+ }
1192
+ return claims;
1193
+ } catch {
1194
+ return null;
1195
+ }
1196
+ }
1197
+ sign(body) {
1198
+ return createHmac("sha256", this.key).update(body).digest("base64url");
1199
+ }
1200
+ };
1201
+ function isZodSchema(schema) {
1202
+ return schema["~standard"].vendor === "zod";
1203
+ }
1204
+ __name(isZodSchema, "isZodSchema");
1205
+ function toInputSchema(schema) {
1206
+ const extension = schemaExtensionOf(schema);
1207
+ if (extension !== void 0) {
1208
+ const base = toInputSchema(extension.base);
1209
+ const properties = {
1210
+ ...base.properties ?? {},
1211
+ ...extension.properties
1212
+ };
1213
+ return {
1214
+ ...base,
1215
+ properties
1216
+ };
1217
+ }
1218
+ let converted;
1219
+ if (isZodSchema(schema)) converted = toJsonSchemaCompat(schema);
1220
+ else {
1221
+ const converter = schema["~standard"].jsonSchema;
1222
+ if (typeof converter?.input === "function") converted = converter.input();
1223
+ }
1224
+ if (converted === void 0) return {
1225
+ type: "object",
1226
+ properties: {},
1227
+ additionalProperties: true
1228
+ };
1229
+ const { type: _type, ...rest } = converted;
1230
+ return {
1231
+ ...rest,
1232
+ type: "object"
1233
+ };
1234
+ }
1235
+ __name(toInputSchema, "toInputSchema");
1236
+ var asText = /* @__PURE__ */ __name((output) => typeof output === "string" ? output : JSON.stringify(output), "asText");
1237
+ var OpenCodeToolRefusedError = class extends Error {
1238
+ static {
1239
+ __name(this, "OpenCodeToolRefusedError");
1240
+ }
1241
+ name = "OpenCodeToolRefusedError";
1242
+ };
1243
+ var OpenCodeMcpEndpoint2 = class {
1244
+ static {
1245
+ __name(this, "OpenCodeMcpEndpoint");
1246
+ }
1247
+ turns;
1248
+ tokens;
1249
+ constructor(turns, tokens) {
1250
+ this.turns = turns;
1251
+ this.tokens = tokens;
1252
+ }
1253
+ /** The claims of a request's bearer token, or `null`. */
1254
+ authenticate(authorization) {
1255
+ const header = Array.isArray(authorization) ? authorization[0] : authorization;
1256
+ const match = /^Bearer\s+(.+)$/i.exec(header ?? "");
1257
+ const token = match?.[1]?.trim();
1258
+ return token ? this.tokens.verify(token) : null;
1259
+ }
1260
+ async handle(req, res, body) {
1261
+ const claims = this.authenticate(req.headers.authorization);
1262
+ if (claims === null) {
1263
+ res.statusCode = 401;
1264
+ res.setHeader("WWW-Authenticate", 'Bearer error="invalid_token"');
1265
+ res.setHeader("content-type", "application/json");
1266
+ res.end(JSON.stringify({
1267
+ error: "unauthorized"
1268
+ }));
1269
+ return;
1270
+ }
1271
+ const server = this.server(claims);
1272
+ const transport = new StreamableHTTPServerTransport({
1273
+ enableJsonResponse: true
1274
+ });
1275
+ res.on("close", () => {
1276
+ void transport.close();
1277
+ void server.close();
1278
+ });
1279
+ try {
1280
+ await server.connect(transport);
1281
+ await transport.handleRequest(req, res, body);
1282
+ } catch (error) {
1283
+ if (!res.headersSent) {
1284
+ res.statusCode = 500;
1285
+ res.setHeader("content-type", "application/json");
1286
+ res.end(JSON.stringify({
1287
+ jsonrpc: "2.0",
1288
+ error: {
1289
+ code: -32603,
1290
+ message: errorText(error, "Internal error")
1291
+ },
1292
+ id: null
1293
+ }));
1294
+ }
1295
+ }
1296
+ }
1297
+ /** One MCP server for one request, answering as the token's actor. */
1298
+ server(claims) {
1299
+ const server = new Server({
1300
+ name: "aviary-opencode",
1301
+ version: "1.0.0"
1302
+ }, {
1303
+ capabilities: {
1304
+ tools: {}
1305
+ }
1306
+ });
1307
+ const { actor } = claims;
1308
+ const call = /* @__PURE__ */ __name((meta, requestId) => this.turns.callContext({
1309
+ actor,
1310
+ serverKey: claims.server,
1311
+ requestId: String(requestId ?? ""),
1312
+ meta
1313
+ }), "call");
1314
+ server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
1315
+ const turn = await call(request.params?._meta, extra.requestId);
1316
+ const input = turn?.input ?? {};
1317
+ const allowed = turn !== void 0 ? this.turns.allowedTools(turn.input) : void 0;
1318
+ const definitions = (await this.turns.registry.definitionsFor(actor, this.turns.rolesPolicyFor(input), allowed)).filter((definition) => !LOOP_SERVED_KINDS.has(definition.kind));
1319
+ const tools = definitions.map((definition) => ({
1320
+ name: definition.name,
1321
+ description: definition.description,
1322
+ inputSchema: toInputSchema(definition.inputSchema),
1323
+ annotations: {
1324
+ readOnlyHint: definition.kind === "read"
1325
+ }
1326
+ }));
1327
+ if (this.turns.memoryWritable()) {
1328
+ tools.push({
1329
+ name: REMEMBER_TOOL_NAME,
1330
+ description: REMEMBER_TOOL_DESCRIPTION,
1331
+ inputSchema: toInputSchema(rememberInputSchema),
1332
+ annotations: {
1333
+ readOnlyHint: false
1334
+ }
1335
+ });
1336
+ }
1337
+ return {
1338
+ tools
1339
+ };
1340
+ });
1341
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1342
+ const { name, arguments: args } = request.params;
1343
+ try {
1344
+ const turn = await call(request.params._meta, extra.requestId);
1345
+ if (turn === void 0) {
1346
+ throw new OpenCodeToolRefusedError("This endpoint serves OpenCode turns: no turn of yours is running on this session.");
1347
+ }
1348
+ const output = name === REMEMBER_TOOL_NAME && this.turns.memoryWritable() ? await this.remember(turn, args) : await this.invoke(turn, actor, name, args, String(extra.requestId ?? ""));
1349
+ return {
1350
+ content: [
1351
+ {
1352
+ type: "text",
1353
+ text: asText(output)
1354
+ }
1355
+ ]
1356
+ };
1357
+ } catch (error) {
1358
+ return {
1359
+ content: [
1360
+ {
1361
+ type: "text",
1362
+ text: errorText(error, "the tool failed")
1363
+ }
1364
+ ],
1365
+ isError: true
1366
+ };
1367
+ }
1368
+ });
1369
+ return server;
1370
+ }
1371
+ async invoke(turn, actor, name, args, requestId) {
1372
+ const { registry } = this.turns;
1373
+ const spec = registry.spec(name);
1374
+ if (spec === void 0 || LOOP_SERVED_KINDS.has(spec.kind)) throw new ToolNotFoundError(name);
1375
+ const allowed = this.turns.allowedTools(turn.input);
1376
+ if (allowed !== void 0 && !allowed.includes(name)) throw new ToolNotFoundError(name);
1377
+ if (spec.kind === "action" && !await this.turns.spendApproval(turn.runId, name, turn.input)) {
1378
+ throw new OpenCodeToolRefusedError(`"${name}" is an action and nobody approved this call; ask for the permission first.`);
1379
+ }
1380
+ return registry.invoke(name, args ?? {}, {
1381
+ actor,
1382
+ threadId: turn.ctx.threadId,
1383
+ runId: turn.runId,
1384
+ requestId: `mcp:${turn.runId}:${requestId}`,
1385
+ emitUi: turn.ctx.emitUi,
1386
+ ...turn.input.agentName !== void 0 ? {
1387
+ agentName: turn.input.agentName
1388
+ } : {},
1389
+ ...turn.input.persona !== void 0 ? {
1390
+ persona: turn.input.persona
1391
+ } : {},
1392
+ ...turn.input.pageContext !== void 0 ? {
1393
+ pageContext: turn.input.pageContext
1394
+ } : {}
1395
+ }, this.turns.rolesPolicyFor(turn.input), allowed !== void 0 ? {
1396
+ allowedTools: allowed
1397
+ } : {});
1398
+ }
1399
+ /** `remember`: one fact, at the actor's own scope only — as the loop serves it. */
1400
+ async remember(turn, args) {
1401
+ const parsed = await rememberInputSchema["~standard"].validate(args ?? {});
1402
+ if (parsed.issues !== void 0) {
1403
+ throw new OpenCodeToolRefusedError(`remember: ${parsed.issues.map((issue) => issue.message).join("; ")}`);
1404
+ }
1405
+ return this.turns.remember(parsed.value, turn);
1406
+ }
1407
+ };
1408
+
1409
+ // src/runner.ts
1410
+ import { RunCancelledError } from "@dudousxd/nestjs-agent-core";
1411
+ import { Inject as Inject3, Injectable as Injectable2 } from "@nestjs/common";
1412
+
1413
+ // src/turns.ts
1414
+ import { randomUUID } from "crypto";
1415
+ import { AGENT_CHAT_QUEUE } from "@dudousxd/nestjs-agent";
1416
+ import { AGENT_DEPS_FACTORY, AGENT_MEMORY, AGENT_SINK, AGENT_SKILLS, AGENT_STORE, AGENT_TOOL_REGISTRY, DEFAULT_MAX_FACT_CHARS, DefaultApprovalPolicy, RUN_ENDED_BEFORE_TOOL_CALL, actorScope, buildMemoryBlock, defaultScopeResolver, encodeStreamEvent as encodeStreamEvent2, memoryWriteVerdict, publishAgentRunFailed, releaseThreadRun, resolveMemoryDigest, resolveSkillCatalog, streamFailure } from "@dudousxd/nestjs-agent-core";
1417
+ import { Inject as Inject2, Injectable, Logger as Logger2, Optional } from "@nestjs/common";
1418
+
1419
+ // src/event-hub.ts
1420
+ import { Logger } from "@nestjs/common";
1421
+ function sessionOf(event) {
1422
+ const id = event.data?.sessionID ?? event.data?.form?.sessionID;
1423
+ return typeof id === "string" ? id : void 0;
1424
+ }
1425
+ __name(sessionOf, "sessionOf");
1426
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
1427
+ var OpenCodeEventHub = class _OpenCodeEventHub {
1428
+ static {
1429
+ __name(this, "OpenCodeEventHub");
1430
+ }
1431
+ options;
1432
+ logger = new Logger(_OpenCodeEventHub.name);
1433
+ streams = /* @__PURE__ */ new Map();
1434
+ constructor(options = {}) {
1435
+ this.options = options;
1436
+ }
1437
+ /**
1438
+ * Listen to one session's events on a server. Resolves once the server's stream is open (its first
1439
+ * event arrived), or after the connect timeout — the turn's safety nets cover a stream that is
1440
+ * slow to open.
1441
+ */
1442
+ async listen(serverKey, client, sessionId, listener) {
1443
+ const stream = this.streamFor(serverKey, client);
1444
+ let set = stream.listeners.get(sessionId);
1445
+ if (set === void 0) {
1446
+ set = /* @__PURE__ */ new Set();
1447
+ stream.listeners.set(sessionId, set);
1448
+ }
1449
+ set.add(listener);
1450
+ await stream.ready;
1451
+ return () => {
1452
+ const live = this.streams.get(serverKey) ?? stream;
1453
+ const current = live.listeners.get(sessionId);
1454
+ current?.delete(listener);
1455
+ if (current?.size === 0) live.listeners.delete(sessionId);
1456
+ if (live.listeners.size === 0 && this.streams.get(serverKey) === live) {
1457
+ live.controller.abort();
1458
+ this.streams.delete(serverKey);
1459
+ }
1460
+ };
1461
+ }
1462
+ close() {
1463
+ for (const stream of this.streams.values()) stream.controller.abort();
1464
+ this.streams.clear();
1465
+ }
1466
+ /**
1467
+ * The server's stream, opened on `client`. A different client for a known key (the host
1468
+ * replaced the connection, e.g. a new sandbox under the same key) moves every listener to a stream
1469
+ * on the new one: the old connection may be dead, and its sessions would never hear back.
1470
+ */
1471
+ streamFor(serverKey, client) {
1472
+ const existing = this.streams.get(serverKey);
1473
+ if (existing !== void 0 && existing.client === client) return existing;
1474
+ existing?.controller.abort();
1475
+ const controller = new AbortController();
1476
+ let opened;
1477
+ const ready = new Promise((resolve) => {
1478
+ opened = resolve;
1479
+ });
1480
+ const timeoutMs = this.options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
1481
+ const timer = setTimeout(() => {
1482
+ this.logger.warn(`event stream of ${serverKey} not open after ${timeoutMs}ms; going on`);
1483
+ opened();
1484
+ }, timeoutMs);
1485
+ timer.unref?.();
1486
+ void ready.then(() => clearTimeout(timer));
1487
+ controller.signal.addEventListener("abort", () => opened(), {
1488
+ once: true
1489
+ });
1490
+ const stream = {
1491
+ client,
1492
+ controller,
1493
+ listeners: existing?.listeners ?? /* @__PURE__ */ new Map(),
1494
+ ready
1495
+ };
1496
+ this.streams.set(serverKey, stream);
1497
+ void this.pump(serverKey, stream, client, opened);
1498
+ return stream;
1499
+ }
1500
+ async pump(serverKey, stream, client, opened) {
1501
+ let backoff = 500;
1502
+ while (!stream.controller.signal.aborted) {
1503
+ try {
1504
+ const events = client.event.subscribe({
1505
+ signal: stream.controller.signal
1506
+ });
1507
+ for await (const event of events) {
1508
+ opened();
1509
+ backoff = 500;
1510
+ const sessionId = sessionOf(event);
1511
+ if (sessionId === void 0) continue;
1512
+ for (const listener of stream.listeners.get(sessionId) ?? []) {
1513
+ try {
1514
+ listener(event);
1515
+ } catch (error) {
1516
+ this.logger.error(`listener failed: ${error.message}`);
1517
+ }
1518
+ }
1519
+ }
1520
+ } catch (error) {
1521
+ if (stream.controller.signal.aborted) return;
1522
+ this.logger.warn(`event stream of ${serverKey} dropped: ${error.message}`);
1523
+ }
1524
+ if (stream.controller.signal.aborted) return;
1525
+ await new Promise((resolve) => setTimeout(resolve, backoff).unref?.());
1526
+ backoff = Math.min(backoff * 2, 1e4);
1527
+ }
1528
+ }
1529
+ };
1530
+
1531
+ // src/turns.ts
1532
+ function _ts_decorate2(decorators, target, key, desc) {
1533
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1534
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
1535
+ r = Reflect.decorate(decorators, target, key, desc);
1536
+ } else {
1537
+ for (var i = decorators.length - 1; i >= 0; i--) {
1538
+ if (d = decorators[i]) {
1539
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1540
+ }
1541
+ }
1542
+ }
1543
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
1544
+ }
1545
+ __name(_ts_decorate2, "_ts_decorate");
1546
+ function _ts_metadata2(metadataKey, metadataValue) {
1547
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
1548
+ return Reflect.metadata(metadataKey, metadataValue);
1549
+ }
1550
+ }
1551
+ __name(_ts_metadata2, "_ts_metadata");
1552
+ function _ts_param2(paramIndex, decorator) {
1553
+ return function(target, key) {
1554
+ decorator(target, key, paramIndex);
1555
+ };
1556
+ }
1557
+ __name(_ts_param2, "_ts_param");
1558
+ var DEFAULT_TURN_TIMEOUT_MS = 30 * 6e4;
1559
+ var DEFAULT_TOOLS_TTL_MS = 7 * 24 * 60 * 6e4;
1560
+ var OpenCodeTurns = class {
1561
+ static {
1562
+ __name(this, "OpenCodeTurns");
1563
+ }
1564
+ host;
1565
+ sessions;
1566
+ settings;
1567
+ store;
1568
+ sink;
1569
+ deps;
1570
+ registry;
1571
+ skills;
1572
+ memory;
1573
+ queue;
1574
+ toolsTokens;
1575
+ logger = new Logger2("OpenCodeTurns");
1576
+ events = new OpenCodeEventHub();
1577
+ live = /* @__PURE__ */ new Map();
1578
+ /** Actions approved per run, and the approvals the MCP endpoint already spent. */
1579
+ grants = /* @__PURE__ */ new Map();
1580
+ spent = /* @__PURE__ */ new Set();
1581
+ /** When the tools endpoint was last registered for a location (`server|boot|directory|actor`). */
1582
+ toolsIssued = /* @__PURE__ */ new Map();
1583
+ constructor(host, sessions, settings, store, sink, deps, registry, skills, memory, queue, toolsTokens2) {
1584
+ this.host = host;
1585
+ this.sessions = sessions;
1586
+ this.settings = settings;
1587
+ this.store = store;
1588
+ this.sink = sink;
1589
+ this.deps = deps;
1590
+ this.registry = registry;
1591
+ this.skills = skills;
1592
+ this.memory = memory;
1593
+ this.queue = queue;
1594
+ this.toolsTokens = toolsTokens2;
1595
+ }
1596
+ /** The tools server's name in OpenCode. */
1597
+ get toolsServer() {
1598
+ return this.settings.tools?.server ?? "aviary";
1599
+ }
1600
+ /** The roles policy a turn's tools are gated by (the agent's own). */
1601
+ rolesPolicyFor(input) {
1602
+ return this.deps.forAgent(input.agentName).rolesPolicy;
1603
+ }
1604
+ /** The tools a turn may reach: the agent's allow-list, narrowed by its persona's. */
1605
+ allowedTools(input) {
1606
+ const deps = this.deps.forAgent(input.agentName);
1607
+ const agent = deps.toolAllowList;
1608
+ const persona = input.persona !== void 0 ? deps.personas?.find((p) => p.id === input.persona)?.allowedTools : void 0;
1609
+ if (agent === void 0) return persona === void 0 ? void 0 : [
1610
+ ...persona
1611
+ ];
1612
+ if (persona === void 0) return [
1613
+ ...agent
1614
+ ];
1615
+ return agent.filter((name) => persona.includes(name));
1616
+ }
1617
+ /**
1618
+ * Whether the tools endpoint serves `remember`: the module's tools are served and the memory
1619
+ * provider writes. Served by the engine's own endpoint only — never put in the module's shared
1620
+ * registry, where every MCP client and the `/tools` catalog would see it.
1621
+ */
1622
+ memoryWritable() {
1623
+ return this.settings.tools !== void 0 && this.memory?.provider.write !== void 0;
1624
+ }
1625
+ /** `remember` for a turn: one fact, at the actor's own scope only — as the loop serves it. */
1626
+ async remember(input, turn) {
1627
+ const memory = this.memory;
1628
+ const write = memory?.provider.write?.bind(memory.provider);
1629
+ if (memory === void 0 || write === void 0) return "Memory is read-only here.";
1630
+ const max = memory.maxFactChars ?? DEFAULT_MAX_FACT_CHARS;
1631
+ if (input.fact.length > max) return `Not recorded: a fact is at most ${max} characters.`;
1632
+ const actor = turn.input.actor;
1633
+ const scopeCtx = {
1634
+ actor,
1635
+ threadId: turn.ctx.threadId,
1636
+ ...turn.input.agentName !== void 0 ? {
1637
+ agentName: turn.input.agentName
1638
+ } : {}
1639
+ };
1640
+ const scopes = await (memory.scopes ?? defaultScopeResolver).resolve(scopeCtx);
1641
+ const scope = actorScope(actor);
1642
+ const verdict = memoryWriteVerdict({
1643
+ scope,
1644
+ scopes,
1645
+ actor,
1646
+ author: {
1647
+ kind: "agent"
1648
+ }
1649
+ });
1650
+ if (!verdict.allowed) return `Not recorded: ${verdict.reason}`;
1651
+ await write({
1652
+ key: input.key,
1653
+ text: input.fact,
1654
+ scope,
1655
+ origin: {
1656
+ author: "agent",
1657
+ actorRef: actor.id,
1658
+ threadId: turn.ctx.threadId,
1659
+ runId: turn.runId
1660
+ },
1661
+ ctx: scopeCtx
1662
+ });
1663
+ return `Recorded "${input.key}".`;
1664
+ }
1665
+ /**
1666
+ * The turn a tool call over the engine's MCP endpoint belongs to, or `undefined` when it belongs
1667
+ * to none — which the endpoint refuses: it exists only to serve turns. OpenCode names its session in
1668
+ * every call's `_meta` (`ai.opencode/sessionID`); the call is that session's running turn's, and
1669
+ * only when the endpoint's caller is the person that turn runs for, on the server the token was
1670
+ * issued for. A session this process is not following is found through OpenCode (`session.get` →
1671
+ * the thread it was created for → the thread's running turn); its components still reach the
1672
+ * stream, as a message of their own.
1673
+ */
1674
+ async callContext(call) {
1675
+ const sessionId = sessionOfMeta(call.meta);
1676
+ if (sessionId === void 0) return void 0;
1677
+ const scope = /* @__PURE__ */ __name((runId2) => `${runId2}:${call.requestId || randomUUID()}`, "scope");
1678
+ for (const [runId2, live] of this.live) {
1679
+ if (live.handle.sessionId !== sessionId) continue;
1680
+ if (live.input.actor.id !== call.actor.id || live.handle.serverKey !== call.serverKey) {
1681
+ return void 0;
1682
+ }
1683
+ return {
1684
+ input: live.input,
1685
+ runId: runId2,
1686
+ ctx: {
1687
+ threadId: live.input.threadId,
1688
+ runId: runId2,
1689
+ emitUi: uiEmitter(scope(runId2), async (component) => {
1690
+ await live.turn.pushUi(component);
1691
+ await this.uiPushed(live.input, runId2, component);
1692
+ })
1693
+ }
1694
+ };
1695
+ }
1696
+ const threadId = await this.threadOfSession(call.actor, sessionId);
1697
+ if (threadId === void 0) return void 0;
1698
+ if (await this.store.ownerOfThread(threadId) !== call.actor.id) return void 0;
1699
+ const known = await this.sessions.get(threadId);
1700
+ if (known === null || known.sessionId !== sessionId || known.serverKey !== call.serverKey) {
1701
+ return void 0;
1702
+ }
1703
+ const runId = await this.activeRun(threadId);
1704
+ if (runId === null) return void 0;
1705
+ const input = {
1706
+ threadId,
1707
+ actor: call.actor,
1708
+ userText: "",
1709
+ ...known.agentName !== void 0 ? {
1710
+ agentName: known.agentName
1711
+ } : {},
1712
+ ...known.persona !== void 0 ? {
1713
+ persona: known.persona
1714
+ } : {}
1715
+ };
1716
+ return {
1717
+ input,
1718
+ runId,
1719
+ ctx: {
1720
+ threadId,
1721
+ runId,
1722
+ emitUi: uiEmitter(scope(runId), async (component) => {
1723
+ const writer = await this.sink.open(runId);
1724
+ await writer.write(encodeStreamEvent2({
1725
+ kind: "ui",
1726
+ ...component
1727
+ }));
1728
+ await this.store.appendMessage({
1729
+ threadId,
1730
+ role: "assistant",
1731
+ content: "",
1732
+ runId,
1733
+ ui: [
1734
+ component
1735
+ ]
1736
+ });
1737
+ await this.uiPushed(input, runId, component);
1738
+ })
1739
+ }
1740
+ };
1741
+ }
1742
+ /**
1743
+ * Spend one approval of `tool` in `runId` — what the MCP endpoint asks before it runs an `action`.
1744
+ * OpenCode asks the person (or the policy answers) before it calls an action tool; the endpoint
1745
+ * runs it only against an approval this engine saw granted, so a caller that skipped OpenCode's
1746
+ * permission rules (a model in code mode that read the endpoint's headers, anything else that got
1747
+ * hold of the token) cannot run an action unapproved. One approval, one call. The approval is the
1748
+ * run's own: live in this process, else read off the run's persisted calls (spent marks are kept
1749
+ * per process).
1750
+ */
1751
+ async spendApproval(runId, tool, input) {
1752
+ const names = /* @__PURE__ */ new Set([
1753
+ tool,
1754
+ `${this.toolsServer}.${tool}`,
1755
+ `${this.toolsServer}_${tool}`
1756
+ ]);
1757
+ const local = (this.grants.get(runId) ?? []).find((grant) => names.has(grant.action) && !this.spent.has(grant.id));
1758
+ if (local !== void 0) {
1759
+ this.spent.add(local.id);
1760
+ return true;
1761
+ }
1762
+ const thread = await this.store.getThread(input.threadId);
1763
+ for (const message of thread?.messages ?? []) {
1764
+ if (message.runId !== runId) continue;
1765
+ for (const call of message.toolCalls ?? []) {
1766
+ if (!names.has(call.name) || this.spent.has(call.id)) continue;
1767
+ const result = message.toolResults?.find((r) => r.id === call.id);
1768
+ const approved = result?.output?.approved === true;
1769
+ if (approved) {
1770
+ this.spent.add(call.id);
1771
+ return true;
1772
+ }
1773
+ }
1774
+ }
1775
+ return false;
1776
+ }
1777
+ async activeRun(threadId) {
1778
+ if (this.store.activeRunForThread !== void 0) return this.store.activeRunForThread(threadId);
1779
+ return (await this.store.getThread(threadId))?.activeRunId ?? null;
1780
+ }
1781
+ async uiPushed(input, runId, component) {
1782
+ await this.host.onUi?.({
1783
+ input,
1784
+ runId,
1785
+ component
1786
+ }).catch((error) => {
1787
+ this.logger.warn(`onUi failed: ${errorText(error, "error")}`);
1788
+ });
1789
+ }
1790
+ /**
1791
+ * Push a component into the run an OpenCode session is serving, for the host's own trusted
1792
+ * callers (no caller check — {@link callContext} is the one for MCP requests). `false` when no run
1793
+ * is live on that session.
1794
+ */
1795
+ async pushToSession(sessionId, component) {
1796
+ for (const [runId, live] of this.live) {
1797
+ if (live.handle.sessionId !== sessionId) continue;
1798
+ await live.turn.pushUi(component);
1799
+ await this.uiPushed(live.input, runId, component);
1800
+ return true;
1801
+ }
1802
+ return false;
1803
+ }
1804
+ /** The runs this process is following now (an updater drains on it). */
1805
+ liveRuns() {
1806
+ return [
1807
+ ...this.live.keys()
1808
+ ];
1809
+ }
1810
+ async threadOfSession(actor, sessionId) {
1811
+ try {
1812
+ const server = await this.host.server(actor);
1813
+ const info = await server.client.session.get?.({
1814
+ sessionID: sessionId
1815
+ });
1816
+ const threadId = info?.metadata?.threadId;
1817
+ return typeof threadId === "string" ? threadId : void 0;
1818
+ } catch {
1819
+ return void 0;
1820
+ }
1821
+ }
1822
+ onApplicationShutdown() {
1823
+ for (const live of this.live.values()) live.stop();
1824
+ this.live.clear();
1825
+ this.events.close();
1826
+ }
1827
+ // ---- begin ---------------------------------------------------------------------------------
1828
+ /**
1829
+ * Persist the user message (or rewind the thread for a regenerate), find or create the thread's
1830
+ * session, and refresh what the session is told this turn. Safe to run again for the same run (a
1831
+ * durable `begin` re-run after a crash): the run's user message is written once.
1832
+ */
1833
+ async begin(runId, input) {
1834
+ const earlier = await this.prepareThread(runId, input);
1835
+ const server = await this.host.server(input.actor);
1836
+ const known = await this.sessions.get(input.threadId);
1837
+ const reusable = known !== null && known.serverKey === server.key && known.bootId === server.bootId && (await this.host.reuse?.({
1838
+ input,
1839
+ runId,
1840
+ session: known
1841
+ }) ?? true);
1842
+ let handle;
1843
+ let created = false;
1844
+ if (known !== null && reusable) {
1845
+ handle = {
1846
+ sessionId: known.sessionId,
1847
+ serverKey: known.serverKey,
1848
+ ...known.bootId !== void 0 ? {
1849
+ bootId: known.bootId
1850
+ } : {},
1851
+ ...known.directory !== void 0 ? {
1852
+ directory: known.directory
1853
+ } : {}
1854
+ };
1855
+ if (input.regenerate === true) await this.revertLastExchange(server.client, handle.sessionId);
1856
+ await this.addTools(input, server, handle.directory, false);
1857
+ } else {
1858
+ handle = await this.createSession(runId, input, server, earlier);
1859
+ created = true;
1860
+ }
1861
+ if (created || known?.agentName !== input.agentName || known?.persona !== input.persona) {
1862
+ await this.sessions.set(input.threadId, {
1863
+ ...handle,
1864
+ ...input.agentName !== void 0 ? {
1865
+ agentName: input.agentName
1866
+ } : {},
1867
+ ...input.persona !== void 0 ? {
1868
+ persona: input.persona
1869
+ } : {}
1870
+ });
1871
+ }
1872
+ await this.host.beforePrompt?.({
1873
+ input,
1874
+ runId,
1875
+ sessionId: handle.sessionId,
1876
+ client: server.client,
1877
+ created
1878
+ });
1879
+ await this.refreshInstructions(runId, input, server.client, handle.sessionId);
1880
+ return handle;
1881
+ }
1882
+ async createSession(runId, input, server, earlier, note) {
1883
+ const { client } = server;
1884
+ const context = {
1885
+ input,
1886
+ runId
1887
+ };
1888
+ const spec = await this.host.session(context);
1889
+ const directory = spec.location?.directory;
1890
+ const skills = await this.writeSkills(input, client, directory);
1891
+ const sessionId = (await client.session.create({
1892
+ ...spec,
1893
+ permissions: [
1894
+ ...spec.permissions ?? [],
1895
+ ...this.toolRules(input),
1896
+ ...skills.map((name) => ({
1897
+ action: "skill",
1898
+ resource: name,
1899
+ effect: "allow"
1900
+ }))
1901
+ ],
1902
+ metadata: {
1903
+ ...spec.metadata ?? {},
1904
+ threadId: input.threadId
1905
+ }
1906
+ })).id;
1907
+ const handle = {
1908
+ sessionId,
1909
+ serverKey: server.key,
1910
+ ...server.bootId !== void 0 ? {
1911
+ bootId: server.bootId
1912
+ } : {},
1913
+ ...directory !== void 0 ? {
1914
+ directory
1915
+ } : {}
1916
+ };
1917
+ await this.sessions.set(input.threadId, handle);
1918
+ await this.addTools(input, server, directory, true);
1919
+ await this.host.prepare?.({
1920
+ ...context,
1921
+ sessionId,
1922
+ client
1923
+ });
1924
+ const transcript = transcriptOf(earlier, this.settings.historyMessages ?? 20);
1925
+ if (transcript || note) {
1926
+ await client.session.instructions.entry.put({
1927
+ sessionID: sessionId,
1928
+ key: "aviary.history",
1929
+ value: [
1930
+ transcript ? `The conversation so far (continue it; do not repeat it):
1931
+ ${transcript}` : "",
1932
+ note ?? ""
1933
+ ].filter(Boolean).join("\n\n")
1934
+ });
1935
+ }
1936
+ return handle;
1937
+ }
1938
+ /** The user message, as the loop persists it (or, on a regenerate, the thread rewound to it). */
1939
+ async prepareThread(runId, input) {
1940
+ const thread = await this.store.getThread(input.threadId);
1941
+ const messages = thread?.messages ?? [];
1942
+ if (input.regenerate === true) {
1943
+ let lastUser = -1;
1944
+ for (let i = messages.length - 1; i >= 0; i -= 1) if (messages[i]?.role === "user") {
1945
+ lastUser = i;
1946
+ break;
1947
+ }
1948
+ const firstDropped = messages[lastUser + 1];
1949
+ if (firstDropped !== void 0) await this.store.truncateFrom(input.threadId, firstDropped.id);
1950
+ return messages.slice(0, Math.max(lastUser, 0));
1951
+ }
1952
+ const mine = messages.findIndex((m) => m.role === "user" && m.runId === runId);
1953
+ if (mine >= 0) return messages.slice(0, mine);
1954
+ await this.store.appendMessage({
1955
+ threadId: input.threadId,
1956
+ role: "user",
1957
+ content: input.userText,
1958
+ runId,
1959
+ ...input.persona !== void 0 ? {
1960
+ persona: input.persona
1961
+ } : {},
1962
+ ...input.attachments !== void 0 ? {
1963
+ attachments: input.attachments
1964
+ } : {}
1965
+ });
1966
+ if (thread !== null && (thread.title === "" || thread.title === "New chat")) {
1967
+ await this.store.setTitle(input.threadId, titleOf(input.userText));
1968
+ }
1969
+ return messages;
1970
+ }
1971
+ /** Rewind the session to before its last user message, so the regenerated answer replaces it. */
1972
+ async revertLastExchange(client, sessionId) {
1973
+ if (client.message === void 0 || client.session.revert === void 0) return;
1974
+ const page = await client.message.list({
1975
+ sessionID: sessionId,
1976
+ order: "desc",
1977
+ limit: 50
1978
+ });
1979
+ const last = page.data.find((m) => m.type === "user");
1980
+ if (last === void 0) return;
1981
+ await client.session.revert.stage({
1982
+ sessionID: sessionId,
1983
+ messageID: last.id,
1984
+ files: false
1985
+ });
1986
+ await client.session.revert.commit({
1987
+ sessionID: sessionId
1988
+ });
1989
+ }
1990
+ /** The agent's prompt, the memory block and the host's entries — refreshed every turn. */
1991
+ async refreshInstructions(runId, input, client, sessionId) {
1992
+ const memory = await this.memoryBlock(input);
1993
+ const entries = {
1994
+ [this.settings.systemKey ?? "aviary.system"]: await this.systemPrompt(input),
1995
+ ...memory !== void 0 ? {
1996
+ "aviary.memory": memory
1997
+ } : {},
1998
+ ...await this.host.instructions?.({
1999
+ input,
2000
+ runId,
2001
+ sessionId
2002
+ }) ?? {}
2003
+ };
2004
+ for (const [key, value] of Object.entries(entries)) {
2005
+ await client.session.instructions.entry.put({
2006
+ sessionID: sessionId,
2007
+ key,
2008
+ value
2009
+ });
2010
+ }
2011
+ }
2012
+ /** The agent's base prompt (`@Agent({ systemPrompt })` / `@SystemPrompt()`) and the contributors. */
2013
+ async systemPrompt(input) {
2014
+ const deps = this.deps.forAgent(input.agentName);
2015
+ const ctx = {
2016
+ actor: input.actor,
2017
+ agentName: input.agentName ?? "default",
2018
+ ...input.pageContext !== void 0 ? {
2019
+ pageContext: input.pageContext
2020
+ } : {},
2021
+ ...input.uiCapabilities !== void 0 ? {
2022
+ uiCapabilities: input.uiCapabilities
2023
+ } : {}
2024
+ };
2025
+ const base = typeof deps.systemPrompt === "function" ? await deps.systemPrompt(ctx) : deps.systemPrompt;
2026
+ const sections = [
2027
+ base
2028
+ ];
2029
+ for (const contribute of deps.promptContributors) {
2030
+ const section = await contribute(ctx);
2031
+ if (section) sections.push(section);
2032
+ }
2033
+ return sections.filter(Boolean).join("\n\n");
2034
+ }
2035
+ scopeContext(input) {
2036
+ return {
2037
+ actor: input.actor,
2038
+ threadId: input.threadId,
2039
+ ...input.agentName !== void 0 ? {
2040
+ agentName: input.agentName
2041
+ } : {},
2042
+ ...input.pageContext !== void 0 ? {
2043
+ pageContext: input.pageContext
2044
+ } : {}
2045
+ };
2046
+ }
2047
+ /** What is on file about the actor (`forRoot({ memory })`); writable when `remember` is served. */
2048
+ async memoryBlock(input) {
2049
+ const memory = this.memory;
2050
+ if (memory === void 0) return void 0;
2051
+ const ctx = this.scopeContext(input);
2052
+ const scopes = await (memory.scopes ?? defaultScopeResolver).resolve(ctx);
2053
+ const records = await memory.provider.list({
2054
+ scopes,
2055
+ ctx
2056
+ });
2057
+ const digest = resolveMemoryDigest({
2058
+ records,
2059
+ scopes,
2060
+ ...memory.maxMemories !== void 0 ? {
2061
+ maxMemories: memory.maxMemories
2062
+ } : {}
2063
+ });
2064
+ if (digest.entries.length === 0) return void 0;
2065
+ return buildMemoryBlock({
2066
+ entries: digest.entries,
2067
+ writable: this.memoryWritable(),
2068
+ partial: digest.omitted > 0
2069
+ });
2070
+ }
2071
+ /**
2072
+ * The module's skills (`forRoot({ skills })` / `@Skill`) as `SKILL.md` files under the session's
2073
+ * directory, where OpenCode's own `skill` tool finds them. Returns the names written.
2074
+ */
2075
+ async writeSkills(input, client, directory) {
2076
+ const skills = this.skills;
2077
+ if (skills === void 0 || client.file === void 0 || directory === void 0) return [];
2078
+ const ctx = this.scopeContext(input);
2079
+ const scopes = await (skills.scopes ?? defaultScopeResolver).resolve(ctx);
2080
+ const summaries = await skills.provider.list({
2081
+ scopes,
2082
+ ctx
2083
+ });
2084
+ const { entries } = resolveSkillCatalog(summaries, scopes, skills.maxSkills);
2085
+ const dir = this.settings.skillsDir ?? ".opencode/skills";
2086
+ const written = [];
2087
+ for (const entry of entries) {
2088
+ const body = await skills.provider.load({
2089
+ name: entry.name,
2090
+ scope: entry.scope,
2091
+ ctx
2092
+ });
2093
+ if (body === null) continue;
2094
+ const file = `---
2095
+ name: ${entry.name}
2096
+ description: ${JSON.stringify(entry.description)}
2097
+ ---
2098
+
2099
+ ${body}
2100
+ `;
2101
+ await client.file.write({
2102
+ location: {
2103
+ directory
2104
+ },
2105
+ path: `${dir}/${entry.name}/SKILL.md`,
2106
+ payload: new TextEncoder().encode(file)
2107
+ });
2108
+ written.push(entry.name);
2109
+ }
2110
+ return written;
2111
+ }
2112
+ /**
2113
+ * Permission rules for the module's tools: the MCP server allowed as a whole (OpenCode offers a
2114
+ * server's tools only when the server itself is allowed), then each `action` tool asked — the last
2115
+ * matching rule wins, so an action still lands on an approval card. Tools outside the agent's
2116
+ * (and persona's) allow-list, and the kinds only the loop serves, are denied. The endpoint enforces
2117
+ * all of it again on every call: these rules are what OpenCode offers, not what keeps a call out.
2118
+ */
2119
+ toolRules(input) {
2120
+ if (this.settings.tools === void 0) return [];
2121
+ const server = this.toolsServer;
2122
+ const allow = this.allowedTools(input);
2123
+ const both = /* @__PURE__ */ __name((name) => [
2124
+ `${server}.${name}`,
2125
+ `${server}_${name}`
2126
+ ], "both");
2127
+ const rules = [
2128
+ {
2129
+ action: `${server}*`,
2130
+ resource: "*",
2131
+ effect: "allow"
2132
+ }
2133
+ ];
2134
+ for (const spec of this.registry.allSpecs()) {
2135
+ const offered = (allow === void 0 || allow.includes(spec.name)) && !LOOP_SERVED_KINDS.has(spec.kind);
2136
+ if (offered && spec.kind === "read") continue;
2137
+ const effect = offered && spec.kind === "action" ? "ask" : "deny";
2138
+ for (const action of both(spec.name)) rules.push({
2139
+ action,
2140
+ resource: "*",
2141
+ effect
2142
+ });
2143
+ }
2144
+ return rules;
2145
+ }
2146
+ /**
2147
+ * Register the tools endpoint at the session's location, with a bearer token for the turn's actor
2148
+ * on this server. On a new session always; on a kept one when this process has not registered it
2149
+ * yet or its token is half-way through its life.
2150
+ */
2151
+ async addTools(input, server, directory, fresh) {
2152
+ const tools = this.settings.tools;
2153
+ const tokens = this.toolsTokens;
2154
+ if (tools === void 0 || tokens == null) return;
2155
+ const { client } = server;
2156
+ if (client.mcp === void 0) {
2157
+ this.logger.warn("`tools` is set but the OpenCode client cannot add MCP servers (`mcp.add`)");
2158
+ return;
2159
+ }
2160
+ const key = `${server.key}|${server.bootId ?? ""}|${directory ?? ""}|${input.actor.id}`;
2161
+ const issued = this.toolsIssued.get(key);
2162
+ const ttl = tools.ttlMs ?? DEFAULT_TOOLS_TTL_MS;
2163
+ if (!fresh && issued !== void 0 && Date.now() - issued < ttl / 2) return;
2164
+ await client.mcp.add({
2165
+ server: this.toolsServer,
2166
+ ...directory !== void 0 ? {
2167
+ location: {
2168
+ directory
2169
+ }
2170
+ } : {},
2171
+ config: {
2172
+ type: "remote",
2173
+ url: tools.url,
2174
+ headers: {
2175
+ Authorization: `Bearer ${tokens.mint(input.actor, server.key)}`
2176
+ },
2177
+ oauth: false
2178
+ }
2179
+ });
2180
+ this.toolsIssued.set(key, Date.now());
2181
+ }
2182
+ // ---- prompt / observe / reply --------------------------------------------------------------
2183
+ /** Start listening to the session (so nothing it says is missed), then send the user message. */
2184
+ async prompt(runId, input, handle) {
2185
+ const live = await this.ensureLive(runId, input, handle, false);
2186
+ try {
2187
+ const prompt = await this.host.promptFor?.({
2188
+ input,
2189
+ runId,
2190
+ sessionId: handle.sessionId
2191
+ }) ?? {
2192
+ text: input.userText,
2193
+ ...input.attachments?.length && this.host.files ? {
2194
+ files: await this.host.files({
2195
+ input,
2196
+ runId
2197
+ })
2198
+ } : {}
2199
+ };
2200
+ await live.client.session.prompt({
2201
+ sessionID: handle.sessionId,
2202
+ text: prompt.text,
2203
+ ...prompt.files?.length ? {
2204
+ files: prompt.files
2205
+ } : {}
2206
+ });
2207
+ } catch (error) {
2208
+ live.turn.fail(`OpenCode refused the message: ${error.message}`);
2209
+ }
2210
+ }
2211
+ /**
2212
+ * Wait for the turn's next milestone. In a process that was not following the turn (a resumed
2213
+ * durable run), the turn is rebuilt and first catches up on requests OpenCode raised meanwhile;
2214
+ * `session.wait` is the safety net for a terminal event that was missed.
2215
+ */
2216
+ async observe(runId, input, handle) {
2217
+ const live = await this.ensureLive(runId, input, handle, true);
2218
+ const { turn, client } = live;
2219
+ if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
2220
+ let done = false;
2221
+ const next = turn.next().then((milestone) => {
2222
+ done = true;
2223
+ return milestone;
2224
+ });
2225
+ const timeoutMs = this.settings.turnTimeoutMs ?? DEFAULT_TURN_TIMEOUT_MS;
2226
+ const timer = setTimeout(() => {
2227
+ if (!done) turn.fail("Timed out waiting for OpenCode.");
2228
+ }, timeoutMs);
2229
+ timer.unref?.();
2230
+ if (client.session.wait !== void 0) {
2231
+ void client.session.wait({
2232
+ sessionID: handle.sessionId
2233
+ }).then(() => new Promise((resolve) => setTimeout(resolve, 2e3))).then(async () => {
2234
+ if (done || turn.finished) return;
2235
+ await turn.catchUp().catch(() => void 0);
2236
+ await new Promise((resolve) => setImmediate(resolve));
2237
+ if (!done && !turn.finished) turn.handle({
2238
+ type: "session.execution.succeeded",
2239
+ data: {
2240
+ sessionID: handle.sessionId
2241
+ }
2242
+ });
2243
+ }).catch(() => void 0);
2244
+ }
2245
+ try {
2246
+ return await this.reached(runId, input, handle, await next);
2247
+ } finally {
2248
+ clearTimeout(timer);
2249
+ }
2250
+ }
2251
+ /**
2252
+ * Hand a person's answer back to OpenCode. When the server restarted while the turn waited, the
2253
+ * request is gone with it: a new session is opened, told what the person decided, and prompted
2254
+ * to go on.
2255
+ */
2256
+ async reply(runId, input, handle, ask, reply) {
2257
+ const server = await this.host.server(input.actor);
2258
+ if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
2259
+ return this.continueAfterRestart(runId, input, server, ask, reply);
2260
+ }
2261
+ const live = await this.ensureLive(runId, input, handle, false);
2262
+ await live.turn.decide(ask, reply);
2263
+ return handle;
2264
+ }
2265
+ async continueAfterRestart(runId, input, server, ask, reply) {
2266
+ const approved = "approved" in reply && reply.approved === true;
2267
+ const note = ask.kind === "approval" ? approved ? `The user approved the action ${ask.action} you asked permission for. Continue the task.` : `The user did not approve the action ${ask.action}. Continue without it and explain.` : `The user answered your question: ${JSON.stringify("answers" in reply ? reply.answers : {})}. Continue.`;
2268
+ const thread = await this.store.getThread(input.threadId);
2269
+ const handle = await this.createSession(runId, input, server, thread?.messages ?? [], note);
2270
+ await this.refreshInstructions(runId, input, server.client, handle.sessionId);
2271
+ this.drop(runId);
2272
+ const live = await this.ensureLive(runId, input, handle, false);
2273
+ await live.turn.decide(ask, reply, {
2274
+ tellOpenCode: false
2275
+ });
2276
+ await live.client.session.prompt({
2277
+ sessionID: handle.sessionId,
2278
+ text: note
2279
+ });
2280
+ return handle;
2281
+ }
2282
+ /** Interrupt the thread's session (cancel). */
2283
+ async interrupt(input) {
2284
+ const known = await this.sessions.get(input.threadId);
2285
+ if (known === null) return;
2286
+ const server = await this.host.server(input.actor);
2287
+ if (server.key !== known.serverKey || server.bootId !== known.bootId) return;
2288
+ await server.client.session.interrupt({
2289
+ sessionID: known.sessionId
2290
+ });
2291
+ }
2292
+ async ensureLive(runId, input, handle, catchUp) {
2293
+ const current = this.live.get(runId);
2294
+ if (current !== void 0 && current.handle.sessionId === handle.sessionId) return current;
2295
+ current?.stop();
2296
+ const server = await this.host.server(input.actor);
2297
+ const deps = this.deps.forAgent(input.agentName);
2298
+ const policy = deps.approvalPolicy ?? new DefaultApprovalPolicy();
2299
+ const writer = await this.sink.open(input.sinkRunId ?? runId);
2300
+ const turn = new OpenCodeTurn({
2301
+ runId,
2302
+ input,
2303
+ client: server.client,
2304
+ sessionId: handle.sessionId,
2305
+ writer,
2306
+ store: this.store,
2307
+ approvalFor: /* @__PURE__ */ __name(async (action) => policy.requirementFor({
2308
+ name: action,
2309
+ kind: "action"
2310
+ }, input.actor, {
2311
+ threadId: input.threadId,
2312
+ runId,
2313
+ ...input.agentName !== void 0 ? {
2314
+ agentName: input.agentName
2315
+ } : {}
2316
+ }), "approvalFor"),
2317
+ modelLabel: input.model ?? "opencode",
2318
+ logger: this.logger,
2319
+ onGranted: /* @__PURE__ */ __name((action, id) => {
2320
+ const grants = this.grants.get(runId) ?? [];
2321
+ grants.push({
2322
+ action,
2323
+ id
2324
+ });
2325
+ this.grants.set(runId, grants);
2326
+ }, "onGranted")
2327
+ });
2328
+ const stop = await this.events.listen(server.key, server.client, handle.sessionId, (event) => turn.handle(event));
2329
+ const live = {
2330
+ turn,
2331
+ input,
2332
+ handle,
2333
+ client: server.client,
2334
+ stop
2335
+ };
2336
+ this.live.set(runId, live);
2337
+ if (catchUp && current === void 0) await turn.catchUp().catch(() => void 0);
2338
+ return live;
2339
+ }
2340
+ /** Stop following a run in this process. */
2341
+ drop(runId) {
2342
+ this.live.get(runId)?.stop();
2343
+ this.live.delete(runId);
2344
+ }
2345
+ /** Stop following a run and drop the approvals it was granted: it is over. */
2346
+ forget(runId) {
2347
+ this.drop(runId);
2348
+ for (const grant of this.grants.get(runId) ?? []) this.spent.delete(grant.id);
2349
+ this.grants.delete(runId);
2350
+ }
2351
+ // ---- settle --------------------------------------------------------------------------------
2352
+ /** A person was asked something: tell the host (cards in other channels, wake-ups). */
2353
+ async reached(runId, input, handle, milestone) {
2354
+ if (milestone.kind === "ask" && this.host.onAsk !== void 0) {
2355
+ await this.host.onAsk({
2356
+ input,
2357
+ runId,
2358
+ sessionId: handle.sessionId,
2359
+ ask: milestone.ask
2360
+ }).catch((error) => this.logger.warn(`onAsk failed: ${errorText(error, "error")}`));
2361
+ }
2362
+ return milestone;
2363
+ }
2364
+ /** What the run produced, as the host's settle hooks see it. */
2365
+ async runResult(runId, input, outcome, durationMs) {
2366
+ const thread = await this.store.getThread(input.threadId);
2367
+ const messages = (thread?.messages ?? []).filter((m) => m.role === "assistant" && m.runId === runId);
2368
+ const usage = {
2369
+ inputTokens: 0,
2370
+ outputTokens: 0,
2371
+ costUsd: 0
2372
+ };
2373
+ for (const m of messages) {
2374
+ usage.inputTokens += m.usage?.inputTokens ?? 0;
2375
+ usage.outputTokens += m.usage?.outputTokens ?? 0;
2376
+ usage.costUsd += m.usage?.costUsd ?? 0;
2377
+ }
2378
+ return {
2379
+ runId,
2380
+ input,
2381
+ outcome,
2382
+ text: messages.map((m) => m.content).filter(Boolean).join("\n\n"),
2383
+ messages,
2384
+ usage,
2385
+ durationMs
2386
+ };
2387
+ }
2388
+ /**
2389
+ * The host's last word on the answer before the stream ends: components to append (a guardrail
2390
+ * notice), or, for a failed run, the error the person reads instead of OpenCode's.
2391
+ */
2392
+ async amend(writer, result) {
2393
+ if (this.host.beforeSettle === void 0) return {};
2394
+ let amendment = {};
2395
+ try {
2396
+ amendment = await this.host.beforeSettle(result) ?? {};
2397
+ } catch (error) {
2398
+ this.logger.warn(`beforeSettle failed: ${errorText(error, "error")}`);
2399
+ }
2400
+ const ui = amendment.ui ?? [];
2401
+ const last = result.messages.at(-1);
2402
+ if (ui.length > 0) {
2403
+ for (const component of ui) await writer.write(encodeStreamEvent2({
2404
+ kind: "ui",
2405
+ ...component
2406
+ }));
2407
+ if (last !== void 0) await this.store.setMessageUi?.(last.id, [
2408
+ ...last.ui ?? [],
2409
+ ...ui
2410
+ ]);
2411
+ }
2412
+ return amendment;
2413
+ }
2414
+ /** Delivery, telemetry, spend: after the run settled. Errors are the host's, never the run's. */
2415
+ async settled(result) {
2416
+ try {
2417
+ await this.host.onSettled?.(result);
2418
+ } catch (error) {
2419
+ this.logger.warn(`onSettled failed: ${errorText(error, "error")}`);
2420
+ }
2421
+ }
2422
+ /** End the run's stream and bookkeeping for how its execution ended. */
2423
+ async settle(runId, input, outcome, durationMs) {
2424
+ if (outcome.status === "failed") {
2425
+ await this.settleFailed(runId, input, outcome.error, durationMs);
2426
+ return;
2427
+ }
2428
+ this.forget(runId);
2429
+ const writer = await this.sink.open(runId);
2430
+ const result = await this.runResult(runId, input, outcome, durationMs);
2431
+ await this.amend(writer, result);
2432
+ if (outcome.status === "interrupted") {
2433
+ this.logger.log(`agent run ${runId} cancelled`);
2434
+ await Promise.resolve(this.store.failUnsettledToolCalls?.(runId, RUN_ENDED_BEFORE_TOOL_CALL)).catch(() => 0);
2435
+ await this.handoff(writer, input, runId, "cancelled");
2436
+ await releaseThreadRun(this.store, input.threadId, runId);
2437
+ await writer.write(encodeStreamEvent2({
2438
+ kind: "cancelled"
2439
+ }));
2440
+ await writer.end();
2441
+ await this.store.recordRunEnd?.({
2442
+ runId,
2443
+ status: "cancelled",
2444
+ durationMs
2445
+ });
2446
+ } else {
2447
+ await this.handoff(writer, input, runId, "completed");
2448
+ await releaseThreadRun(this.store, input.threadId, runId);
2449
+ await writer.end();
2450
+ await this.store.recordRunEnd?.({
2451
+ runId,
2452
+ status: "completed",
2453
+ durationMs
2454
+ });
2455
+ }
2456
+ await this.settled(result);
2457
+ }
2458
+ async settleFailed(runId, input, error, durationMs = 0) {
2459
+ this.forget(runId);
2460
+ const writer = await this.sink.open(runId);
2461
+ const result = await this.runResult(runId, input, {
2462
+ status: "failed",
2463
+ error
2464
+ }, durationMs);
2465
+ const worded = (await this.amend(writer, result)).error;
2466
+ const message = worded ?? streamFailure(new Error(error)).message;
2467
+ this.logger.error(`agent run ${runId} failed: ${error}`);
2468
+ publishAgentRunFailed({
2469
+ runId,
2470
+ code: "opencode_failed",
2471
+ message: error
2472
+ });
2473
+ await this.store.recordRunEnd?.({
2474
+ runId,
2475
+ status: "failed",
2476
+ errorCode: "opencode_failed",
2477
+ errorMessage: error
2478
+ });
2479
+ await Promise.resolve(this.store.failUnsettledToolCalls?.(runId, RUN_ENDED_BEFORE_TOOL_CALL)).catch(() => 0);
2480
+ await this.handoff(writer, input, runId, "failed", error);
2481
+ await releaseThreadRun(this.store, input.threadId, runId);
2482
+ await writer.fail({
2483
+ code: "opencode_failed",
2484
+ message
2485
+ });
2486
+ await this.settled({
2487
+ ...result,
2488
+ outcome: {
2489
+ status: "failed",
2490
+ error: worded ?? error
2491
+ }
2492
+ });
2493
+ }
2494
+ /** Starts the next queued message of the thread; set by the runner (it owns `start`). */
2495
+ startNext;
2496
+ /** As `InlineAgentRunner`: hand the thread to its next queued message, and say so on the stream. */
2497
+ async handoff(writer, input, runId, outcome, error) {
2498
+ const queue = this.queue;
2499
+ const startNext = this.startNext;
2500
+ if (queue === void 0 || !queue.supported || startNext === void 0) return;
2501
+ if (input.sinkRunId !== void 0 || input.deliverTo !== void 0) return;
2502
+ try {
2503
+ const frame = await queue.handoff({
2504
+ threadId: input.threadId,
2505
+ runId,
2506
+ outcome,
2507
+ ...error !== void 0 ? {
2508
+ error
2509
+ } : {}
2510
+ }, (next, nextRunId) => startNext(next, nextRunId));
2511
+ if (frame !== void 0) await writer.write(encodeStreamEvent2(frame));
2512
+ } catch (failure) {
2513
+ this.logger.error(`could not advance the queue of thread ${input.threadId} after run ${runId}: ${failure instanceof Error ? failure.message : String(failure)}`);
2514
+ }
2515
+ }
2516
+ };
2517
+ OpenCodeTurns = _ts_decorate2([
2518
+ Injectable(),
2519
+ _ts_param2(0, Inject2(OPENCODE_HOST)),
2520
+ _ts_param2(1, Inject2(OPENCODE_SESSIONS)),
2521
+ _ts_param2(2, Inject2(OPENCODE_OPTIONS)),
2522
+ _ts_param2(3, Inject2(AGENT_STORE)),
2523
+ _ts_param2(4, Inject2(AGENT_SINK)),
2524
+ _ts_param2(5, Inject2(AGENT_DEPS_FACTORY)),
2525
+ _ts_param2(6, Inject2(AGENT_TOOL_REGISTRY)),
2526
+ _ts_param2(7, Optional()),
2527
+ _ts_param2(7, Inject2(AGENT_SKILLS)),
2528
+ _ts_param2(8, Optional()),
2529
+ _ts_param2(8, Inject2(AGENT_MEMORY)),
2530
+ _ts_param2(9, Optional()),
2531
+ _ts_param2(9, Inject2(AGENT_CHAT_QUEUE)),
2532
+ _ts_param2(10, Optional()),
2533
+ _ts_param2(10, Inject2(OPENCODE_TOOLS_TOKENS)),
2534
+ _ts_metadata2("design:type", Function),
2535
+ _ts_metadata2("design:paramtypes", [
2536
+ typeof OpenCodeHost === "undefined" ? Object : OpenCodeHost,
2537
+ typeof OpenCodeSessionStore === "undefined" ? Object : OpenCodeSessionStore,
2538
+ typeof OpenCodeEngineSettings === "undefined" ? Object : OpenCodeEngineSettings,
2539
+ typeof AgentStore === "undefined" ? Object : AgentStore,
2540
+ typeof TokenStreamSink === "undefined" ? Object : TokenStreamSink,
2541
+ typeof AgentDepsFactory === "undefined" ? Object : AgentDepsFactory,
2542
+ typeof ToolRegistry === "undefined" ? Object : ToolRegistry,
2543
+ typeof SkillsConfig === "undefined" ? Object : SkillsConfig,
2544
+ typeof MemoryConfig === "undefined" ? Object : MemoryConfig,
2545
+ typeof ChatQueueService === "undefined" ? Object : ChatQueueService,
2546
+ Object
2547
+ ])
2548
+ ], OpenCodeTurns);
2549
+ function titleOf(text) {
2550
+ const trimmed = text.trim().replace(/\s+/g, " ");
2551
+ return trimmed.length > 60 ? `${trimmed.slice(0, 57)}...` : trimmed || "New chat";
2552
+ }
2553
+ __name(titleOf, "titleOf");
2554
+ function transcriptOf(messages, limit) {
2555
+ return messages.filter((m) => (m.role === "user" || m.role === "assistant") && m.content.trim()).slice(-limit).map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content.slice(0, 2e3)}`).join("\n");
2556
+ }
2557
+ __name(transcriptOf, "transcriptOf");
2558
+ var SESSION_META_KEY = "ai.opencode/sessionID";
2559
+ function sessionOfMeta(meta) {
2560
+ const value = meta?.[SESSION_META_KEY];
2561
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2562
+ }
2563
+ __name(sessionOfMeta, "sessionOfMeta");
2564
+ function uiEmitter(scope, push) {
2565
+ let next = 0;
2566
+ return async (component, props, options = {}) => {
2567
+ if (typeof component !== "string" || component.length === 0) {
2568
+ throw new Error("emitUi: component must be a non-empty string");
2569
+ }
2570
+ if (typeof props !== "object" || props === null || Array.isArray(props)) {
2571
+ throw new Error("emitUi: props must be a JSON object");
2572
+ }
2573
+ const id = options.id ?? `${scope}:ui:${next++}`;
2574
+ await push({
2575
+ id,
2576
+ component,
2577
+ props: JSON.parse(JSON.stringify(props)),
2578
+ ...options.version !== void 0 ? {
2579
+ version: options.version
2580
+ } : {}
2581
+ });
2582
+ return {
2583
+ id
2584
+ };
2585
+ };
2586
+ }
2587
+ __name(uiEmitter, "uiEmitter");
2588
+
2589
+ // src/runner.ts
2590
+ function _ts_decorate3(decorators, target, key, desc) {
2591
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2592
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
2593
+ r = Reflect.decorate(decorators, target, key, desc);
2594
+ } else {
2595
+ for (var i = decorators.length - 1; i >= 0; i--) {
2596
+ if (d = decorators[i]) {
2597
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2598
+ }
2599
+ }
2600
+ }
2601
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2602
+ }
2603
+ __name(_ts_decorate3, "_ts_decorate");
2604
+ function _ts_metadata3(metadataKey, metadataValue) {
2605
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
2606
+ return Reflect.metadata(metadataKey, metadataValue);
2607
+ }
2608
+ }
2609
+ __name(_ts_metadata3, "_ts_metadata");
2610
+ function _ts_param3(paramIndex, decorator) {
2611
+ return function(target, key) {
2612
+ decorator(target, key, paramIndex);
2613
+ };
2614
+ }
2615
+ __name(_ts_param3, "_ts_param");
2616
+ var OpenCodeAgentRunner = class {
2617
+ static {
2618
+ __name(this, "OpenCodeAgentRunner");
2619
+ }
2620
+ turns;
2621
+ settings;
2622
+ live = /* @__PURE__ */ new Set();
2623
+ cancelled = /* @__PURE__ */ new Set();
2624
+ pending = /* @__PURE__ */ new Map();
2625
+ constructor(turns, settings) {
2626
+ this.turns = turns;
2627
+ this.settings = settings;
2628
+ turns.startNext = (next, runId) => this.start(next, {
2629
+ runId
2630
+ });
2631
+ }
2632
+ runIdFor(input) {
2633
+ return this.settings.runId?.(input) ?? crypto.randomUUID();
2634
+ }
2635
+ async isRunActive(runId) {
2636
+ if (this.live.has(runId)) return true;
2637
+ for (const key of this.pending.keys()) if (key.startsWith(`${runId}:`)) return true;
2638
+ return false;
2639
+ }
2640
+ async start(input, options = {}) {
2641
+ const runId = options.runId ?? this.runIdFor(input);
2642
+ this.live.add(runId);
2643
+ void this.run(runId, input).finally(() => {
2644
+ this.live.delete(runId);
2645
+ this.cancelled.delete(runId);
2646
+ for (const key of [
2647
+ ...this.pending.keys()
2648
+ ]) if (key.startsWith(`${runId}:`)) this.pending.delete(key);
2649
+ });
2650
+ return {
2651
+ runId
2652
+ };
2653
+ }
2654
+ /**
2655
+ * Deliver a person's reply. Answers addressed at an approval are refused
2656
+ * ({@link OpenCodeReplyMismatchError}, a 409) and the approval keeps waiting: they say nothing
2657
+ * about whether the action should run.
2658
+ */
2659
+ async signal(runId, toolCallId, reply) {
2660
+ const key = `${runId}:${toolCallId}`;
2661
+ const waiter = this.pending.get(key);
2662
+ if (waiter === void 0) return;
2663
+ if (waiter.on === "approval" && !isDecision(reply)) {
2664
+ throw new OpenCodeReplyMismatchError(runId, toolCallId);
2665
+ }
2666
+ this.pending.delete(key);
2667
+ waiter.resolve(reply);
2668
+ }
2669
+ /**
2670
+ * Stop the run: what it waits on a person for is dropped, and OpenCode is interrupted — it answers
2671
+ * `session.execution.interrupted`, which settles the run as cancelled.
2672
+ */
2673
+ async cancel(runId) {
2674
+ this.cancelled.add(runId);
2675
+ for (const [key, waiter] of [
2676
+ ...this.pending
2677
+ ]) {
2678
+ if (!key.startsWith(`${runId}:`)) continue;
2679
+ this.pending.delete(key);
2680
+ waiter.reject(new RunCancelledError());
2681
+ }
2682
+ const input = this.inputs.get(runId);
2683
+ if (input !== void 0) await this.turns.interrupt(input).catch(() => void 0);
2684
+ }
2685
+ inputs = /* @__PURE__ */ new Map();
2686
+ async run(runId, input) {
2687
+ const started = Date.now();
2688
+ this.inputs.set(runId, input);
2689
+ try {
2690
+ let handle = await this.turns.begin(runId, input);
2691
+ if (this.cancelled.has(runId)) throw new RunCancelledError();
2692
+ await this.turns.prompt(runId, input, handle);
2693
+ for (; ; ) {
2694
+ const milestone = await this.turns.observe(runId, input, handle);
2695
+ if (milestone.kind === "finished") {
2696
+ await this.turns.settle(runId, input, milestone.outcome, Date.now() - started);
2697
+ return;
2698
+ }
2699
+ const reply = await this.park(runId, milestone.ask.id, milestone.ask.kind === "approval" ? "approval" : "answers", milestone.timeoutMs);
2700
+ handle = await this.turns.reply(runId, input, handle, milestone.ask, reply);
2701
+ }
2702
+ } catch (error) {
2703
+ if (error instanceof RunCancelledError) {
2704
+ await this.turns.interrupt(input).catch(() => void 0);
2705
+ await this.turns.settle(runId, input, {
2706
+ status: "interrupted"
2707
+ }, Date.now() - started);
2708
+ return;
2709
+ }
2710
+ await this.turns.settleFailed(runId, input, errorText(error, "the turn failed"));
2711
+ } finally {
2712
+ this.inputs.delete(runId);
2713
+ }
2714
+ }
2715
+ /** Wait for a person's answer; a lapsed approval answers itself as expired. */
2716
+ park(runId, toolCallId, on, timeoutMs) {
2717
+ const key = `${runId}:${toolCallId}`;
2718
+ const waiting = new Promise((resolve, reject) => {
2719
+ this.pending.set(key, {
2720
+ on,
2721
+ resolve,
2722
+ reject
2723
+ });
2724
+ });
2725
+ if (timeoutMs === void 0) return waiting;
2726
+ let timer;
2727
+ const lapse = new Promise((resolve) => {
2728
+ timer = setTimeout(() => {
2729
+ if (!this.pending.has(key)) return;
2730
+ this.pending.delete(key);
2731
+ resolve({
2732
+ approved: false,
2733
+ expired: true
2734
+ });
2735
+ }, timeoutMs);
2736
+ timer.unref?.();
2737
+ });
2738
+ return Promise.race([
2739
+ waiting,
2740
+ lapse
2741
+ ]).finally(() => clearTimeout(timer));
2742
+ }
2743
+ };
2744
+ OpenCodeAgentRunner = _ts_decorate3([
2745
+ Injectable2(),
2746
+ _ts_param3(1, Inject3(OPENCODE_OPTIONS)),
2747
+ _ts_metadata3("design:type", Function),
2748
+ _ts_metadata3("design:paramtypes", [
2749
+ typeof OpenCodeTurns === "undefined" ? Object : OpenCodeTurns,
2750
+ typeof OpenCodeEngineSettings === "undefined" ? Object : OpenCodeEngineSettings
2751
+ ])
2752
+ ], OpenCodeAgentRunner);
2753
+
2754
+ // src/engine.ts
2755
+ function isInstance(value, method) {
2756
+ return typeof value === "object" && value !== null && typeof value[method] === "function";
2757
+ }
2758
+ __name(isInstance, "isInstance");
2759
+ function bind(token, value, method) {
2760
+ if (isInstance(value, method)) return [
2761
+ {
2762
+ provide: token,
2763
+ useValue: value
2764
+ }
2765
+ ];
2766
+ if (typeof value === "function") {
2767
+ return [
2768
+ value,
2769
+ {
2770
+ provide: token,
2771
+ useExisting: value
2772
+ }
2773
+ ];
2774
+ }
2775
+ return [
2776
+ {
2777
+ provide: token,
2778
+ useExisting: value
2779
+ }
2780
+ ];
2781
+ }
2782
+ __name(bind, "bind");
2783
+ function openCodeControllers(options) {
2784
+ return options.tools !== void 0 ? [
2785
+ OpenCodeMcpController
2786
+ ] : [];
2787
+ }
2788
+ __name(openCodeControllers, "openCodeControllers");
2789
+ var DEFAULT_TOOLS_TTL_MS2 = 7 * 24 * 60 * 6e4;
2790
+ function toolsTokens(settings) {
2791
+ const tools = settings.tools;
2792
+ if (tools === void 0) return null;
2793
+ if (tools.secret === void 0) {
2794
+ new Logger3("OpenCodeEngine").warn("`tools` has no `secret`: the tools endpoint signs its tokens with a per-process secret, so it only works with one process (and sessions lose their tools on a restart until their next turn).");
2795
+ }
2796
+ return new OpenCodeToolsTokens(tools.secret, tools.ttlMs ?? DEFAULT_TOOLS_TTL_MS2);
2797
+ }
2798
+ __name(toolsTokens, "toolsTokens");
2799
+ function openCodeProviders(options) {
2800
+ const { host, sessions, ...settings } = options;
2801
+ return [
2802
+ ...bind(OPENCODE_HOST, host, "server"),
2803
+ ...bind(OPENCODE_SESSIONS, sessions ?? new InMemoryOpenCodeSessionStore(), "get"),
2804
+ {
2805
+ provide: OPENCODE_OPTIONS,
2806
+ useValue: settings
2807
+ },
2808
+ {
2809
+ provide: OPENCODE_TOOLS_TOKENS,
2810
+ useFactory: /* @__PURE__ */ __name(() => toolsTokens(settings), "useFactory")
2811
+ },
2812
+ OpenCodeTurns,
2813
+ {
2814
+ provide: OPENCODE_TURNS,
2815
+ useExisting: OpenCodeTurns
2816
+ },
2817
+ {
2818
+ provide: OPENCODE_MCP_ENDPOINT,
2819
+ useFactory: /* @__PURE__ */ __name((turns, tokens) => tokens === null ? null : new OpenCodeMcpEndpoint2(turns, tokens), "useFactory"),
2820
+ inject: [
2821
+ OpenCodeTurns,
2822
+ OPENCODE_TOOLS_TOKENS
2823
+ ]
2824
+ }
2825
+ ];
2826
+ }
2827
+ __name(openCodeProviders, "openCodeProviders");
2828
+
2829
+ // src/durable/runner.ts
2830
+ import { randomUUID as randomUUID2 } from "crypto";
2831
+ import { AGENT_STORE as AGENT_STORE2 } from "@dudousxd/nestjs-agent-core";
2832
+ import { RUN_GATEWAY as RUN_GATEWAY2, WorkflowService } from "@dudousxd/nestjs-durable";
2833
+ import { isWorkflowControlFlowSignal as isWorkflowControlFlowSignal2 } from "@dudousxd/nestjs-durable-core";
2834
+ import { Inject as Inject5, Injectable as Injectable4, Logger as Logger4 } from "@nestjs/common";
2835
+
2836
+ // src/durable/workflow.ts
2837
+ import { RUN_GATEWAY, Workflow } from "@dudousxd/nestjs-durable";
2838
+ import { SignalTimeoutError, isWorkflowControlFlowSignal } from "@dudousxd/nestjs-durable-core";
2839
+ import { Inject as Inject4, Injectable as Injectable3, Optional as Optional2 } from "@nestjs/common";
2840
+ function _ts_decorate4(decorators, target, key, desc) {
2841
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2842
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
2843
+ r = Reflect.decorate(decorators, target, key, desc);
2844
+ } else {
2845
+ for (var i = decorators.length - 1; i >= 0; i--) {
2846
+ if (d = decorators[i]) {
2847
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2848
+ }
2849
+ }
2850
+ }
2851
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2852
+ }
2853
+ __name(_ts_decorate4, "_ts_decorate");
2854
+ function _ts_metadata4(metadataKey, metadataValue) {
2855
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
2856
+ return Reflect.metadata(metadataKey, metadataValue);
2857
+ }
2858
+ }
2859
+ __name(_ts_metadata4, "_ts_metadata");
2860
+ function _ts_param4(paramIndex, decorator) {
2861
+ return function(target, key) {
2862
+ decorator(target, key, paramIndex);
2863
+ };
2864
+ }
2865
+ __name(_ts_param4, "_ts_param");
2866
+ var OPENCODE_RUN_WORKFLOW = "agent.opencode.run";
2867
+ function decisionToken(runId, toolCallId) {
2868
+ return `tool:${runId}:${toolCallId}`;
2869
+ }
2870
+ __name(decisionToken, "decisionToken");
2871
+ var OpenCodeRunWorkflow = class {
2872
+ static {
2873
+ __name(this, "OpenCodeRunWorkflow");
2874
+ }
2875
+ turns;
2876
+ runs;
2877
+ constructor(turns, runs) {
2878
+ this.turns = turns;
2879
+ this.runs = runs;
2880
+ }
2881
+ async run(ctx, input) {
2882
+ try {
2883
+ const begun = await ctx.localStep("begin", async () => ({
2884
+ handle: await this.turns.begin(ctx.runId, input),
2885
+ startedAt: Date.now()
2886
+ }));
2887
+ let handle = begun.handle;
2888
+ await ctx.localStep("prompt", async () => {
2889
+ await this.turns.prompt(ctx.runId, input, handle);
2890
+ return true;
2891
+ });
2892
+ for (let n = 0; ; n += 1) {
2893
+ const milestone = await ctx.localStep(`observe:${n}`, () => this.turns.observe(ctx.runId, input, handle));
2894
+ if (milestone.kind === "finished") {
2895
+ await ctx.localStep("finish", async () => {
2896
+ if (await this.cancelled(ctx.runId)) this.turns.drop(ctx.runId);
2897
+ else await this.turns.settle(ctx.runId, input, milestone.outcome, Date.now() - begun.startedAt);
2898
+ return true;
2899
+ });
2900
+ return {
2901
+ outcome: milestone.outcome.status
2902
+ };
2903
+ }
2904
+ let reply;
2905
+ try {
2906
+ reply = await ctx.waitForSignal(decisionToken(ctx.runId, milestone.ask.id), milestone.timeoutMs !== void 0 ? {
2907
+ timeoutMs: milestone.timeoutMs
2908
+ } : void 0);
2909
+ } catch (error) {
2910
+ if (!(error instanceof SignalTimeoutError)) throw error;
2911
+ reply = {
2912
+ approved: false,
2913
+ expired: true
2914
+ };
2915
+ }
2916
+ const ask = milestone.ask;
2917
+ handle = await ctx.localStep(`reply:${n}`, () => this.turns.reply(ctx.runId, input, handle, ask, reply));
2918
+ }
2919
+ } catch (error) {
2920
+ if (isWorkflowControlFlowSignal(error)) throw error;
2921
+ await ctx.localStep("fail", async () => {
2922
+ await this.turns.settleFailed(ctx.runId, input, errorText(error, "the turn failed"));
2923
+ return true;
2924
+ });
2925
+ return {
2926
+ outcome: "failed"
2927
+ };
2928
+ }
2929
+ }
2930
+ async cancelled(runId) {
2931
+ try {
2932
+ const status = (await this.runs?.getRunDetail(runId))?.run.status;
2933
+ return status === "cancelling" || status === "cancelled";
2934
+ } catch {
2935
+ return false;
2936
+ }
2937
+ }
2938
+ };
2939
+ OpenCodeRunWorkflow = _ts_decorate4([
2940
+ Injectable3(),
2941
+ Workflow({
2942
+ name: OPENCODE_RUN_WORKFLOW,
2943
+ version: "1"
2944
+ }),
2945
+ _ts_param4(1, Optional2()),
2946
+ _ts_param4(1, Inject4(RUN_GATEWAY)),
2947
+ _ts_metadata4("design:type", Function),
2948
+ _ts_metadata4("design:paramtypes", [
2949
+ typeof OpenCodeTurns === "undefined" ? Object : OpenCodeTurns,
2950
+ typeof RunGateway === "undefined" ? Object : RunGateway
2951
+ ])
2952
+ ], OpenCodeRunWorkflow);
2953
+
2954
+ // src/durable/runner.ts
2955
+ function _ts_decorate5(decorators, target, key, desc) {
2956
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2957
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
2958
+ r = Reflect.decorate(decorators, target, key, desc);
2959
+ } else {
2960
+ for (var i = decorators.length - 1; i >= 0; i--) {
2961
+ if (d = decorators[i]) {
2962
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
2963
+ }
2964
+ }
2965
+ }
2966
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
2967
+ }
2968
+ __name(_ts_decorate5, "_ts_decorate");
2969
+ function _ts_metadata5(metadataKey, metadataValue) {
2970
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
2971
+ return Reflect.metadata(metadataKey, metadataValue);
2972
+ }
2973
+ }
2974
+ __name(_ts_metadata5, "_ts_metadata");
2975
+ function _ts_param5(paramIndex, decorator) {
2976
+ return function(target, key) {
2977
+ decorator(target, key, paramIndex);
2978
+ };
2979
+ }
2980
+ __name(_ts_param5, "_ts_param");
2981
+ function isRunInput(value) {
2982
+ return typeof value === "object" && value !== null && typeof value.threadId === "string" && typeof value.actor === "object";
2983
+ }
2984
+ __name(isRunInput, "isRunInput");
2985
+ var DurableOpenCodeAgentRunner = class _DurableOpenCodeAgentRunner {
2986
+ static {
2987
+ __name(this, "DurableOpenCodeAgentRunner");
2988
+ }
2989
+ workflows;
2990
+ runs;
2991
+ turns;
2992
+ settings;
2993
+ host;
2994
+ store;
2995
+ logger = new Logger4(_DurableOpenCodeAgentRunner.name);
2996
+ constructor(workflows, runs, turns, settings, host, store) {
2997
+ this.workflows = workflows;
2998
+ this.runs = runs;
2999
+ this.turns = turns;
3000
+ this.settings = settings;
3001
+ this.host = host;
3002
+ this.store = store;
3003
+ turns.startNext = (next, runId) => this.start(next, {
3004
+ runId
3005
+ });
3006
+ }
3007
+ runIdFor(input) {
3008
+ return this.settings.runId?.(input) ?? randomUUID2();
3009
+ }
3010
+ async start(input, options = {}) {
3011
+ const runId = options.runId ?? this.runIdFor(input);
3012
+ const durable = this.settings.durable;
3013
+ const startOptions = {
3014
+ ...await durable?.start?.(input, runId) ?? {},
3015
+ ...await this.host.startOptions?.(input, runId) ?? {}
3016
+ };
3017
+ try {
3018
+ await this.workflows.start(OpenCodeRunWorkflow, input, runId, startOptions);
3019
+ } catch (error) {
3020
+ if (isWorkflowControlFlowSignal2(error)) return {
3021
+ runId
3022
+ };
3023
+ throw this.host.startError?.(error, input) ?? durable?.startError?.(error, input) ?? error;
3024
+ }
3025
+ return {
3026
+ runId
3027
+ };
3028
+ }
3029
+ async isRunActive(runId) {
3030
+ try {
3031
+ const status = (await this.runs.getRunDetail(runId))?.run.status;
3032
+ return !(status === "completed" || status === "failed" || status === "cancelled" || status === "dead");
3033
+ } catch {
3034
+ return true;
3035
+ }
3036
+ }
3037
+ /**
3038
+ * Signal a person's reply into the run. Answers addressed at an approval (a call recorded with an
3039
+ * approver; questions have none) are refused ({@link OpenCodeReplyMismatchError}, a 409) before
3040
+ * they reach the workflow, which would otherwise spend its wait on them.
3041
+ */
3042
+ async signal(runId, toolCallId, reply) {
3043
+ if (!isDecision(reply)) {
3044
+ const call = await this.store.toolCallApproval?.(toolCallId);
3045
+ if (call != null && call.approver !== null) {
3046
+ throw new OpenCodeReplyMismatchError(runId, toolCallId);
3047
+ }
3048
+ }
3049
+ await this.workflows.signal(decisionToken(runId, toolCallId), reply);
3050
+ }
3051
+ /**
3052
+ * Interrupt OpenCode, cancel the run in the runtime (a run parked on a person never runs its body
3053
+ * again), and settle it here: the queue, the thread, the `cancelled` frame and the run row.
3054
+ */
3055
+ async cancel(runId) {
3056
+ this.logger.log(`cancelling agent run ${runId}`);
3057
+ const input = await this.runs.getRunDetail(runId).then((detail) => detail?.run.input).catch(() => void 0);
3058
+ if (isRunInput(input)) await this.turns.interrupt(input).catch(() => void 0);
3059
+ await this.runs.cancel(runId, {
3060
+ compensate: true
3061
+ });
3062
+ if (isRunInput(input)) await this.turns.settle(runId, input, {
3063
+ status: "interrupted"
3064
+ }, 0);
3065
+ }
3066
+ };
3067
+ DurableOpenCodeAgentRunner = _ts_decorate5([
3068
+ Injectable4(),
3069
+ _ts_param5(1, Inject5(RUN_GATEWAY2)),
3070
+ _ts_param5(3, Inject5(OPENCODE_OPTIONS)),
3071
+ _ts_param5(4, Inject5(OPENCODE_HOST)),
3072
+ _ts_param5(5, Inject5(AGENT_STORE2)),
3073
+ _ts_metadata5("design:type", Function),
3074
+ _ts_metadata5("design:paramtypes", [
3075
+ typeof WorkflowService === "undefined" ? Object : WorkflowService,
3076
+ typeof RunGateway === "undefined" ? Object : RunGateway,
3077
+ typeof OpenCodeTurns === "undefined" ? Object : OpenCodeTurns,
3078
+ typeof OpenCodeEngineSettings === "undefined" ? Object : OpenCodeEngineSettings,
3079
+ typeof OpenCodeHost === "undefined" ? Object : OpenCodeHost,
3080
+ typeof AgentStore === "undefined" ? Object : AgentStore
3081
+ ])
3082
+ ], DurableOpenCodeAgentRunner);
3083
+
3084
+ // src/durable/index.ts
3085
+ function openCodeDurable(options) {
3086
+ return {
3087
+ name: "opencode-durable",
3088
+ providers: [
3089
+ ...openCodeProviders(options),
3090
+ OpenCodeRunWorkflow,
3091
+ DurableOpenCodeAgentRunner
3092
+ ],
3093
+ runner: DurableOpenCodeAgentRunner,
3094
+ exports: [
3095
+ OpenCodeTurns,
3096
+ OPENCODE_TURNS
3097
+ ],
3098
+ controllers: openCodeControllers(options)
3099
+ };
3100
+ }
3101
+ __name(openCodeDurable, "openCodeDurable");
3102
+ export {
3103
+ DurableOpenCodeAgentRunner,
3104
+ OPENCODE_RUN_WORKFLOW,
3105
+ OpenCodeRunWorkflow,
3106
+ decisionToken,
3107
+ openCodeDurable
3108
+ };
3109
+ //# sourceMappingURL=index.js.map