@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.
@@ -1,5 +1,5 @@
1
1
  import { AgentEngine } from '@dudousxd/nestjs-agent';
2
- import { O as OpenCodeTurns, a as OpenCodeEngineSettings, b as OpenCodeHost, c as OpenCodeEngineOptions } from '../engine-Cp5uysaN.cjs';
2
+ import { O as OpenCodeTurns, a as OpenCodeEngineSettings, b as OpenCodeHost, c as OpenCodeEngineOptions } from '../engine-BX6MLzxs.cjs';
3
3
  import { AgentRunner, AgentStore, AgentRunInput, AgentRunStartOptions, HumanReply } from '@dudousxd/nestjs-agent-core';
4
4
  import { WorkflowService } from '@dudousxd/nestjs-durable';
5
5
  import { RunGateway, WorkflowCtx } from '@dudousxd/nestjs-durable-core';
@@ -53,7 +53,8 @@ declare function decisionToken(runId: string, toolCallId: string): string;
53
53
  * the steps from their checkpoints and replying to OpenCode from there (`OpenCodeTurns` rebuilds the
54
54
  * live turn, and re-prompts a fresh session when OpenCode itself restarted meanwhile). A process
55
55
  * that dies while OpenCode works re-runs that `observe` step, which catches up on what OpenCode
56
- * asked while nobody listened.
56
+ * asked while nobody listened, and — when the execution ends — on what it said: the text, tool
57
+ * calls and usage it streamed meanwhile, read back from the session's messages.
57
58
  */
58
59
  declare class OpenCodeRunWorkflow {
59
60
  private readonly turns;
@@ -1,5 +1,5 @@
1
1
  import { AgentEngine } from '@dudousxd/nestjs-agent';
2
- import { O as OpenCodeTurns, a as OpenCodeEngineSettings, b as OpenCodeHost, c as OpenCodeEngineOptions } from '../engine-Cp5uysaN.js';
2
+ import { O as OpenCodeTurns, a as OpenCodeEngineSettings, b as OpenCodeHost, c as OpenCodeEngineOptions } from '../engine-BX6MLzxs.js';
3
3
  import { AgentRunner, AgentStore, AgentRunInput, AgentRunStartOptions, HumanReply } from '@dudousxd/nestjs-agent-core';
4
4
  import { WorkflowService } from '@dudousxd/nestjs-durable';
5
5
  import { RunGateway, WorkflowCtx } from '@dudousxd/nestjs-durable-core';
@@ -53,7 +53,8 @@ declare function decisionToken(runId: string, toolCallId: string): string;
53
53
  * the steps from their checkpoints and replying to OpenCode from there (`OpenCodeTurns` rebuilds the
54
54
  * live turn, and re-prompts a fresh session when OpenCode itself restarted meanwhile). A process
55
55
  * that dies while OpenCode works re-runs that `observe` step, which catches up on what OpenCode
56
- * asked while nobody listened.
56
+ * asked while nobody listened, and — when the execution ends — on what it said: the text, tool
57
+ * calls and usage it streamed meanwhile, read back from the session's messages.
57
58
  */
58
59
  declare class OpenCodeRunWorkflow {
59
60
  private readonly turns;
@@ -125,7 +125,8 @@ OpenCodeMcpController = _ts_decorate([
125
125
 
126
126
  // src/mcp.ts
127
127
  import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
128
- import { REMEMBER_TOOL_DESCRIPTION, REMEMBER_TOOL_NAME, ToolNotFoundError, rememberInputSchema, schemaExtensionOf } from "@dudousxd/nestjs-agent-core";
128
+ import { GenuiRunRecorder, REMEMBER_TOOL_DESCRIPTION, REMEMBER_TOOL_NAME, ToolNotFoundError, rememberInputSchema, schemaExtensionOf } from "@dudousxd/nestjs-agent-core";
129
+ import { turnChannel } from "@dudousxd/nestjs-agent-core/genui";
129
130
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
130
131
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
131
132
  import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
@@ -256,6 +257,9 @@ function subUsage(a, b) {
256
257
  };
257
258
  }
258
259
  __name(subUsage, "subUsage");
260
+ var LOST_ANSWER_ERROR = "The answer was lost while the agent restarted; ask again.";
261
+ var RECOVER_PAGES = 10;
262
+ var RECOVER_PAGE_SIZE = 100;
259
263
  var ASK_TOOL_NAME = "ask";
260
264
  var FAILED_STATUSES = /* @__PURE__ */ new Set([
261
265
  "error",
@@ -344,6 +348,14 @@ var OpenCodeTurn = class {
344
348
  wroteMessage = false;
345
349
  ended = false;
346
350
  stepError;
351
+ /** What this turn object took from the session's assistant messages (see {@link TurnProgress}). */
352
+ took = {
353
+ text: /* @__PURE__ */ new Map(),
354
+ reasoning: /* @__PURE__ */ new Map(),
355
+ called: /* @__PURE__ */ new Set(),
356
+ settled: /* @__PURE__ */ new Set(),
357
+ steps: /* @__PURE__ */ new Set()
358
+ };
347
359
  constructor(a) {
348
360
  this.a = a;
349
361
  this.model = modelOfLabel(a.input.model);
@@ -361,6 +373,30 @@ var OpenCodeTurn = class {
361
373
  tokens: info.tokens
362
374
  };
363
375
  }
376
+ /** What the turn took so far: carried from before this turn object, plus what it took. */
377
+ progress() {
378
+ const before = this.a.progress;
379
+ const merged = /* @__PURE__ */ __name((carried, own) => {
380
+ const out = {
381
+ ...carried ?? {}
382
+ };
383
+ for (const [id, n] of own) out[id] = (out[id] ?? 0) + n;
384
+ return out;
385
+ }, "merged");
386
+ const union = /* @__PURE__ */ __name((carried, own) => [
387
+ .../* @__PURE__ */ new Set([
388
+ ...carried ?? [],
389
+ ...own
390
+ ])
391
+ ], "union");
392
+ return {
393
+ text: merged(before?.text, this.took.text),
394
+ reasoning: merged(before?.reasoning, this.took.reasoning),
395
+ called: union(before?.called, this.took.called),
396
+ settled: union(before?.settled, this.took.settled),
397
+ steps: union(before?.steps, this.took.steps)
398
+ };
399
+ }
364
400
  /** What the run spent so far: carried from before this turn object, plus what it saw. */
365
401
  totalUsage() {
366
402
  return addUsage(this.a.carried, this.spent);
@@ -451,7 +487,11 @@ var OpenCodeTurn = class {
451
487
  this.reported = {
452
488
  ...this.spent
453
489
  };
454
- const milestone = {
490
+ const milestone = reached.kind === "ask" ? {
491
+ ...reached,
492
+ usage,
493
+ progress: this.progress()
494
+ } : {
455
495
  ...reached,
456
496
  usage
457
497
  };
@@ -506,6 +546,7 @@ var OpenCodeTurn = class {
506
546
  innerSettled: /* @__PURE__ */ new Set()
507
547
  };
508
548
  this.tools.set(id, state);
549
+ this.took.called.add(id);
509
550
  this.segment.calls.push({
510
551
  id,
511
552
  name,
@@ -633,6 +674,7 @@ var OpenCodeTurn = class {
633
674
  case "session.text.delta": {
634
675
  const delta = typeof d.delta === "string" ? d.delta : "";
635
676
  if (!delta) return;
677
+ this.count(this.took.text, d.assistantMessageID, delta.length);
636
678
  this.endReasoning();
637
679
  if (!this.sawText && !delta.trim()) return;
638
680
  await this.step();
@@ -654,6 +696,7 @@ ${delta}` : delta;
654
696
  case "session.reasoning.delta": {
655
697
  const delta = typeof d.delta === "string" ? d.delta : "";
656
698
  if (!delta) return;
699
+ this.count(this.took.reasoning, d.assistantMessageID, delta.length);
657
700
  this.reasoningSince ??= Date.now();
658
701
  await this.step();
659
702
  this.segment.reasoning += delta;
@@ -708,6 +751,7 @@ ${delta}` : delta;
708
751
  }
709
752
  case "session.step.ended":
710
753
  this.endReasoning();
754
+ if (typeof d.assistantMessageID === "string") this.took.steps.add(d.assistantMessageID);
711
755
  await this.stepEnded(d);
712
756
  return;
713
757
  case "session.step.failed":
@@ -748,10 +792,16 @@ ${delta}` : delta;
748
792
  return;
749
793
  }
750
794
  }
795
+ /** Characters taken from an assistant message's text or reasoning. */
796
+ count(into, messageId, n) {
797
+ if (typeof messageId !== "string") return;
798
+ into.set(messageId, (into.get(messageId) ?? 0) + n);
799
+ }
751
800
  async settleTool(id, ok, error = "failed") {
752
801
  const state = this.tool(id);
753
802
  if (state.settled) return;
754
803
  state.settled = true;
804
+ this.took.settled.add(id);
755
805
  if (ok) {
756
806
  this.segment.results.push({
757
807
  id,
@@ -1301,9 +1351,171 @@ ${delta}` : delta;
1301
1351
  cost: cost < 1e-9 ? 0 : cost
1302
1352
  }, this.compacted ? "history_summary" : "title");
1303
1353
  }
1304
- async end(outcome) {
1354
+ /**
1355
+ * The session's messages since this turn's prompt, oldest first: everything after its newest user
1356
+ * message. `undefined` when they cannot be read (no `message.list`, no prompt within reach); empty
1357
+ * when the newest prompt is another run's (this turn's never reached the session).
1358
+ */
1359
+ async messagesSincePrompt() {
1360
+ const messages = this.a.client.message;
1361
+ if (messages === void 0) return void 0;
1362
+ const since = [];
1363
+ let cursor;
1364
+ for (let page = 0; page < RECOVER_PAGES; page += 1) {
1365
+ const listed = await messages.list({
1366
+ sessionID: this.a.sessionId,
1367
+ order: "desc",
1368
+ limit: RECOVER_PAGE_SIZE,
1369
+ ...cursor !== void 0 ? {
1370
+ cursor
1371
+ } : {}
1372
+ });
1373
+ for (const message of listed.data) {
1374
+ if (message.type === "user") {
1375
+ const runId = message.metadata?.runId;
1376
+ if (typeof runId === "string" && runId !== this.a.runId) return [];
1377
+ return since.reverse();
1378
+ }
1379
+ since.push(message);
1380
+ }
1381
+ const next = listed.cursor?.next;
1382
+ if (typeof next !== "string" || next.length === 0 || listed.data.length === 0) break;
1383
+ cursor = next;
1384
+ }
1385
+ return void 0;
1386
+ }
1387
+ /**
1388
+ * A turn rebuilt after a restart heard only what the session said once it re-attached: what it
1389
+ * said in between — read back from the session's messages since the prompt, less what an earlier
1390
+ * process already took ({@link TurnProgress}) and what this one heard — is taken now, as its
1391
+ * events would have been: text, reasoning, tool calls and results, each step's usage. A session
1392
+ * that left nothing for the turn fails it ({@link LOST_ANSWER_ERROR}) rather than reading as an
1393
+ * empty answer.
1394
+ */
1395
+ async recover(outcome) {
1396
+ const messages = await this.messagesSincePrompt().catch((error) => {
1397
+ this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session messages")}`);
1398
+ return void 0;
1399
+ });
1400
+ const before = this.a.progress;
1401
+ const text = [];
1402
+ let reasoning = "";
1403
+ let lastError;
1404
+ for (const message of messages ?? []) {
1405
+ if (message.type !== "assistant") continue;
1406
+ const id = message.id;
1407
+ const parts = message.content ?? [];
1408
+ text.push(...missed(parts, "text", before?.text[id] ?? 0, this.took.text.get(id) ?? 0));
1409
+ reasoning += missed(parts, "reasoning", before?.reasoning[id] ?? 0, this.took.reasoning.get(id) ?? 0).map((piece) => piece.text).join("");
1410
+ for (const part of parts) if (part.type === "tool") await this.missedTool(part);
1411
+ if (message.tokens !== void 0 && !before?.steps.includes(id) && !this.took.steps.has(id)) {
1412
+ this.took.steps.add(id);
1413
+ const model = message.model;
1414
+ if (typeof model?.id === "string" && typeof model.providerID === "string") this.model = {
1415
+ providerID: model.providerID,
1416
+ id: model.id
1417
+ };
1418
+ await this.spend({
1419
+ tokens: message.tokens,
1420
+ cost: message.cost
1421
+ }, "chat");
1422
+ }
1423
+ if (message.error?.message) lastError = message.error.message;
1424
+ }
1425
+ const recovered = text.map((piece) => piece.text).join("\n\n").trimStart();
1426
+ if (recovered.trim()) {
1427
+ await this.step();
1428
+ const joint = this.segment.text && !text.at(-1)?.continues ? "\n\n" : "";
1429
+ this.segment.text = `${recovered}${joint}${this.segment.text}`;
1430
+ this.sawText = true;
1431
+ await this.write({
1432
+ kind: "text",
1433
+ text: recovered
1434
+ });
1435
+ }
1436
+ if (reasoning) {
1437
+ await this.step();
1438
+ this.segment.reasoning = `${reasoning}${this.segment.reasoning}`;
1439
+ await this.write({
1440
+ kind: "reasoning",
1441
+ text: reasoning
1442
+ });
1443
+ }
1444
+ if (outcome.status !== "succeeded") return outcome;
1445
+ const idle = (messages ?? []).filter((m) => m.type === "idle").at(-1);
1446
+ if (idle?.outcome === "failed") return {
1447
+ status: "failed",
1448
+ error: lastError ?? this.stepError ?? "the execution failed"
1449
+ };
1450
+ if (idle?.outcome === "interrupted") return {
1451
+ status: "interrupted"
1452
+ };
1453
+ const s = this.segment;
1454
+ const said = s.text || s.reasoning || s.calls.length > 0 || s.ui.length > 0;
1455
+ const progress = this.progress();
1456
+ const tookAny = Object.keys(progress.text).length > 0 || progress.called.length > 0 || progress.steps.length > 0;
1457
+ if (!said && !this.wroteMessage && !tookAny) return {
1458
+ status: "failed",
1459
+ error: LOST_ANSWER_ERROR
1460
+ };
1461
+ return outcome;
1462
+ }
1463
+ /** A tool part of a message the turn did not hear: its call, its result, whichever it lacks. */
1464
+ async missedTool(part) {
1465
+ const id = part.id;
1466
+ const name = part.name ?? "tool";
1467
+ if (id === void 0 || HIDDEN_TOOLS.has(name)) return;
1468
+ const state = part.state ?? {};
1469
+ const before = this.a.progress;
1470
+ if (!before?.called.includes(id) && !this.tools.has(id)) {
1471
+ await this.step();
1472
+ this.setInput(id, state.input, name);
1473
+ await this.makeAvailable(id);
1474
+ }
1475
+ const ok = state.status === "completed";
1476
+ if (!ok && state.status !== "error" || before?.settled.includes(id)) return;
1477
+ const error = errorText(state.error, "tool failed");
1478
+ if (this.tools.has(id)) {
1479
+ await this.settleTool(id, ok, error);
1480
+ return;
1481
+ }
1482
+ this.took.settled.add(id);
1483
+ const result = ok ? {
1484
+ id,
1485
+ name,
1486
+ output: {
1487
+ ok: true
1488
+ }
1489
+ } : {
1490
+ id,
1491
+ name,
1492
+ output: {
1493
+ ok: false
1494
+ },
1495
+ error
1496
+ };
1497
+ const messageId = await this.messageOf(id);
1498
+ if (messageId !== void 0) await this.addResult(messageId, result);
1499
+ else this.segment.results.push(result);
1500
+ await this.write(ok ? {
1501
+ kind: "tool-output",
1502
+ id,
1503
+ output: {
1504
+ ok: true
1505
+ }
1506
+ } : {
1507
+ kind: "tool-output-error",
1508
+ id,
1509
+ error
1510
+ });
1511
+ }
1512
+ async end(ended) {
1305
1513
  if (this.ended) return;
1306
1514
  this.ended = true;
1515
+ const outcome = this.a.resumed === true ? await this.recover(ended).catch((error) => {
1516
+ this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not recover the turn")}`);
1517
+ return ended;
1518
+ }) : ended;
1307
1519
  await this.unreported().catch((error) => this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session usage")}`));
1308
1520
  if (outcome.status !== "failed" || this.segment.text || this.segment.calls.length > 0 || this.segment.ui.length > 0) {
1309
1521
  await this.flush(true);
@@ -1320,6 +1532,26 @@ ${delta}` : delta;
1320
1532
  });
1321
1533
  }
1322
1534
  };
1535
+ function missed(parts, type, head, tail) {
1536
+ const texts = parts.flatMap((p) => p.type === type && typeof p.text === "string" ? [
1537
+ p.text
1538
+ ] : []);
1539
+ const total = texts.reduce((n, t) => n + t.length, 0);
1540
+ const end = total - tail;
1541
+ const out = [];
1542
+ let at = 0;
1543
+ for (const t of texts) {
1544
+ const from = Math.max(head, at);
1545
+ const to = Math.min(end, at + t.length);
1546
+ if (to > from) out.push({
1547
+ text: t.slice(from - at, to - at),
1548
+ continues: to < at + t.length
1549
+ });
1550
+ at += t.length;
1551
+ }
1552
+ return out;
1553
+ }
1554
+ __name(missed, "missed");
1323
1555
  function modelOfLabel(label) {
1324
1556
  const slash = label?.indexOf("/") ?? -1;
1325
1557
  if (label === void 0 || slash <= 0 || slash === label.length - 1) return void 0;
@@ -1446,6 +1678,8 @@ var OpenCodeMcpEndpoint2 = class {
1446
1678
  }
1447
1679
  turns;
1448
1680
  tokens;
1681
+ /** Genui telemetry of the UI calls OpenCode makes (its code mode calls tools here, not in the loop). */
1682
+ genui = new GenuiRunRecorder();
1449
1683
  constructor(turns, tokens) {
1450
1684
  this.turns = turns;
1451
1685
  this.tokens = tokens;
@@ -1577,7 +1811,7 @@ var OpenCodeMcpEndpoint2 = class {
1577
1811
  if (spec.kind === "action" && !await this.turns.spendApproval(turn.runId, name, turn.input)) {
1578
1812
  throw new OpenCodeToolRefusedError(`"${name}" is an action and nobody approved this call; ask for the permission first.`);
1579
1813
  }
1580
- return registry.invoke(name, args ?? {}, {
1814
+ const invoke = /* @__PURE__ */ __name(() => registry.invoke(name, args ?? {}, {
1581
1815
  actor,
1582
1816
  threadId: turn.ctx.threadId,
1583
1817
  runId: turn.runId,
@@ -1594,7 +1828,24 @@ var OpenCodeMcpEndpoint2 = class {
1594
1828
  } : {}
1595
1829
  }, this.turns.rolesPolicyFor(turn.input), allowed !== void 0 ? {
1596
1830
  allowedTools: allowed
1597
- } : {});
1831
+ } : {}), "invoke");
1832
+ return this.genui.track({
1833
+ store: this.turns.store,
1834
+ marker: spec.genui,
1835
+ runId: turn.runId,
1836
+ threadId: turn.ctx.threadId,
1837
+ actorRef: actor.id,
1838
+ ...turn.input.agentName !== void 0 ? {
1839
+ agentName: turn.input.agentName
1840
+ } : {},
1841
+ channel: turnChannel(turn.input.pageContext),
1842
+ modelId: turn.input.model ?? "opencode",
1843
+ call: {
1844
+ id: `mcp:${turn.runId}:${requestId}`,
1845
+ name,
1846
+ input: args ?? {}
1847
+ }
1848
+ }, invoke);
1598
1849
  }
1599
1850
  /** `remember`: one fact, at the actor's own scope only — as the loop serves it. */
1600
1851
  async remember(turn, args) {
@@ -2446,7 +2697,11 @@ ${body}
2446
2697
  text: prompt.text,
2447
2698
  ...prompt.files?.length ? {
2448
2699
  files: prompt.files
2449
- } : {}
2700
+ } : {},
2701
+ // Marks the prompt as this run's: a turn resumed after a restart reads what came after it.
2702
+ metadata: {
2703
+ runId
2704
+ }
2450
2705
  });
2451
2706
  } catch (error) {
2452
2707
  live.turn.fail(`OpenCode refused the message: ${error.message}`);
@@ -2459,9 +2714,11 @@ ${body}
2459
2714
  *
2460
2715
  * `spent` is what the run spent before (the sum of the `usage` of its earlier milestones): a turn
2461
2716
  * rebuilt here starts from it, so the answer it persists carries the whole run's usage.
2717
+ * `progress` is what the turn took before (its last `ask` milestone's): a turn rebuilt here reads
2718
+ * back from the session's messages, when the execution ends, what nobody heard after that.
2462
2719
  */
2463
- async observe(runId, input, handle, spent) {
2464
- const live = await this.ensureLive(runId, input, handle, true, spent);
2720
+ async observe(runId, input, handle, spent, progress) {
2721
+ const live = await this.ensureLive(runId, input, handle, true, spent, progress);
2465
2722
  const { turn, client } = live;
2466
2723
  if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
2467
2724
  let done = false;
@@ -2500,12 +2757,12 @@ ${body}
2500
2757
  * request is gone with it: a new session is opened, told what the person decided, and prompted
2501
2758
  * to go on.
2502
2759
  */
2503
- async reply(runId, input, handle, ask, reply, spent) {
2760
+ async reply(runId, input, handle, ask, reply, spent, progress) {
2504
2761
  const server = await this.host.server(input.actor);
2505
2762
  if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
2506
2763
  return this.continueAfterRestart(runId, input, server, ask, reply, spent);
2507
2764
  }
2508
- const live = await this.ensureLive(runId, input, handle, false, spent);
2765
+ const live = await this.ensureLive(runId, input, handle, false, spent, progress);
2509
2766
  await live.turn.decide(ask, reply);
2510
2767
  return handle;
2511
2768
  }
@@ -2523,7 +2780,10 @@ ${body}
2523
2780
  live.turn.setBaseline(await this.sessionUsage(live.client, handle.sessionId));
2524
2781
  await live.client.session.prompt({
2525
2782
  sessionID: handle.sessionId,
2526
- text: note
2783
+ text: note,
2784
+ metadata: {
2785
+ runId
2786
+ }
2527
2787
  });
2528
2788
  return handle;
2529
2789
  }
@@ -2547,9 +2807,10 @@ ${body}
2547
2807
  sessionID: known.sessionId
2548
2808
  });
2549
2809
  }
2550
- async ensureLive(runId, input, handle, catchUp, spent) {
2810
+ async ensureLive(runId, input, handle, catchUp, spent, progress) {
2551
2811
  const current = this.live.get(runId);
2552
2812
  if (current !== void 0 && current.handle.sessionId === handle.sessionId) return current;
2813
+ const resumed = catchUp && current === void 0;
2553
2814
  current?.stop();
2554
2815
  const server = await this.host.server(input.actor);
2555
2816
  const deps = this.deps.forAgent(input.agentName);
@@ -2580,6 +2841,10 @@ ${body}
2580
2841
  ...spent !== void 0 ? {
2581
2842
  carried: spent
2582
2843
  } : {},
2844
+ ...progress !== void 0 ? {
2845
+ progress
2846
+ } : {},
2847
+ resumed,
2583
2848
  logger: this.logger,
2584
2849
  onGranted: /* @__PURE__ */ __name((action, id) => {
2585
2850
  const grants = this.grants.get(runId) ?? [];
@@ -2599,7 +2864,7 @@ ${body}
2599
2864
  stop
2600
2865
  };
2601
2866
  this.live.set(runId, live);
2602
- if (catchUp && current === void 0) await turn.catchUp().catch(() => void 0);
2867
+ if (resumed) await turn.catchUp().catch(() => void 0);
2603
2868
  return live;
2604
2869
  }
2605
2870
  /** Stop following a run in this process. */
@@ -3177,6 +3442,7 @@ var OpenCodeRunWorkflow = class {
3177
3442
  }
3178
3443
  async run(ctx, input) {
3179
3444
  let spent = emptyUsage();
3445
+ let progress;
3180
3446
  try {
3181
3447
  const begun = await ctx.localStep("begin", async () => ({
3182
3448
  handle: await this.turns.begin(ctx.runId, input),
@@ -3189,7 +3455,8 @@ var OpenCodeRunWorkflow = class {
3189
3455
  });
3190
3456
  for (let n = 0; ; n += 1) {
3191
3457
  const before = spent;
3192
- const milestone = await ctx.localStep(`observe:${n}`, () => this.turns.observe(ctx.runId, input, handle, before));
3458
+ const taken = progress;
3459
+ const milestone = await ctx.localStep(`observe:${n}`, () => this.turns.observe(ctx.runId, input, handle, before, taken));
3193
3460
  spent = addUsage(spent, milestone.usage);
3194
3461
  const total = spent;
3195
3462
  if (milestone.kind === "finished") {
@@ -3215,7 +3482,9 @@ var OpenCodeRunWorkflow = class {
3215
3482
  };
3216
3483
  }
3217
3484
  const ask = milestone.ask;
3218
- handle = await ctx.localStep(`reply:${n}`, () => this.turns.reply(ctx.runId, input, handle, ask, reply, total));
3485
+ progress = milestone.progress ?? progress;
3486
+ const tookSoFar = progress;
3487
+ handle = await ctx.localStep(`reply:${n}`, () => this.turns.reply(ctx.runId, input, handle, ask, reply, total, tookSoFar));
3219
3488
  }
3220
3489
  } catch (error) {
3221
3490
  if (isWorkflowControlFlowSignal(error)) throw error;