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