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