@dudousxd/nestjs-agent-opencode 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-BNNxQAFV.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-BNNxQAFV.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;
@@ -257,6 +257,9 @@ function subUsage(a, b) {
257
257
  };
258
258
  }
259
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;
260
263
  var ASK_TOOL_NAME = "ask";
261
264
  var FAILED_STATUSES = /* @__PURE__ */ new Set([
262
265
  "error",
@@ -345,6 +348,14 @@ var OpenCodeTurn = class {
345
348
  wroteMessage = false;
346
349
  ended = false;
347
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
+ };
348
359
  constructor(a) {
349
360
  this.a = a;
350
361
  this.model = modelOfLabel(a.input.model);
@@ -362,6 +373,30 @@ var OpenCodeTurn = class {
362
373
  tokens: info.tokens
363
374
  };
364
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
+ }
365
400
  /** What the run spent so far: carried from before this turn object, plus what it saw. */
366
401
  totalUsage() {
367
402
  return addUsage(this.a.carried, this.spent);
@@ -452,7 +487,11 @@ var OpenCodeTurn = class {
452
487
  this.reported = {
453
488
  ...this.spent
454
489
  };
455
- const milestone = {
490
+ const milestone = reached.kind === "ask" ? {
491
+ ...reached,
492
+ usage,
493
+ progress: this.progress()
494
+ } : {
456
495
  ...reached,
457
496
  usage
458
497
  };
@@ -507,6 +546,7 @@ var OpenCodeTurn = class {
507
546
  innerSettled: /* @__PURE__ */ new Set()
508
547
  };
509
548
  this.tools.set(id, state);
549
+ this.took.called.add(id);
510
550
  this.segment.calls.push({
511
551
  id,
512
552
  name,
@@ -634,6 +674,7 @@ var OpenCodeTurn = class {
634
674
  case "session.text.delta": {
635
675
  const delta = typeof d.delta === "string" ? d.delta : "";
636
676
  if (!delta) return;
677
+ this.count(this.took.text, d.assistantMessageID, delta.length);
637
678
  this.endReasoning();
638
679
  if (!this.sawText && !delta.trim()) return;
639
680
  await this.step();
@@ -655,6 +696,7 @@ ${delta}` : delta;
655
696
  case "session.reasoning.delta": {
656
697
  const delta = typeof d.delta === "string" ? d.delta : "";
657
698
  if (!delta) return;
699
+ this.count(this.took.reasoning, d.assistantMessageID, delta.length);
658
700
  this.reasoningSince ??= Date.now();
659
701
  await this.step();
660
702
  this.segment.reasoning += delta;
@@ -709,6 +751,7 @@ ${delta}` : delta;
709
751
  }
710
752
  case "session.step.ended":
711
753
  this.endReasoning();
754
+ if (typeof d.assistantMessageID === "string") this.took.steps.add(d.assistantMessageID);
712
755
  await this.stepEnded(d);
713
756
  return;
714
757
  case "session.step.failed":
@@ -749,10 +792,16 @@ ${delta}` : delta;
749
792
  return;
750
793
  }
751
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
+ }
752
800
  async settleTool(id, ok, error = "failed") {
753
801
  const state = this.tool(id);
754
802
  if (state.settled) return;
755
803
  state.settled = true;
804
+ this.took.settled.add(id);
756
805
  if (ok) {
757
806
  this.segment.results.push({
758
807
  id,
@@ -1302,9 +1351,171 @@ ${delta}` : delta;
1302
1351
  cost: cost < 1e-9 ? 0 : cost
1303
1352
  }, this.compacted ? "history_summary" : "title");
1304
1353
  }
1305
- 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) {
1306
1513
  if (this.ended) return;
1307
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;
1308
1519
  await this.unreported().catch((error) => this.a.logger.warn(`run ${this.a.runId}: ${errorText(error, "could not read the session usage")}`));
1309
1520
  if (outcome.status !== "failed" || this.segment.text || this.segment.calls.length > 0 || this.segment.ui.length > 0) {
1310
1521
  await this.flush(true);
@@ -1321,6 +1532,26 @@ ${delta}` : delta;
1321
1532
  });
1322
1533
  }
1323
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");
1324
1555
  function modelOfLabel(label) {
1325
1556
  const slash = label?.indexOf("/") ?? -1;
1326
1557
  if (label === void 0 || slash <= 0 || slash === label.length - 1) return void 0;
@@ -1633,7 +1864,7 @@ import { Inject as Inject3, Injectable as Injectable2 } from "@nestjs/common";
1633
1864
  // src/turns.ts
1634
1865
  import { randomUUID } from "crypto";
1635
1866
  import { AGENT_CHAT_QUEUE } from "@dudousxd/nestjs-agent";
1636
- import { AGENT_DEPS_FACTORY, AGENT_MEMORY, AGENT_OPTIONS, AGENT_PRICING_STORE, AGENT_SINK, AGENT_SKILLS, AGENT_STORE, AGENT_TOOL_REGISTRY, DEFAULT_MAX_FACT_CHARS, DefaultApprovalPolicy, ModelPriceResolver, RUN_ENDED_BEFORE_TOOL_CALL, actorScope, buildMemoryBlock, defaultScopeResolver, encodeStreamEvent as encodeStreamEvent2, memoryWriteVerdict, parseBedrockModelId, publishAgentRunFailed, releaseThreadRun, resolveMemoryDigest, resolveSkillCatalog, resolveUsageCost, streamFailure } from "@dudousxd/nestjs-agent-core";
1867
+ import { AGENT_DEPS_FACTORY, AGENT_MEMORY, AGENT_OPTIONS, AGENT_PRICING_STORE, AGENT_SINK, AGENT_SKILLS, AGENT_STORE, AGENT_TOOL_REGISTRY, DEFAULT_MAX_FACT_CHARS, DefaultApprovalPolicy, ModelPriceResolver, RUN_ENDED_BEFORE_TOOL_CALL, actorScope, agentRunContext, buildMemoryBlock, defaultScopeResolver, encodeStreamEvent as encodeStreamEvent2, memoryWriteVerdict, parseBedrockModelId, publishAgentRunCancelled, publishAgentRunFailed, releaseThreadRun, resolveMemoryDigest, resolveSkillCatalog, resolveUsageCost, streamFailure } from "@dudousxd/nestjs-agent-core";
1637
1868
  import { Inject as Inject2, Injectable, Logger as Logger2, Optional } from "@nestjs/common";
1638
1869
 
1639
1870
  // src/event-hub.ts
@@ -2466,7 +2697,11 @@ ${body}
2466
2697
  text: prompt.text,
2467
2698
  ...prompt.files?.length ? {
2468
2699
  files: prompt.files
2469
- } : {}
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
+ }
2470
2705
  });
2471
2706
  } catch (error) {
2472
2707
  live.turn.fail(`OpenCode refused the message: ${error.message}`);
@@ -2479,9 +2714,11 @@ ${body}
2479
2714
  *
2480
2715
  * `spent` is what the run spent before (the sum of the `usage` of its earlier milestones): a turn
2481
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.
2482
2719
  */
2483
- async observe(runId, input, handle, spent) {
2484
- 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);
2485
2722
  const { turn, client } = live;
2486
2723
  if (turn.hasMilestone()) return this.reached(runId, input, handle, await turn.next());
2487
2724
  let done = false;
@@ -2520,12 +2757,12 @@ ${body}
2520
2757
  * request is gone with it: a new session is opened, told what the person decided, and prompted
2521
2758
  * to go on.
2522
2759
  */
2523
- async reply(runId, input, handle, ask, reply, spent) {
2760
+ async reply(runId, input, handle, ask, reply, spent, progress) {
2524
2761
  const server = await this.host.server(input.actor);
2525
2762
  if (server.key !== handle.serverKey || server.bootId !== handle.bootId) {
2526
2763
  return this.continueAfterRestart(runId, input, server, ask, reply, spent);
2527
2764
  }
2528
- const live = await this.ensureLive(runId, input, handle, false, spent);
2765
+ const live = await this.ensureLive(runId, input, handle, false, spent, progress);
2529
2766
  await live.turn.decide(ask, reply);
2530
2767
  return handle;
2531
2768
  }
@@ -2543,7 +2780,10 @@ ${body}
2543
2780
  live.turn.setBaseline(await this.sessionUsage(live.client, handle.sessionId));
2544
2781
  await live.client.session.prompt({
2545
2782
  sessionID: handle.sessionId,
2546
- text: note
2783
+ text: note,
2784
+ metadata: {
2785
+ runId
2786
+ }
2547
2787
  });
2548
2788
  return handle;
2549
2789
  }
@@ -2567,9 +2807,10 @@ ${body}
2567
2807
  sessionID: known.sessionId
2568
2808
  });
2569
2809
  }
2570
- async ensureLive(runId, input, handle, catchUp, spent) {
2810
+ async ensureLive(runId, input, handle, catchUp, spent, progress) {
2571
2811
  const current = this.live.get(runId);
2572
2812
  if (current !== void 0 && current.handle.sessionId === handle.sessionId) return current;
2813
+ const resumed = catchUp && current === void 0;
2573
2814
  current?.stop();
2574
2815
  const server = await this.host.server(input.actor);
2575
2816
  const deps = this.deps.forAgent(input.agentName);
@@ -2600,6 +2841,10 @@ ${body}
2600
2841
  ...spent !== void 0 ? {
2601
2842
  carried: spent
2602
2843
  } : {},
2844
+ ...progress !== void 0 ? {
2845
+ progress
2846
+ } : {},
2847
+ resumed,
2603
2848
  logger: this.logger,
2604
2849
  onGranted: /* @__PURE__ */ __name((action, id) => {
2605
2850
  const grants = this.grants.get(runId) ?? [];
@@ -2619,7 +2864,7 @@ ${body}
2619
2864
  stop
2620
2865
  };
2621
2866
  this.live.set(runId, live);
2622
- if (catchUp && current === void 0) await turn.catchUp().catch(() => void 0);
2867
+ if (resumed) await turn.catchUp().catch(() => void 0);
2623
2868
  return live;
2624
2869
  }
2625
2870
  /** Stop following a run in this process. */
@@ -2733,6 +2978,12 @@ ${body}
2733
2978
  kind: "cancelled"
2734
2979
  }));
2735
2980
  await writer.end();
2981
+ publishAgentRunCancelled({
2982
+ runId,
2983
+ threadId: input.threadId,
2984
+ durationMs,
2985
+ context: agentRunContext(input)
2986
+ });
2736
2987
  await this.store.recordRunEnd?.({
2737
2988
  runId,
2738
2989
  status: "cancelled",
@@ -2763,12 +3014,16 @@ ${body}
2763
3014
  this.logger.error(`agent run ${runId} failed: ${error}`);
2764
3015
  publishAgentRunFailed({
2765
3016
  runId,
3017
+ threadId: input.threadId,
2766
3018
  code: "opencode_failed",
2767
- message: error
3019
+ message: error,
3020
+ durationMs,
3021
+ context: agentRunContext(input)
2768
3022
  });
2769
3023
  await this.store.recordRunEnd?.({
2770
3024
  runId,
2771
3025
  status: "failed",
3026
+ durationMs,
2772
3027
  errorCode: "opencode_failed",
2773
3028
  errorMessage: error
2774
3029
  });
@@ -3197,6 +3452,7 @@ var OpenCodeRunWorkflow = class {
3197
3452
  }
3198
3453
  async run(ctx, input) {
3199
3454
  let spent = emptyUsage();
3455
+ let progress;
3200
3456
  try {
3201
3457
  const begun = await ctx.localStep("begin", async () => ({
3202
3458
  handle: await this.turns.begin(ctx.runId, input),
@@ -3209,7 +3465,8 @@ var OpenCodeRunWorkflow = class {
3209
3465
  });
3210
3466
  for (let n = 0; ; n += 1) {
3211
3467
  const before = spent;
3212
- const milestone = await ctx.localStep(`observe:${n}`, () => this.turns.observe(ctx.runId, input, handle, before));
3468
+ const taken = progress;
3469
+ const milestone = await ctx.localStep(`observe:${n}`, () => this.turns.observe(ctx.runId, input, handle, before, taken));
3213
3470
  spent = addUsage(spent, milestone.usage);
3214
3471
  const total = spent;
3215
3472
  if (milestone.kind === "finished") {
@@ -3235,7 +3492,9 @@ var OpenCodeRunWorkflow = class {
3235
3492
  };
3236
3493
  }
3237
3494
  const ask = milestone.ask;
3238
- handle = await ctx.localStep(`reply:${n}`, () => this.turns.reply(ctx.runId, input, handle, ask, reply, total));
3495
+ progress = milestone.progress ?? progress;
3496
+ const tookSoFar = progress;
3497
+ handle = await ctx.localStep(`reply:${n}`, () => this.turns.reply(ctx.runId, input, handle, ask, reply, total, tookSoFar));
3239
3498
  }
3240
3499
  } catch (error) {
3241
3500
  if (isWorkflowControlFlowSignal(error)) throw error;