@dudousxd/nestjs-agent-opencode 0.0.0-stage → 0.1.0

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