@krapht/ork 1.0.0-alpha.15 → 1.0.0-alpha.16

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.
Files changed (46) hide show
  1. package/README.md +10 -4
  2. package/dist/{__build_info__-Bsb1kjgF.mjs → __build_info__-DcRdK0Du.mjs} +25 -6
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/{attach-CPespxlQ.mjs → attach-7lB5_jpr.mjs} +5 -5
  5. package/dist/{attach-BHO3X3cf.mjs → attach-B-FeE1dJ.mjs} +46 -8
  6. package/dist/bin.mjs +238 -369
  7. package/dist/{brief-za0ozN_E.mjs → brief-BvMtIFio.mjs} +4 -4
  8. package/dist/{builtin-files-2bHf76x-.mjs → builtin-files-BRSOObAv.mjs} +1 -1
  9. package/dist/command-tree-BkSYWmYJ.mjs +63 -0
  10. package/dist/{completion-CQ2UYKuJ.mjs → completion-Cgb2qPPh.mjs} +45 -6
  11. package/dist/{context-BNqDw9z1.mjs → context-BN9A2Nnu.mjs} +3 -3
  12. package/dist/{find-BYLlszqj.mjs → find-mLYsR0uC.mjs} +2 -2
  13. package/dist/flags-CdgIH1L6.mjs +230 -0
  14. package/dist/{follow-ups-B4GJ3Vzw.mjs → follow-ups-DxCsOby1.mjs} +59 -24
  15. package/dist/{format-C8MsFj8_.mjs → format-hiDvtNGM.mjs} +17 -4
  16. package/dist/{help-groups-CQ98mLjF.mjs → help-groups-hB6kY34d.mjs} +1 -63
  17. package/dist/{in-a-run-Bp0k0J09.mjs → in-a-run-a447hxq8.mjs} +1 -1
  18. package/dist/{install-DWtXTq4y.mjs → install-23g_1ngs.mjs} +1 -1
  19. package/dist/{limits-B1qzS4cX.mjs → limits-C_Xz_t3C.mjs} +1 -1
  20. package/dist/{mcp-BOrVkyhN.mjs → mcp-CWng9I9n.mjs} +6 -6
  21. package/dist/{once-C1fL3GVo.mjs → once-lvwcWfVf.mjs} +3 -3
  22. package/dist/{pause-DOMK-R8V.mjs → pause-Cy11KrXK.mjs} +3 -3
  23. package/dist/{post-BzA6Qobq.mjs → post-C9soCx7i.mjs} +5 -3
  24. package/dist/print-BaWiWf4v.mjs +19 -0
  25. package/dist/project-records-BRiyQzUo.mjs +230 -0
  26. package/dist/{relay-OWuu4fib.mjs → relay-C0M1TWMB.mjs} +1 -1
  27. package/dist/{run--4-1xAF2.mjs → run-B-j3Qxqc.mjs} +26 -13
  28. package/dist/{run-CiPsW9Kf.mjs → run-D_u9V_Ve.mjs} +1 -1
  29. package/dist/{send-CnNNW-ot.mjs → send-qBxj0JbJ.mjs} +2 -2
  30. package/dist/{src-B9zWIVqo.mjs → src-CXBmaGwS.mjs} +93 -72
  31. package/dist/{src-DcrhgipG.mjs → src-DHDQ0itC.mjs} +91 -42
  32. package/dist/{src-Dr_fTSu-.mjs → src-DN-RPsil.mjs} +781 -502
  33. package/dist/{src-DQKz2eOH.mjs → src-nXLIwDrt.mjs} +9 -12
  34. package/dist/{store-oKJ-cCf3.mjs → store-Dyr1mP8K.mjs} +1 -1
  35. package/dist/ui/assets/app-BPSCxb_l.css +1 -0
  36. package/dist/ui/assets/{app-Drp8Dm9y.js → app-CnNRowFn.js} +40 -40
  37. package/dist/ui/assets/{index-BtwkftKe.js → index-CVzwYlz3.js} +2 -2
  38. package/dist/ui/index.html +1 -1
  39. package/dist/{ui-CzKlsCnm.mjs → ui-DLrSt5ny.mjs} +3 -3
  40. package/dist/{usage-DHGNk2JL.mjs → usage-CxmpYYO4.mjs} +61 -107
  41. package/dist/{validate-Bs_gg22g.mjs → validate-BVrT43GO.mjs} +35 -8
  42. package/dist/{watch-CCCfw3n7.mjs → watch-mkLm4t9D.mjs} +5 -5
  43. package/dist/{workflows-CW28ZU-z.mjs → workflows-Bvl2Fquu.mjs} +2 -2
  44. package/dist/{worktrees-CyE8aF6j.mjs → worktrees-DOOCoS2w.mjs} +49 -20
  45. package/package.json +8 -4
  46. package/dist/ui/assets/app-Bdc6OBeZ.css +0 -1
@@ -1,4 +1,4 @@
1
- import { Bn as outcomeEnd, Et as parseReport, Gn as FEEDBACK_SEVERITIES, Gt as programFound, Hn as END_KINDS, Kn as FEEDBACK_TARGETS, Kt as startAt, Nn as countedTokens, Un as endFor, Wn as FEEDBACK_KINDS, Wt as lowerChild, _i as AGENT_DRIVERS, fr as maskSecrets, ur as MASK } from "./src-Dr_fTSu-.mjs";
1
+ import { It as lowerChild, Jn as END_KINDS, Kn as outcomeEnd, Lt as programFound, Qn as FEEDBACK_TARGETS, Rt as startAt, Xn as FEEDBACK_KINDS, Yn as endFor, Zn as FEEDBACK_SEVERITIES, gr as MASK, un as FOLLOW_UP_KINDS, vr as maskSecrets, vt as parseReport, wi as AGENT_DRIVERS, zn as countedTokens } from "./src-DN-RPsil.mjs";
2
2
  import { n as holdGuard, r as orphanGuard } from "./orphan-guard-BFKg8VSd.mjs";
3
3
  import path from "node:path";
4
4
  import { z } from "zod";
@@ -9,13 +9,6 @@ import { createInterface } from "node:readline";
9
9
  import { execFile, spawn } from "node:child_process";
10
10
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
11
11
  import { createServer } from "node:http";
12
- // ../core/src/follow-ups/types.ts
13
- const FOLLOW_UP_KINDS = [
14
- "change",
15
- "message",
16
- "task",
17
- "check"
18
- ];
19
12
  // ../runtime/src/hold.ts
20
13
  /**
21
14
  * Waits at a tool call while `hold` is held: "go" once it is released within
@@ -456,8 +449,10 @@ const classifyProviderError = (text, status) => {
456
449
  * it can see (a turn cap, an exit code, a provider's error), and the report
457
450
  * the session handed in decides the rest. The runner then writes the end in
458
451
  * the node's numbers, its detail scrubbed with the feedback redactor. A
459
- * session the driver could not run ends as `driverFailure` says, and one that
460
- * reached its token cap as `tokenCapOutcome` says.
452
+ * session the driver could not run (or that was stopped, with what it had
453
+ * spent) ends as `driverFailure` says, one whose process died as
454
+ * `failureReason` explains, and one that reached its token cap as
455
+ * `tokenCapOutcome` says.
461
456
  */
462
457
  /** The end a report gives: done, a reported failure, or none handed in. */
463
458
  const reportEnd = (report) => {
@@ -482,9 +477,15 @@ const errorEnd = (text, fallback = "unknown", status) => endFor(classifyProvider
482
477
  */
483
478
  const settleEnd = (outcome, facts, scrub) => {
484
479
  const end = outcome.report === null ? outcome.end ?? outcomeEnd(outcome) : reportEnd(outcome.report);
480
+ const { usage } = outcome;
481
+ const used = {
482
+ ...facts,
483
+ usedTokens: countedTokens(usage),
484
+ ...usage.costUsd > 0 ? { usedUsd: usage.costUsd } : {}
485
+ };
485
486
  return {
486
487
  ...outcome,
487
- end: endFor(end.kind, facts, end.detail, scrub)
488
+ end: endFor(end.kind, used, end.detail, scrub)
488
489
  };
489
490
  };
490
491
  /**
@@ -517,6 +518,17 @@ const tokenCapOutcome = ({ budgetTokens }, usage, sessionId) => budgetTokens ===
517
518
  usage,
518
519
  errors: [`Reached the token budget (${String(budgetTokens)})`]
519
520
  };
521
+ const REASON_LIMIT = 500;
522
+ /** Noise a Node crash ends stderr with, or a stack frame: never the reason. */
523
+ const isNoise = (line) => /^Node\.js v\d/u.test(line) || /^\s+at /u.test(line);
524
+ /**
525
+ * Why a process ended without a result: the last block of its stderr as one
526
+ * masked, capped line (the reason reaches the terminal, which is not masked).
527
+ */
528
+ const failureReason = (name, stderr, code) => {
529
+ const block = stderr.split("\n").filter((line) => !isNoise(line)).join("\n").split(/\n\s*\n/u).map((text) => text.trim().replaceAll(/\s*\n\s*/gu, " ")).findLast((text) => text !== "");
530
+ return block === void 0 ? `${name} exited with code ${String(code)}` : maskSecrets(block).slice(0, REASON_LIMIT);
531
+ };
520
532
  /**
521
533
  * The kind of a failure to run the driver: its program missing, else what a
522
534
  * provider's words in it say, else a crash.
@@ -527,27 +539,32 @@ const failureKind = (error) => {
527
539
  if (/\bENOENT\b/u.test(text)) return "tool_missing";
528
540
  return classifyProviderError(text)?.kind ?? "driver_crashed";
529
541
  };
542
+ const NOTHING_SPENT = {
543
+ usage: {
544
+ costUsd: 0,
545
+ inputTokens: 0,
546
+ outputTokens: 0,
547
+ cacheReadTokens: 0,
548
+ cacheWriteTokens: 0,
549
+ turns: 0,
550
+ durationMs: 0
551
+ },
552
+ sessionId: ""
553
+ };
530
554
  /**
531
555
  * A finished event for a session the driver could not run, ended as `kind` with
532
- * `message` (Ork's words or the tool's, masked) as its detail.
556
+ * `message` (Ork's words or the tool's, masked) as its detail; a stopped one
557
+ * keeps what it had `spent` (its tokens decoded so far, dollars $0).
533
558
  */
534
- const driverFailure = (message, kind, scope) => ({
559
+ const driverFailure = (message, kind, scope, spent = NOTHING_SPENT) => ({
535
560
  type: "session.finished",
536
561
  outcome: {
537
562
  subtype: "error_driver",
538
563
  end: endFor(kind, scope.facts, kind === "interrupted" ? void 0 : message, scope.scrub),
539
- sessionId: "",
564
+ sessionId: spent.sessionId,
540
565
  report: null,
541
566
  text: "",
542
- usage: {
543
- costUsd: 0,
544
- inputTokens: 0,
545
- outputTokens: 0,
546
- cacheReadTokens: 0,
547
- cacheWriteTokens: 0,
548
- turns: 0,
549
- durationMs: 0
550
- },
567
+ usage: spent.usage,
551
568
  errors: [message]
552
569
  }
553
570
  });
@@ -608,7 +625,6 @@ const spawnLeg = (start) => {
608
625
  * Windows the process ends with `ork`, whatever ends `ork` (the orphan
609
626
  * guard).
610
627
  */
611
- const REASON_LIMIT = 500;
612
628
  /** Marks a session's start with the driver that runs it. */
613
629
  const withDriver = (event, driver) => event.type === "session.started" ? {
614
630
  ...event,
@@ -622,10 +638,11 @@ const settled = (outcome, input) => settleEnd(input.reported === void 0 ? outcom
622
638
  ...outcome,
623
639
  report: input.reported()
624
640
  }, input.scope.facts, input.scope.scrub);
641
+ /** What this leg `used`, with what the legs before it spent. */
642
+ const withCarried = (input, used) => used === void 0 || input.carried === void 0 ? used ?? input.carried : input.adapter.usage.combine(input.carried, used);
625
643
  /** The session's end at its token cap, its earlier legs' usage included. */
626
644
  const tokenCapEnd = (input, used, sessionId) => {
627
- const usage = used === void 0 || input.carried === void 0 ? used : input.adapter.usage.combine(input.carried, used);
628
- const capped = tokenCapOutcome(input.scope.facts, usage, sessionId);
645
+ const capped = tokenCapOutcome(input.scope.facts, used === void 0 ? void 0 : withCarried(input, used), sessionId);
629
646
  return capped === void 0 ? void 0 : {
630
647
  type: "session.finished",
631
648
  outcome: settled(capped, input)
@@ -645,37 +662,36 @@ const legStep = (decoded, input) => {
645
662
  const events = [];
646
663
  let ended;
647
664
  for (const said of decoded.events) {
648
- const event = said.type === "session.finished" ? {
665
+ if (said.type === "session.started" && carried !== void 0) continue;
666
+ if (said.type !== "session.finished") {
667
+ events.push(withDriver(said, adapter.driver));
668
+ continue;
669
+ }
670
+ const own = said.outcome.usage;
671
+ const usage = carried === void 0 ? own : adapter.usage.combine(carried, own);
672
+ const last = {
649
673
  ...said,
650
- outcome: settled(said.outcome, input)
651
- } : said;
652
- if (event.type === "session.started" && carried !== void 0) continue;
653
- if (event.type !== "session.finished") events.push(withDriver(event, adapter.driver));
654
- else if (deferred) ended = {
674
+ outcome: settled({
675
+ ...said.outcome,
676
+ usage
677
+ }, input)
678
+ };
679
+ const { sessionId, end } = last.outcome;
680
+ if (deferred) ended = {
655
681
  kind: "deferred",
656
- sessionId: event.outcome.sessionId,
657
- usage: event.outcome.usage,
658
- last: event
682
+ sessionId,
683
+ usage: own,
684
+ last
685
+ };
686
+ else if (input.withhold() && wentWell(end?.kind)) ended = {
687
+ kind: "continued",
688
+ sessionId,
689
+ usage: own,
690
+ last
659
691
  };
660
692
  else {
661
- const usage = carried === void 0 ? event.outcome.usage : adapter.usage.combine(carried, event.outcome.usage);
662
- const last = {
663
- ...event,
664
- outcome: {
665
- ...event.outcome,
666
- usage
667
- }
668
- };
669
- if (input.withhold() && wentWell(event.outcome.end?.kind)) ended = {
670
- kind: "continued",
671
- sessionId: event.outcome.sessionId,
672
- usage: event.outcome.usage,
673
- last
674
- };
675
- else {
676
- events.push(last);
677
- ended = { kind: "finished" };
678
- }
693
+ events.push(last);
694
+ ended = { kind: "finished" };
679
695
  }
680
696
  }
681
697
  return ended === void 0 ? { events } : {
@@ -683,16 +699,6 @@ const legStep = (decoded, input) => {
683
699
  ended
684
700
  };
685
701
  };
686
- /** Noise a Node crash ends stderr with, or a stack frame: never the reason. */
687
- const isNoise = (line) => /^Node\.js v\d/u.test(line) || /^\s+at /u.test(line);
688
- /**
689
- * Why a process ended without a result: the last block of its stderr as one
690
- * masked, capped line (the reason reaches the terminal, which is not masked).
691
- */
692
- const failureReason = (name, stderr, code) => {
693
- const block = stderr.split("\n").filter((line) => !isNoise(line)).join("\n").split(/\n\s*\n/u).map((text) => text.trim().replaceAll(/\s*\n\s*/gu, " ")).findLast((text) => text !== "");
694
- return block === void 0 ? `${name} exited with code ${String(code)}` : maskSecrets(block).slice(0, REASON_LIMIT);
695
- };
696
702
  /** Runs one process of a session, yielding its events; returns how it ended. */
697
703
  const runLeg = async function* (input) {
698
704
  const child = spawnLeg(input);
@@ -728,11 +734,14 @@ const runLeg = async function* (input) {
728
734
  if (input.adapter.command.input === void 0) child.stdin.end();
729
735
  let outcome;
730
736
  let sessionId = "";
737
+ /** What this leg has used so far, as the last usage step said. */
738
+ let used;
731
739
  const { decoder } = input;
732
740
  const decode = decoder?.decode ?? input.adapter.decode;
733
741
  try {
734
742
  for await (const line of createInterface({ input: child.stdout })) {
735
743
  const decoded = decode(line);
744
+ used = decoded.usage ?? used;
736
745
  const { events, ended } = legStep(decoded, input);
737
746
  for (const event of decoded.events) if (event.type === "session.started") sessionId = event.sessionId;
738
747
  yield* events;
@@ -760,11 +769,18 @@ const runLeg = async function* (input) {
760
769
  reason: `could not start ${input.bin}: ${end.message}`,
761
770
  end: failureKind(end)
762
771
  };
763
- if (stopped || end.signal !== null) return {
764
- kind: "failed",
765
- reason: "interrupted",
766
- end: "interrupted"
767
- };
772
+ if (stopped || end.signal !== null) {
773
+ const usage = withCarried(input, used);
774
+ return {
775
+ kind: "failed",
776
+ reason: "interrupted",
777
+ end: "interrupted",
778
+ ...usage === void 0 ? {} : { spent: {
779
+ usage,
780
+ sessionId
781
+ } }
782
+ };
783
+ }
768
784
  const reason = failureReason(input.adapter.command.bin, stderr, end.code);
769
785
  return {
770
786
  kind: "failed",
@@ -850,6 +866,7 @@ const releaseTool = defineTool({
850
866
  });
851
867
  // ../runtime/src/tools/delegate.ts
852
868
  const BRIEF = z.string().min(1).describe("Self-contained: the goal, the files that matter, and what done looks like. The child does not see your conversation.");
869
+ const DESCRIPTION = z.string().exactOptional().describe("A few words naming this task; shown as the child's label.");
853
870
  /** What the leader reads back: each report without the usage details. */
854
871
  const reportLine = (report) => {
855
872
  const { usage, ...output } = report.output;
@@ -875,7 +892,8 @@ const delegateTool = defineTool({
875
892
  const ids = ports.ownership?.children.map((child) => child.id) ?? [];
876
893
  return { tasks: z.array(z.strictObject({
877
894
  child: z.enum(ids).describe(`The id of a node you own: ${ids.join(", ")}.`),
878
- brief: BRIEF
895
+ brief: BRIEF,
896
+ description: DESCRIPTION
879
897
  })).min(1) };
880
898
  },
881
899
  available: ({ ports }) => (ports.ownership?.children.length ?? 0) > 0,
@@ -891,7 +909,8 @@ const spawnWorkerTool = defineTool({
891
909
  const ids = ports.ownership?.templates ?? [];
892
910
  return { tasks: z.array(z.strictObject({
893
911
  template: z.enum(ids).describe(`The id of a child worker you own: ${ids.join(", ")}.`),
894
- brief: BRIEF
912
+ brief: BRIEF,
913
+ description: DESCRIPTION
895
914
  })).min(1) };
896
915
  },
897
916
  available: ({ ports }) => ports.ownership !== void 0 && ports.ownership.templates.length > 0 && ports.ownership.workersLeft() > 0,
@@ -959,7 +978,7 @@ const followUpTool = defineTool({
959
978
  text: z.string().min(1).max(4e3)
960
979
  }).optional().describe("For a message: what the maintainer will send."),
961
980
  steps: z.array(z.strictObject({
962
- text: z.string().min(1),
981
+ text: z.string().trim().min(1),
963
982
  command: z.string().optional().describe("Shown to the maintainer to copy; Ork never runs it.")
964
983
  })).max(20).optional().describe("For a task or check: what to do, in order.")
965
984
  },
@@ -1335,7 +1354,9 @@ const createSessionRuntime = (adapter, options = {}) => ({
1335
1354
  }
1336
1355
  const leg = yield* runLeg(legOf(legSpec, spent));
1337
1356
  if (leg.kind === "failed") {
1338
- yield driverFailure(leg.reason, leg.end, scope);
1357
+ const { spent: stopped } = leg;
1358
+ if (stopped?.sessionId === "") stopped.sessionId = legSpec.resume ?? "";
1359
+ yield driverFailure(leg.reason, leg.end, scope, stopped);
1339
1360
  return;
1340
1361
  }
1341
1362
  if (leg.kind === "finished") return;
@@ -1,5 +1,5 @@
1
- import { $t as sessionDriver, Bn as outcomeEnd, Bt as stoppedEnd, Dt as reportSchemaFor, Ft as runScope, Gr as beforeOf, Ht as slotClock, It as calledWorkflows, Kr as programsRunBy, Kt as startAt, Nn as countedTokens, Pt as heldTimeout, Rt as endedWith, Tt as planNodeSession, Un as endFor, Vn as END_DETAIL_LIMIT, Vt as declaredProblem, Wt as lowerChild, Xr as markValue, Xt as envReadsOf, _i as AGENT_DRIVERS, ar as renderTemplate, ci as timeoutMs, di as integrationFor, ii as isToolNode, in as serversIn, ir as parseTemplate, li as INTEGRATIONS, nn as MCP_FILE, nt as sessionRefusals, ri as isClaudeNode, rn as mcpNotText, rr as evaluate, tn as declaredOutputOf, ui as TOOL_PREFIX, xt as planReason, zn as endOf, zt as failedWith } from "./src-Dr_fTSu-.mjs";
2
- import { t as runOnce } from "./run-CiPsW9Kf.mjs";
1
+ import { $r as programsRunBy, At as endedWith, Dt as runScope, Et as heldTimeout, Gn as endOf, It as lowerChild, Kn as outcomeEnd, Mt as stoppedEnd, Nt as declaredProblem, Ot as calledWorkflows, Pt as slotClock, Qr as beforeOf, Rt as startAt, Y as sessionRefusals, Yn as endFor, _i as TOOL_PREFIX, _t as planNodeSession, cn as mcpNotText, di as isToolNode, dr as renderTemplate, gi as INTEGRATIONS, hi as timeoutMs, in as sessionDriver, jt as failedWith, ln as serversIn, lr as evaluate, mt as planReason, on as declaredOutputOf, qn as END_DETAIL_LIMIT, ri as markValue, sn as MCP_FILE, tn as envReadsOf, ui as isClaudeNode, ur as parseTemplate, vi as integrationFor, wi as AGENT_DRIVERS, yt as reportSchemaFor, zn as countedTokens } from "./src-DN-RPsil.mjs";
2
+ import { t as runOnce } from "./run-D_u9V_Ve.mjs";
3
3
  import { n as holdGuard, r as orphanGuard, t as KILL_AFTER_MS } from "./orphan-guard-BFKg8VSd.mjs";
4
4
  import path from "node:path";
5
5
  import { accessSync, constants, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
@@ -60,7 +60,9 @@ const callExecutor = (context) => {
60
60
  // ../nodes/src/claude-plan.ts
61
61
  /**
62
62
  * @file What a Claude node's session is planned with, read from the node's
63
- * context.
63
+ * context, and what it is told beyond its plan: the note of a sent-back input
64
+ * on re-entry, a resumed leader's message, and the turn for messages that
65
+ * came in as it finished.
64
66
  */
65
67
  /** The plan options every session of a node starts from. */
66
68
  const planOptionsOf = (context, inPlace) => {
@@ -83,6 +85,30 @@ const planOptionsOf = (context, inPlace) => {
83
85
  if (context.claims !== void 0) planOptions.claims = context.claims;
84
86
  return planOptions;
85
87
  };
88
+ const MAX_INPUT = 8192;
89
+ const inputJson = (input) => JSON.stringify(input ?? {}, void 0, 2);
90
+ /** What a sent-back input is whole, for `read_input`, when its note cuts it. */
91
+ const wholeInput = (input) => {
92
+ const json = inputJson(input);
93
+ return json.length > MAX_INPUT ? json : void 0;
94
+ };
95
+ /** A re-entry's note of what was sent back; `read_input` has a cut rest. */
96
+ const reentryNote = (round, input) => {
97
+ const json = inputJson(input);
98
+ const cut = json.length > MAX_INPUT;
99
+ const body = cut ? `${json.slice(0, MAX_INPUT)}\n…` : json;
100
+ const rest = cut ? `\n\nThat is the first ${String(MAX_INPUT)} of its ${String(json.length)} characters. Call read_input for all of it before you act on it.` : "";
101
+ return `\n\n## Round ${String(round)}\n\nThe workflow sent this task back to you with:\n\n\`\`\`json\n${body}\n\`\`\`${rest}`;
102
+ };
103
+ /**
104
+ * What a resumed leader is told: it already has its brief and its earlier work,
105
+ * so only the new round, and the new task when its owner sent one.
106
+ */
107
+ const resumeMessage = (round, input, brief) => [brief === void 0 ? sentBack(round, input) : `## New task from \`${brief.from}\`\n\n${brief.text}`, "End with a new structured report."].join("\n\n");
108
+ /** A first-round resume is a session the run was cut off in (`ork resume`). */
109
+ const sentBack = (round, input) => round === 1 ? "Ork was stopped while you worked on this task, and the run is resumed. Your earlier turns are above: check what you finished, and carry on from there." : `The workflow sent your task back to you.${reentryNote(round, input)}`;
110
+ /** What a session is told when messages came in as it finished. */
111
+ const lateMessage = (count) => `[ork] ${String(count)} message${count === 1 ? "" : "s"} came in while you finished. Call read_inbox, act on ${count === 1 ? "it" : "them"} if needed, and end with a new structured report.`;
86
112
  // ../nodes/src/claude-result.ts
87
113
  /**
88
114
  * @file A finished session as a node's result: its report is the output, held
@@ -552,12 +578,13 @@ const leftOf = (caps, used, quotaLeft) => least(...caps.map((cap) => cap === voi
552
578
  * budget has counted. The quota has: the ledger took it as the session
553
579
  * finished, so what it says is left is left. The run's tokens left have too:
554
580
  * only the node's own `budgetTokens` takes `tokens` off. `null` when nothing
555
- * is, and the turn does not start.
581
+ * is, and the turn does not start. `turnsAtLeast` lifts the turns left to that
582
+ * many (a failed check's resume: `CHECK_RETRY_TURNS`).
556
583
  */
557
- const lateLimits = (spec, used, runLeft, quotaLeft, tokensLeft = {}) => {
584
+ const lateLimits = (spec, used, runLeft, quotaLeft, tokensLeft = {}, turnsAtLeast = 0) => {
558
585
  const budgetUsd = leftOf([spec.budgetUsd, runLeft], used.spent, quotaLeft);
559
586
  const budgetTokens = leftOf([spec.budgetTokens], used.tokens ?? 0, least(tokensLeft.run, tokensLeft.quota));
560
- const maxTurns = spec.maxTurns === void 0 ? void 0 : spec.maxTurns - used.turns;
587
+ const maxTurns = spec.maxTurns === void 0 ? void 0 : Math.max(spec.maxTurns - used.turns, turnsAtLeast);
561
588
  if ([
562
589
  budgetUsd,
563
590
  budgetTokens,
@@ -569,18 +596,36 @@ const lateLimits = (spec, used, runLeft, quotaLeft, tokensLeft = {}) => {
569
596
  ...maxTurns === void 0 ? {} : { maxTurns }
570
597
  };
571
598
  };
599
+ const reached = (cap, used) => cap !== void 0 && used >= cap;
600
+ /**
601
+ * Why the run's budget stopped a session that reached its cap, if it did. A
602
+ * unit blames the run when the session reached it, or when it is the only cap
603
+ * the session had; otherwise the node's own cap in the other unit may have
604
+ * stopped it. A resumed turn's dollars are its own: its cost is the session's
605
+ * total (ADR-046) while its cap is what was left, so `priorCostUsd` comes off,
606
+ * read as `ownSpend` reads it (a lower total counted again from zero).
607
+ */
608
+ const runSpent = (run, asked, outcome, priorCostUsd) => {
609
+ const total = outcome.usage.costUsd;
610
+ const turnCostUsd = total < priorCostUsd ? total : total - priorCostUsd;
611
+ if (run.tokens !== void 0 && (reached(asked.budgetTokens, countedTokens(outcome.usage)) || asked.budgetUsd === void 0)) return `the run's budget of ${String(run.tokens)} tokens is spent`;
612
+ return run.usd !== void 0 && (reached(asked.budgetUsd, turnCostUsd) || asked.budgetTokens === void 0) ? `the run's budget of $${String(run.usd)} is spent` : void 0;
613
+ };
572
614
  /**
573
615
  * Whether a limit across runs, rather than the node's work, ended a node: a
574
616
  * session whose cap was a quota's and that ran into it, or one its plan refused
575
- * and that gave no report. A node that still succeeded is left alone.
617
+ * and that gave no report. A session whose cap was the run's budget and that
618
+ * ran into it stops the run as `budget_exhausted` (`run`, from `runCapsOf`). A
619
+ * node that still succeeded is left alone.
576
620
  */
577
- const limitWatch = (context) => {
621
+ const limitWatch = (context, run = {}) => {
578
622
  let why;
623
+ let runWhy;
579
624
  return {
580
625
  stoppedBy: (reason) => {
581
626
  why = reason;
582
627
  },
583
- session: (asked) => {
628
+ session: (asked, priorCostUsd = 0) => {
584
629
  const { leftUsd: left, leftTokens } = context.quota?.() ?? {};
585
630
  let refused;
586
631
  return {
@@ -592,6 +637,7 @@ const limitWatch = (context) => {
592
637
  const capped = left !== void 0 && asked.budgetUsd !== void 0 && asked.budgetUsd >= left || leftTokens !== void 0 && asked.budgetTokens !== void 0 && asked.budgetTokens >= leftTokens;
593
638
  const spent = outcome !== null && outcomeEnd(outcome).kind === "budget_limit";
594
639
  if (capped && spent) why = context.quota?.().blocked ?? "a session reached what was left of a quota";
640
+ else if (spent) runWhy = runSpent(run, asked, outcome, priorCostUsd);
595
641
  }
596
642
  };
597
643
  },
@@ -601,6 +647,11 @@ const limitWatch = (context) => {
601
647
  ...endedWith(result, endFor("plan_limit", {}, why)),
602
648
  limited: why
603
649
  };
650
+ if (runWhy !== void 0) return {
651
+ ...result,
652
+ limited: runWhy,
653
+ stopsAs: "budget_exhausted"
654
+ };
604
655
  const { end } = result;
605
656
  return end?.kind === "plan_limit" ? {
606
657
  ...result,
@@ -678,9 +729,31 @@ const startWith = async (context, left, own, reserve) => {
678
729
  * `budgetUsd`. With nothing left, it waits for a worker running beside it to
679
730
  * finish, as long as one may.
680
731
  */
681
- const budgetToStart = (context) => startWith(context, () => budgetNow(context), isClaudeNode(context.node) ? context.node.budgetUsd ?? context.workflow.defaults?.budgetUsd : void 0, context.reserveBudget);
732
+ const budgetToStart = (context) => startWith(context, () => budgetNow(context), ownCaps(context).usd, context.reserveBudget);
733
+ /** The node's own caps: its `budgetUsd` and `budgetTokens`, else the defaults'. */
734
+ const ownCaps = (context) => isClaudeNode(context.node) ? {
735
+ usd: context.node.budgetUsd ?? context.workflow.defaults?.budgetUsd,
736
+ tokens: context.node.budgetTokens ?? context.workflow.defaults?.budgetTokens
737
+ } : {};
738
+ /**
739
+ * Where the run's budget, not the node's own cap, caps the node's first
740
+ * session: the run has a budget in that unit, and what `startBudgets` left the
741
+ * session is below the node's own cap, or it has none. A later turn of the
742
+ * session keeps them: both its caps go down by what it used, and the run's by
743
+ * what others used too.
744
+ */
745
+ const runCapsOf = (context, planOptions) => {
746
+ const own = ownCaps(context);
747
+ const limits = context.workflow.limits;
748
+ const caps = {};
749
+ const usd = planOptions.budgetLeftUsd;
750
+ if (context.budgetLeftNow?.() !== void 0 && limits?.budgetUsd !== void 0 && usd !== void 0 && usd < (own.usd ?? Infinity)) caps.usd = limits.budgetUsd;
751
+ const tokens = planOptions.budgetLeftTokens;
752
+ if (context.tokensLeftNow?.() !== void 0 && limits?.budgetTokens !== void 0 && tokens !== void 0 && tokens < (own.tokens ?? Infinity)) caps.tokens = limits.budgetTokens;
753
+ return caps;
754
+ };
682
755
  /** `budgetToStart` in counted tokens: the run's `budgetTokens` and a quota's. */
683
- const tokensToStart = (context) => startWith(context, () => tokensNow(context), isClaudeNode(context.node) ? context.node.budgetTokens ?? context.workflow.defaults?.budgetTokens : void 0, context.reserveTokens);
756
+ const tokensToStart = (context) => startWith(context, () => tokensNow(context), ownCaps(context).tokens, context.reserveTokens);
684
757
  /**
685
758
  * What a node's first session may spend, in dollars and in counted tokens, set
686
759
  * into `planOptions`; the node's result instead when nothing is left of
@@ -1730,30 +1803,6 @@ const checkedResult = (result, { check, resumes }) => {
1730
1803
  * through the session's ports, and resumes its session on re-entry
1731
1804
  * (ADR-019).
1732
1805
  */
1733
- const MAX_INPUT = 8192;
1734
- const inputJson = (input) => JSON.stringify(input ?? {}, void 0, 2);
1735
- /** What a sent-back input is whole, for `read_input`, when its note cuts it. */
1736
- const wholeInput = (input) => {
1737
- const json = inputJson(input);
1738
- return json.length > MAX_INPUT ? json : void 0;
1739
- };
1740
- /** A re-entry's note of what was sent back; `read_input` has a cut rest. */
1741
- const reentryNote = (round, input) => {
1742
- const json = inputJson(input);
1743
- const cut = json.length > MAX_INPUT;
1744
- const body = cut ? `${json.slice(0, MAX_INPUT)}\n…` : json;
1745
- const rest = cut ? `\n\nThat is the first ${String(MAX_INPUT)} of its ${String(json.length)} characters. Call read_input for all of it before you act on it.` : "";
1746
- return `\n\n## Round ${String(round)}\n\nThe workflow sent this task back to you with:\n\n\`\`\`json\n${body}\n\`\`\`${rest}`;
1747
- };
1748
- /**
1749
- * What a resumed leader is told: it already has its brief and its earlier work,
1750
- * so only the new round, and the new task when its owner sent one.
1751
- */
1752
- const resumeMessage = (round, input, brief) => [brief === void 0 ? sentBack(round, input) : `## New task from \`${brief.from}\`\n\n${brief.text}`, "End with a new structured report."].join("\n\n");
1753
- /** A first-round resume is a session the run was cut off in (`ork resume`). */
1754
- const sentBack = (round, input) => round === 1 ? "Ork was stopped while you worked on this task, and the run is resumed. Your earlier turns are above: check what you finished, and carry on from there." : `The workflow sent your task back to you.${reentryNote(round, input)}`;
1755
- /** What a session is told when messages came in as it finished. */
1756
- const lateMessage = (count) => `[ork] ${String(count)} message${count === 1 ? "" : "s"} came in while you finished. Call read_inbox, act on ${count === 1 ? "it" : "them"} if needed, and end with a new structured report.`;
1757
1806
  /** A re-entered leader resumes; anyone else starts with the full brief. */
1758
1807
  const specFor = (spec, context) => {
1759
1808
  if (context.resume !== void 0) return {
@@ -1820,9 +1869,9 @@ const claudeExecutor = (options) => async (context) => {
1820
1869
  if (context.followUps !== void 0) ports.followUps = context.followUps;
1821
1870
  const whole = context.round > 1 ? wholeInput(context.input) : void 0;
1822
1871
  if (whole !== void 0) ports.input = whole;
1823
- const watch = limitWatch(context);
1824
- const run = async (asked) => {
1825
- const watching = watch.session(asked);
1872
+ const watch = limitWatch(context, runCapsOf(context, planOptions));
1873
+ const run = async (asked, prior = 0) => {
1874
+ const watching = watch.session(asked, prior);
1826
1875
  context.reserveBudget?.(asked.budgetUsd);
1827
1876
  context.reserveTokens?.(asked.budgetTokens);
1828
1877
  const session = runtime.start(asked, ports);
@@ -1851,7 +1900,7 @@ const claudeExecutor = (options) => async (context) => {
1851
1900
  return outcome;
1852
1901
  };
1853
1902
  const first = await run(spec);
1854
- const limitsAfter = ({ sessionId, usage }) => {
1903
+ const limitsAfter = ({ sessionId, usage }, turnsAtLeast = 0) => {
1855
1904
  const quota = context.quota?.();
1856
1905
  if (quota?.blocked !== void 0) {
1857
1906
  watch.stoppedBy(quota.blocked);
@@ -1864,14 +1913,14 @@ const claudeExecutor = (options) => async (context) => {
1864
1913
  }, context.budgetLeftNow?.(), quota?.leftUsd, {
1865
1914
  run: context.tokensLeftNow?.(),
1866
1915
  quota: quota?.leftTokens
1867
- });
1916
+ }, turnsAtLeast);
1868
1917
  };
1869
1918
  const resumeWith = (outcome, prompt, limits) => run({
1870
1919
  ...spec,
1871
1920
  ...limits,
1872
1921
  resume: outcome.sessionId,
1873
1922
  prompt
1874
- });
1923
+ }, outcome.usage.costUsd);
1875
1924
  const lateTurn = (current) => {
1876
1925
  const late = context.mailbox?.pending() ?? 0;
1877
1926
  const limits = limitsAfter(current);
@@ -1897,7 +1946,7 @@ const claudeExecutor = (options) => async (context) => {
1897
1946
  },
1898
1947
  late: lateTurn,
1899
1948
  resume: async (current, check) => {
1900
- const limits = limitsAfter(current);
1949
+ const limits = limitsAfter(current, 20);
1901
1950
  if (limits === null) return null;
1902
1951
  await context.hold?.wait("start", context.signal);
1903
1952
  return context.signal.aborted ? null : resumeWith(current, checkFailedMessage(check, {