@dudousxd/nestjs-agent-opencode 0.3.0 → 0.4.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/README.md +10 -3
- package/dist/durable/index.cjs +283 -14
- package/dist/durable/index.cjs.map +1 -1
- package/dist/durable/index.d.cts +3 -2
- package/dist/durable/index.d.ts +3 -2
- package/dist/durable/index.js +284 -15
- package/dist/durable/index.js.map +1 -1
- package/dist/{engine-Cp5uysaN.d.cts → engine-BX6MLzxs.d.cts} +109 -10
- package/dist/{engine-Cp5uysaN.d.ts → engine-BX6MLzxs.d.ts} +109 -10
- package/dist/index.cjs +279 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +279 -13
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -94,8 +94,9 @@ the dashboard and `GET /quota` see an OpenCode turn like any other.
|
|
|
94
94
|
the turn's last message) is added up from the turn's own calls, not read back from the store: each
|
|
95
95
|
milestone carries what was spent since the last one, and a durable run journals it with the
|
|
96
96
|
milestone, so a run resumed in another process (an approval answered after a restart) still reports
|
|
97
|
-
the whole run, cache tokens included. A process that dies while OpenCode works
|
|
98
|
-
saw
|
|
97
|
+
the whole run, cache tokens included. A process that dies while OpenCode works does not lose the
|
|
98
|
+
calls it saw: the `observe` re-run reads each step's usage back from the session's messages (see
|
|
99
|
+
below).
|
|
99
100
|
|
|
100
101
|
## Durable turns
|
|
101
102
|
|
|
@@ -109,7 +110,13 @@ AgentModule.forRoot({ engine: openCodeDurable({ host: MyOpenCodeHost, sessions:
|
|
|
109
110
|
Every step of a turn is checkpointed (`begin → prompt → observe → [wait for a person → reply →
|
|
110
111
|
observe]* → finish`) and a person's decision is a durable signal: a turn waiting on an approval
|
|
111
112
|
survives restarts and is resumed by whichever process gets the decision. If OpenCode restarted
|
|
112
|
-
meanwhile, a new session is opened with the conversation and the decision.
|
|
113
|
+
meanwhile, a new session is opened with the conversation and the decision. If the API restarted
|
|
114
|
+
while OpenCode worked (its `observe` step re-runs), the rebuilt turn catches up on what OpenCode
|
|
115
|
+
asked meanwhile and, when the execution ends, on what it said: the text, tool calls and results and
|
|
116
|
+
each step's usage are read back from the session's messages after the turn's prompt (the prompt
|
|
117
|
+
carries the run id in its `metadata`), less what was already taken before the restart (journaled
|
|
118
|
+
with each ask). A session that left no answer at all fails the turn (`LOST_ANSWER_ERROR`) instead
|
|
119
|
+
of settling it as an empty success. It needs the client's `message.list`. `openCode()` runs the
|
|
113
120
|
same steps in memory (single replica). Several processes need a cross-process sink and a persistent
|
|
114
121
|
`OpenCodeSessionStore`.
|
|
115
122
|
|
package/dist/durable/index.cjs
CHANGED
|
@@ -154,6 +154,7 @@ OpenCodeMcpController = _ts_decorate([
|
|
|
154
154
|
// src/mcp.ts
|
|
155
155
|
var import_node_crypto = require("crypto");
|
|
156
156
|
var import_nestjs_agent_core3 = require("@dudousxd/nestjs-agent-core");
|
|
157
|
+
var import_genui = require("@dudousxd/nestjs-agent-core/genui");
|
|
157
158
|
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
158
159
|
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
159
160
|
var import_zod_json_schema_compat = require("@modelcontextprotocol/sdk/server/zod-json-schema-compat.js");
|
|
@@ -284,6 +285,9 @@ function subUsage(a, b) {
|
|
|
284
285
|
};
|
|
285
286
|
}
|
|
286
287
|
__name(subUsage, "subUsage");
|
|
288
|
+
var LOST_ANSWER_ERROR = "The answer was lost while the agent restarted; ask again.";
|
|
289
|
+
var RECOVER_PAGES = 10;
|
|
290
|
+
var RECOVER_PAGE_SIZE = 100;
|
|
287
291
|
var ASK_TOOL_NAME = "ask";
|
|
288
292
|
var FAILED_STATUSES = /* @__PURE__ */ new Set([
|
|
289
293
|
"error",
|
|
@@ -372,6 +376,14 @@ var OpenCodeTurn = class {
|
|
|
372
376
|
wroteMessage = false;
|
|
373
377
|
ended = false;
|
|
374
378
|
stepError;
|
|
379
|
+
/** What this turn object took from the session's assistant messages (see {@link TurnProgress}). */
|
|
380
|
+
took = {
|
|
381
|
+
text: /* @__PURE__ */ new Map(),
|
|
382
|
+
reasoning: /* @__PURE__ */ new Map(),
|
|
383
|
+
called: /* @__PURE__ */ new Set(),
|
|
384
|
+
settled: /* @__PURE__ */ new Set(),
|
|
385
|
+
steps: /* @__PURE__ */ new Set()
|
|
386
|
+
};
|
|
375
387
|
constructor(a) {
|
|
376
388
|
this.a = a;
|
|
377
389
|
this.model = modelOfLabel(a.input.model);
|
|
@@ -389,6 +401,30 @@ var OpenCodeTurn = class {
|
|
|
389
401
|
tokens: info.tokens
|
|
390
402
|
};
|
|
391
403
|
}
|
|
404
|
+
/** What the turn took so far: carried from before this turn object, plus what it took. */
|
|
405
|
+
progress() {
|
|
406
|
+
const before = this.a.progress;
|
|
407
|
+
const merged = /* @__PURE__ */ __name((carried, own) => {
|
|
408
|
+
const out = {
|
|
409
|
+
...carried ?? {}
|
|
410
|
+
};
|
|
411
|
+
for (const [id, n] of own) out[id] = (out[id] ?? 0) + n;
|
|
412
|
+
return out;
|
|
413
|
+
}, "merged");
|
|
414
|
+
const union = /* @__PURE__ */ __name((carried, own) => [
|
|
415
|
+
.../* @__PURE__ */ new Set([
|
|
416
|
+
...carried ?? [],
|
|
417
|
+
...own
|
|
418
|
+
])
|
|
419
|
+
], "union");
|
|
420
|
+
return {
|
|
421
|
+
text: merged(before?.text, this.took.text),
|
|
422
|
+
reasoning: merged(before?.reasoning, this.took.reasoning),
|
|
423
|
+
called: union(before?.called, this.took.called),
|
|
424
|
+
settled: union(before?.settled, this.took.settled),
|
|
425
|
+
steps: union(before?.steps, this.took.steps)
|
|
426
|
+
};
|
|
427
|
+
}
|
|
392
428
|
/** What the run spent so far: carried from before this turn object, plus what it saw. */
|
|
393
429
|
totalUsage() {
|
|
394
430
|
return addUsage(this.a.carried, this.spent);
|
|
@@ -479,7 +515,11 @@ var OpenCodeTurn = class {
|
|
|
479
515
|
this.reported = {
|
|
480
516
|
...this.spent
|
|
481
517
|
};
|
|
482
|
-
const milestone = {
|
|
518
|
+
const milestone = reached.kind === "ask" ? {
|
|
519
|
+
...reached,
|
|
520
|
+
usage,
|
|
521
|
+
progress: this.progress()
|
|
522
|
+
} : {
|
|
483
523
|
...reached,
|
|
484
524
|
usage
|
|
485
525
|
};
|
|
@@ -534,6 +574,7 @@ var OpenCodeTurn = class {
|
|
|
534
574
|
innerSettled: /* @__PURE__ */ new Set()
|
|
535
575
|
};
|
|
536
576
|
this.tools.set(id, state);
|
|
577
|
+
this.took.called.add(id);
|
|
537
578
|
this.segment.calls.push({
|
|
538
579
|
id,
|
|
539
580
|
name,
|
|
@@ -661,6 +702,7 @@ var OpenCodeTurn = class {
|
|
|
661
702
|
case "session.text.delta": {
|
|
662
703
|
const delta = typeof d.delta === "string" ? d.delta : "";
|
|
663
704
|
if (!delta) return;
|
|
705
|
+
this.count(this.took.text, d.assistantMessageID, delta.length);
|
|
664
706
|
this.endReasoning();
|
|
665
707
|
if (!this.sawText && !delta.trim()) return;
|
|
666
708
|
await this.step();
|
|
@@ -682,6 +724,7 @@ ${delta}` : delta;
|
|
|
682
724
|
case "session.reasoning.delta": {
|
|
683
725
|
const delta = typeof d.delta === "string" ? d.delta : "";
|
|
684
726
|
if (!delta) return;
|
|
727
|
+
this.count(this.took.reasoning, d.assistantMessageID, delta.length);
|
|
685
728
|
this.reasoningSince ??= Date.now();
|
|
686
729
|
await this.step();
|
|
687
730
|
this.segment.reasoning += delta;
|
|
@@ -736,6 +779,7 @@ ${delta}` : delta;
|
|
|
736
779
|
}
|
|
737
780
|
case "session.step.ended":
|
|
738
781
|
this.endReasoning();
|
|
782
|
+
if (typeof d.assistantMessageID === "string") this.took.steps.add(d.assistantMessageID);
|
|
739
783
|
await this.stepEnded(d);
|
|
740
784
|
return;
|
|
741
785
|
case "session.step.failed":
|
|
@@ -776,10 +820,16 @@ ${delta}` : delta;
|
|
|
776
820
|
return;
|
|
777
821
|
}
|
|
778
822
|
}
|
|
823
|
+
/** Characters taken from an assistant message's text or reasoning. */
|
|
824
|
+
count(into, messageId, n) {
|
|
825
|
+
if (typeof messageId !== "string") return;
|
|
826
|
+
into.set(messageId, (into.get(messageId) ?? 0) + n);
|
|
827
|
+
}
|
|
779
828
|
async settleTool(id, ok, error = "failed") {
|
|
780
829
|
const state = this.tool(id);
|
|
781
830
|
if (state.settled) return;
|
|
782
831
|
state.settled = true;
|
|
832
|
+
this.took.settled.add(id);
|
|
783
833
|
if (ok) {
|
|
784
834
|
this.segment.results.push({
|
|
785
835
|
id,
|
|
@@ -1329,9 +1379,171 @@ ${delta}` : delta;
|
|
|
1329
1379
|
cost: cost < 1e-9 ? 0 : cost
|
|
1330
1380
|
}, this.compacted ? "history_summary" : "title");
|
|
1331
1381
|
}
|
|
1332
|
-
|
|
1382
|
+
/**
|
|
1383
|
+
* The session's messages since this turn's prompt, oldest first: everything after its newest user
|
|
1384
|
+
* message. `undefined` when they cannot be read (no `message.list`, no prompt within reach); empty
|
|
1385
|
+
* when the newest prompt is another run's (this turn's never reached the session).
|
|
1386
|
+
*/
|
|
1387
|
+
async messagesSincePrompt() {
|
|
1388
|
+
const messages = this.a.client.message;
|
|
1389
|
+
if (messages === void 0) return void 0;
|
|
1390
|
+
const since = [];
|
|
1391
|
+
let cursor;
|
|
1392
|
+
for (let page = 0; page < RECOVER_PAGES; page += 1) {
|
|
1393
|
+
const listed = await messages.list({
|
|
1394
|
+
sessionID: this.a.sessionId,
|
|
1395
|
+
order: "desc",
|
|
1396
|
+
limit: RECOVER_PAGE_SIZE,
|
|
1397
|
+
...cursor !== void 0 ? {
|
|
1398
|
+
cursor
|
|
1399
|
+
} : {}
|
|
1400
|
+
});
|
|
1401
|
+
for (const message of listed.data) {
|
|
1402
|
+
if (message.type === "user") {
|
|
1403
|
+
const runId = message.metadata?.runId;
|
|
1404
|
+
if (typeof runId === "string" && runId !== this.a.runId) return [];
|
|
1405
|
+
return since.reverse();
|
|
1406
|
+
}
|
|
1407
|
+
since.push(message);
|
|
1408
|
+
}
|
|
1409
|
+
const next = listed.cursor?.next;
|
|
1410
|
+
if (typeof next !== "string" || next.length === 0 || listed.data.length === 0) break;
|
|
1411
|
+
cursor = next;
|
|
1412
|
+
}
|
|
1413
|
+
return void 0;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* A turn rebuilt after a restart heard only what the session said once it re-attached: what it
|
|
1417
|
+
* said in between — read back from the session's messages since the prompt, less what an earlier
|
|
1418
|
+
* process already took ({@link TurnProgress}) and what this one heard — is taken now, as its
|
|
1419
|
+
* events would have been: text, reasoning, tool calls and results, each step's usage. A session
|
|
1420
|
+
* that left nothing for the turn fails it ({@link LOST_ANSWER_ERROR}) rather than reading as an
|
|
1421
|
+
* empty answer.
|
|
1422
|
+
*/
|
|
1423
|
+
async recover(outcome) {
|
|
1424
|
+
const messages = await this.messagesSincePrompt().catch((error) => {
|
|
1425
|
+
this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session messages")}`);
|
|
1426
|
+
return void 0;
|
|
1427
|
+
});
|
|
1428
|
+
const before = this.a.progress;
|
|
1429
|
+
const text = [];
|
|
1430
|
+
let reasoning = "";
|
|
1431
|
+
let lastError;
|
|
1432
|
+
for (const message of messages ?? []) {
|
|
1433
|
+
if (message.type !== "assistant") continue;
|
|
1434
|
+
const id = message.id;
|
|
1435
|
+
const parts = message.content ?? [];
|
|
1436
|
+
text.push(...missed(parts, "text", before?.text[id] ?? 0, this.took.text.get(id) ?? 0));
|
|
1437
|
+
reasoning += missed(parts, "reasoning", before?.reasoning[id] ?? 0, this.took.reasoning.get(id) ?? 0).map((piece) => piece.text).join("");
|
|
1438
|
+
for (const part of parts) if (part.type === "tool") await this.missedTool(part);
|
|
1439
|
+
if (message.tokens !== void 0 && !before?.steps.includes(id) && !this.took.steps.has(id)) {
|
|
1440
|
+
this.took.steps.add(id);
|
|
1441
|
+
const model = message.model;
|
|
1442
|
+
if (typeof model?.id === "string" && typeof model.providerID === "string") this.model = {
|
|
1443
|
+
providerID: model.providerID,
|
|
1444
|
+
id: model.id
|
|
1445
|
+
};
|
|
1446
|
+
await this.spend({
|
|
1447
|
+
tokens: message.tokens,
|
|
1448
|
+
cost: message.cost
|
|
1449
|
+
}, "chat");
|
|
1450
|
+
}
|
|
1451
|
+
if (message.error?.message) lastError = message.error.message;
|
|
1452
|
+
}
|
|
1453
|
+
const recovered = text.map((piece) => piece.text).join("\n\n").trimStart();
|
|
1454
|
+
if (recovered.trim()) {
|
|
1455
|
+
await this.step();
|
|
1456
|
+
const joint = this.segment.text && !text.at(-1)?.continues ? "\n\n" : "";
|
|
1457
|
+
this.segment.text = `${recovered}${joint}${this.segment.text}`;
|
|
1458
|
+
this.sawText = true;
|
|
1459
|
+
await this.write({
|
|
1460
|
+
kind: "text",
|
|
1461
|
+
text: recovered
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
if (reasoning) {
|
|
1465
|
+
await this.step();
|
|
1466
|
+
this.segment.reasoning = `${reasoning}${this.segment.reasoning}`;
|
|
1467
|
+
await this.write({
|
|
1468
|
+
kind: "reasoning",
|
|
1469
|
+
text: reasoning
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
if (outcome.status !== "succeeded") return outcome;
|
|
1473
|
+
const idle = (messages ?? []).filter((m) => m.type === "idle").at(-1);
|
|
1474
|
+
if (idle?.outcome === "failed") return {
|
|
1475
|
+
status: "failed",
|
|
1476
|
+
error: lastError ?? this.stepError ?? "the execution failed"
|
|
1477
|
+
};
|
|
1478
|
+
if (idle?.outcome === "interrupted") return {
|
|
1479
|
+
status: "interrupted"
|
|
1480
|
+
};
|
|
1481
|
+
const s = this.segment;
|
|
1482
|
+
const said = s.text || s.reasoning || s.calls.length > 0 || s.ui.length > 0;
|
|
1483
|
+
const progress = this.progress();
|
|
1484
|
+
const tookAny = Object.keys(progress.text).length > 0 || progress.called.length > 0 || progress.steps.length > 0;
|
|
1485
|
+
if (!said && !this.wroteMessage && !tookAny) return {
|
|
1486
|
+
status: "failed",
|
|
1487
|
+
error: LOST_ANSWER_ERROR
|
|
1488
|
+
};
|
|
1489
|
+
return outcome;
|
|
1490
|
+
}
|
|
1491
|
+
/** A tool part of a message the turn did not hear: its call, its result, whichever it lacks. */
|
|
1492
|
+
async missedTool(part) {
|
|
1493
|
+
const id = part.id;
|
|
1494
|
+
const name = part.name ?? "tool";
|
|
1495
|
+
if (id === void 0 || HIDDEN_TOOLS.has(name)) return;
|
|
1496
|
+
const state = part.state ?? {};
|
|
1497
|
+
const before = this.a.progress;
|
|
1498
|
+
if (!before?.called.includes(id) && !this.tools.has(id)) {
|
|
1499
|
+
await this.step();
|
|
1500
|
+
this.setInput(id, state.input, name);
|
|
1501
|
+
await this.makeAvailable(id);
|
|
1502
|
+
}
|
|
1503
|
+
const ok = state.status === "completed";
|
|
1504
|
+
if (!ok && state.status !== "error" || before?.settled.includes(id)) return;
|
|
1505
|
+
const error = errorText(state.error, "tool failed");
|
|
1506
|
+
if (this.tools.has(id)) {
|
|
1507
|
+
await this.settleTool(id, ok, error);
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
this.took.settled.add(id);
|
|
1511
|
+
const result = ok ? {
|
|
1512
|
+
id,
|
|
1513
|
+
name,
|
|
1514
|
+
output: {
|
|
1515
|
+
ok: true
|
|
1516
|
+
}
|
|
1517
|
+
} : {
|
|
1518
|
+
id,
|
|
1519
|
+
name,
|
|
1520
|
+
output: {
|
|
1521
|
+
ok: false
|
|
1522
|
+
},
|
|
1523
|
+
error
|
|
1524
|
+
};
|
|
1525
|
+
const messageId = await this.messageOf(id);
|
|
1526
|
+
if (messageId !== void 0) await this.addResult(messageId, result);
|
|
1527
|
+
else this.segment.results.push(result);
|
|
1528
|
+
await this.write(ok ? {
|
|
1529
|
+
kind: "tool-output",
|
|
1530
|
+
id,
|
|
1531
|
+
output: {
|
|
1532
|
+
ok: true
|
|
1533
|
+
}
|
|
1534
|
+
} : {
|
|
1535
|
+
kind: "tool-output-error",
|
|
1536
|
+
id,
|
|
1537
|
+
error
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
async end(ended) {
|
|
1333
1541
|
if (this.ended) return;
|
|
1334
1542
|
this.ended = true;
|
|
1543
|
+
const outcome = this.a.resumed === true ? await this.recover(ended).catch((error) => {
|
|
1544
|
+
this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not recover the turn")}`);
|
|
1545
|
+
return ended;
|
|
1546
|
+
}) : ended;
|
|
1335
1547
|
await this.unreported().catch((error) => this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session usage")}`));
|
|
1336
1548
|
if (outcome.status !== "failed" || this.segment.text || this.segment.calls.length > 0 || this.segment.ui.length > 0) {
|
|
1337
1549
|
await this.flush(true);
|
|
@@ -1348,6 +1560,26 @@ ${delta}` : delta;
|
|
|
1348
1560
|
});
|
|
1349
1561
|
}
|
|
1350
1562
|
};
|
|
1563
|
+
function missed(parts, type, head, tail) {
|
|
1564
|
+
const texts = parts.flatMap((p) => p.type === type && typeof p.text === "string" ? [
|
|
1565
|
+
p.text
|
|
1566
|
+
] : []);
|
|
1567
|
+
const total = texts.reduce((n, t) => n + t.length, 0);
|
|
1568
|
+
const end = total - tail;
|
|
1569
|
+
const out = [];
|
|
1570
|
+
let at = 0;
|
|
1571
|
+
for (const t of texts) {
|
|
1572
|
+
const from = Math.max(head, at);
|
|
1573
|
+
const to = Math.min(end, at + t.length);
|
|
1574
|
+
if (to > from) out.push({
|
|
1575
|
+
text: t.slice(from - at, to - at),
|
|
1576
|
+
continues: to < at + t.length
|
|
1577
|
+
});
|
|
1578
|
+
at += t.length;
|
|
1579
|
+
}
|
|
1580
|
+
return out;
|
|
1581
|
+
}
|
|
1582
|
+
__name(missed, "missed");
|
|
1351
1583
|
function modelOfLabel(label) {
|
|
1352
1584
|
const slash = label?.indexOf("/") ?? -1;
|
|
1353
1585
|
if (label === void 0 || slash <= 0 || slash === label.length - 1) return void 0;
|
|
@@ -1474,6 +1706,8 @@ var OpenCodeMcpEndpoint2 = class {
|
|
|
1474
1706
|
}
|
|
1475
1707
|
turns;
|
|
1476
1708
|
tokens;
|
|
1709
|
+
/** Genui telemetry of the UI calls OpenCode makes (its code mode calls tools here, not in the loop). */
|
|
1710
|
+
genui = new import_nestjs_agent_core3.GenuiRunRecorder();
|
|
1477
1711
|
constructor(turns, tokens) {
|
|
1478
1712
|
this.turns = turns;
|
|
1479
1713
|
this.tokens = tokens;
|
|
@@ -1605,7 +1839,7 @@ var OpenCodeMcpEndpoint2 = class {
|
|
|
1605
1839
|
if (spec.kind === "action" && !await this.turns.spendApproval(turn.runId, name, turn.input)) {
|
|
1606
1840
|
throw new OpenCodeToolRefusedError(`"${name}" is an action and nobody approved this call; ask for the permission first.`);
|
|
1607
1841
|
}
|
|
1608
|
-
|
|
1842
|
+
const invoke = /* @__PURE__ */ __name(() => registry.invoke(name, args ?? {}, {
|
|
1609
1843
|
actor,
|
|
1610
1844
|
threadId: turn.ctx.threadId,
|
|
1611
1845
|
runId: turn.runId,
|
|
@@ -1622,7 +1856,24 @@ var OpenCodeMcpEndpoint2 = class {
|
|
|
1622
1856
|
} : {}
|
|
1623
1857
|
}, this.turns.rolesPolicyFor(turn.input), allowed !== void 0 ? {
|
|
1624
1858
|
allowedTools: allowed
|
|
1625
|
-
} : {});
|
|
1859
|
+
} : {}), "invoke");
|
|
1860
|
+
return this.genui.track({
|
|
1861
|
+
store: this.turns.store,
|
|
1862
|
+
marker: spec.genui,
|
|
1863
|
+
runId: turn.runId,
|
|
1864
|
+
threadId: turn.ctx.threadId,
|
|
1865
|
+
actorRef: actor.id,
|
|
1866
|
+
...turn.input.agentName !== void 0 ? {
|
|
1867
|
+
agentName: turn.input.agentName
|
|
1868
|
+
} : {},
|
|
1869
|
+
channel: (0, import_genui.turnChannel)(turn.input.pageContext),
|
|
1870
|
+
modelId: turn.input.model ?? "opencode",
|
|
1871
|
+
call: {
|
|
1872
|
+
id: `mcp:${turn.runId}:${requestId}`,
|
|
1873
|
+
name,
|
|
1874
|
+
input: args ?? {}
|
|
1875
|
+
}
|
|
1876
|
+
}, invoke);
|
|
1626
1877
|
}
|
|
1627
1878
|
/** `remember`: one fact, at the actor's own scope only — as the loop serves it. */
|
|
1628
1879
|
async remember(turn, args) {
|
|
@@ -2474,7 +2725,11 @@ ${body}
|
|
|
2474
2725
|
text: prompt.text,
|
|
2475
2726
|
...prompt.files?.length ? {
|
|
2476
2727
|
files: prompt.files
|
|
2477
|
-
} : {}
|
|
2728
|
+
} : {},
|
|
2729
|
+
// Marks the prompt as this run's: a turn resumed after a restart reads what came after it.
|
|
2730
|
+
metadata: {
|
|
2731
|
+
runId
|
|
2732
|
+
}
|
|
2478
2733
|
});
|
|
2479
2734
|
} catch (error) {
|
|
2480
2735
|
live.turn.fail(`OpenCode refused the message: ${error.message}`);
|
|
@@ -2487,9 +2742,11 @@ ${body}
|
|
|
2487
2742
|
*
|
|
2488
2743
|
* `spent` is what the run spent before (the sum of the `usage` of its earlier milestones): a turn
|
|
2489
2744
|
* rebuilt here starts from it, so the answer it persists carries the whole run's usage.
|
|
2745
|
+
* `progress` is what the turn took before (its last `ask` milestone's): a turn rebuilt here reads
|
|
2746
|
+
* back from the session's messages, when the execution ends, what nobody heard after that.
|
|
2490
2747
|
*/
|
|
2491
|
-
async observe(runId, input, handle, spent) {
|
|
2492
|
-
const live = await this.ensureLive(runId, input, handle, true, spent);
|
|
2748
|
+
async observe(runId, input, handle, spent, progress) {
|
|
2749
|
+
const live = await this.ensureLive(runId, input, handle, true, spent, progress);
|
|
2493
2750
|
const { turn, client } = live;
|
|
2494
2751
|
if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
|
|
2495
2752
|
let done = false;
|
|
@@ -2528,12 +2785,12 @@ ${body}
|
|
|
2528
2785
|
* request is gone with it: a new session is opened, told what the person decided, and prompted
|
|
2529
2786
|
* to go on.
|
|
2530
2787
|
*/
|
|
2531
|
-
async reply(runId, input, handle, ask, reply, spent) {
|
|
2788
|
+
async reply(runId, input, handle, ask, reply, spent, progress) {
|
|
2532
2789
|
const server = await this.host.server(input.actor);
|
|
2533
2790
|
if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
|
|
2534
2791
|
return this.continueAfterRestart(runId, input, server, ask, reply, spent);
|
|
2535
2792
|
}
|
|
2536
|
-
const live = await this.ensureLive(runId, input, handle, false, spent);
|
|
2793
|
+
const live = await this.ensureLive(runId, input, handle, false, spent, progress);
|
|
2537
2794
|
await live.turn.decide(ask, reply);
|
|
2538
2795
|
return handle;
|
|
2539
2796
|
}
|
|
@@ -2551,7 +2808,10 @@ ${body}
|
|
|
2551
2808
|
live.turn.setBaseline(await this.sessionUsage(live.client, handle.sessionId));
|
|
2552
2809
|
await live.client.session.prompt({
|
|
2553
2810
|
sessionID: handle.sessionId,
|
|
2554
|
-
text: note
|
|
2811
|
+
text: note,
|
|
2812
|
+
metadata: {
|
|
2813
|
+
runId
|
|
2814
|
+
}
|
|
2555
2815
|
});
|
|
2556
2816
|
return handle;
|
|
2557
2817
|
}
|
|
@@ -2575,9 +2835,10 @@ ${body}
|
|
|
2575
2835
|
sessionID: known.sessionId
|
|
2576
2836
|
});
|
|
2577
2837
|
}
|
|
2578
|
-
async ensureLive(runId, input, handle, catchUp, spent) {
|
|
2838
|
+
async ensureLive(runId, input, handle, catchUp, spent, progress) {
|
|
2579
2839
|
const current = this.live.get(runId);
|
|
2580
2840
|
if (current !== void 0 && current.handle.sessionId === handle.sessionId) return current;
|
|
2841
|
+
const resumed = catchUp && current === void 0;
|
|
2581
2842
|
current?.stop();
|
|
2582
2843
|
const server = await this.host.server(input.actor);
|
|
2583
2844
|
const deps = this.deps.forAgent(input.agentName);
|
|
@@ -2608,6 +2869,10 @@ ${body}
|
|
|
2608
2869
|
...spent !== void 0 ? {
|
|
2609
2870
|
carried: spent
|
|
2610
2871
|
} : {},
|
|
2872
|
+
...progress !== void 0 ? {
|
|
2873
|
+
progress
|
|
2874
|
+
} : {},
|
|
2875
|
+
resumed,
|
|
2611
2876
|
logger: this.logger,
|
|
2612
2877
|
onGranted: /* @__PURE__ */ __name((action, id) => {
|
|
2613
2878
|
const grants = this.grants.get(runId) ?? [];
|
|
@@ -2627,7 +2892,7 @@ ${body}
|
|
|
2627
2892
|
stop
|
|
2628
2893
|
};
|
|
2629
2894
|
this.live.set(runId, live);
|
|
2630
|
-
if (
|
|
2895
|
+
if (resumed) await turn.catchUp().catch(() => void 0);
|
|
2631
2896
|
return live;
|
|
2632
2897
|
}
|
|
2633
2898
|
/** Stop following a run in this process. */
|
|
@@ -3205,6 +3470,7 @@ var OpenCodeRunWorkflow = class {
|
|
|
3205
3470
|
}
|
|
3206
3471
|
async run(ctx, input) {
|
|
3207
3472
|
let spent = emptyUsage();
|
|
3473
|
+
let progress;
|
|
3208
3474
|
try {
|
|
3209
3475
|
const begun = await ctx.localStep("begin", async () => ({
|
|
3210
3476
|
handle: await this.turns.begin(ctx.runId, input),
|
|
@@ -3217,7 +3483,8 @@ var OpenCodeRunWorkflow = class {
|
|
|
3217
3483
|
});
|
|
3218
3484
|
for (let n = 0; ; n += 1) {
|
|
3219
3485
|
const before = spent;
|
|
3220
|
-
const
|
|
3486
|
+
const taken = progress;
|
|
3487
|
+
const milestone = await ctx.localStep(`observe:${n}`, () => this.turns.observe(ctx.runId, input, handle, before, taken));
|
|
3221
3488
|
spent = addUsage(spent, milestone.usage);
|
|
3222
3489
|
const total = spent;
|
|
3223
3490
|
if (milestone.kind === "finished") {
|
|
@@ -3243,7 +3510,9 @@ var OpenCodeRunWorkflow = class {
|
|
|
3243
3510
|
};
|
|
3244
3511
|
}
|
|
3245
3512
|
const ask = milestone.ask;
|
|
3246
|
-
|
|
3513
|
+
progress = milestone.progress ?? progress;
|
|
3514
|
+
const tookSoFar = progress;
|
|
3515
|
+
handle = await ctx.localStep(`reply:${n}`, () => this.turns.reply(ctx.runId, input, handle, ask, reply, total, tookSoFar));
|
|
3247
3516
|
}
|
|
3248
3517
|
} catch (error) {
|
|
3249
3518
|
if ((0, import_nestjs_durable_core.isWorkflowControlFlowSignal)(error)) throw error;
|