@krapht/ork 1.0.0-alpha.13 → 1.0.0-alpha.14

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 (42) hide show
  1. package/dist/{__build_info__-Da73_nBs.mjs → __build_info__-B82-dGJY.mjs} +2 -2
  2. package/dist/{attach-E6NNyA3T.mjs → attach-CdtGX9se.mjs} +5 -5
  3. package/dist/bin.mjs +194 -215
  4. package/dist/{builtin-files-n6o6oWpB.mjs → builtin-files-Bdq23IIi.mjs} +1 -1
  5. package/dist/{chips-CyeB_rhR.mjs → chips-BqcT58qr.mjs} +1 -1
  6. package/dist/{completion-SNNNxnv9.mjs → completion--ywde-56.mjs} +52 -11
  7. package/dist/{context-DHudLA5e.mjs → context-Y_I4Mofq.mjs} +17 -7
  8. package/dist/{find-CjsNYAAf.mjs → find-BVX-bL4u.mjs} +1 -1
  9. package/dist/{format-C60TFpRe.mjs → format-CvTE5CZQ.mjs} +12 -12
  10. package/dist/{help-groups-CyfPWlST.mjs → help-groups-CpR2UzSn.mjs} +16 -6
  11. package/dist/{in-a-run-BYjTXaST.mjs → in-a-run-cL5N9gpH.mjs} +1 -1
  12. package/dist/{limits-C5QKZNLT.mjs → limits-Dhe--wmX.mjs} +1 -1
  13. package/dist/{mcp-bcctR-wc.mjs → mcp-CsmboyXv.mjs} +6 -6
  14. package/dist/{once-DPQu5Q3H.mjs → once-CrwI-ZdN.mjs} +4 -4
  15. package/dist/{output-CnRzrbUM.mjs → output-B1kwo0YU.mjs} +17 -1
  16. package/dist/{pause-BPvlDIvz.mjs → pause-DssrYcda.mjs} +4 -4
  17. package/dist/{relay-C8W0t_zc.mjs → relay-DASRgsTQ.mjs} +2 -2
  18. package/dist/{run-CQIfq918.mjs → run-D8divGCs.mjs} +1 -1
  19. package/dist/send-CXv8bx0i.mjs +105 -0
  20. package/dist/{src-CGJGL1sZ.mjs → src-8Mul0695.mjs} +8 -10
  21. package/dist/{src-D93tHZ5B.mjs → src-C7z_6fhn.mjs} +92 -39
  22. package/dist/{src-CqRqsb2t.mjs → src-njdtejgZ.mjs} +98 -33
  23. package/dist/{src-BeR6f70A.mjs → src-tsuZRu0H.mjs} +2353 -918
  24. package/dist/{store-CM3cERGl.mjs → store-CleF7KeS.mjs} +21 -73
  25. package/dist/ui/assets/app-D_nAPnHM.css +1 -0
  26. package/dist/ui/assets/{app-DYRC0V4p.js → app-DqBAtXSZ.js} +19 -19
  27. package/dist/ui/assets/{index-CWwm8HTG.js → index-D-_uPI2v.js} +2 -2
  28. package/dist/ui/index.html +1 -1
  29. package/dist/{ui-D2Ydofq0.mjs → ui-CNO1M0qT.mjs} +4 -4
  30. package/dist/{usage-6WIowblz.mjs → usage-BVXi0iFY.mjs} +13 -141
  31. package/dist/{verdict-CIy8tAyX.mjs → verdict-CMvPCpWC.mjs} +2 -2
  32. package/dist/{watch-uOU_WeBI.mjs → watch-DVNIciHp.mjs} +10 -8
  33. package/dist/{workflows-7tQdMnrA.mjs → workflows-B30QkwTk.mjs} +3 -3
  34. package/dist/{worktrees-ZCII298F.mjs → worktrees-DD4_TpkH.mjs} +3 -5
  35. package/package.json +2 -2
  36. package/dist/clean-Bh4KZgzc.mjs +0 -91
  37. package/dist/decide-DuIHACvH.mjs +0 -50
  38. package/dist/list-BUb5Uc3H.mjs +0 -117
  39. package/dist/remove-BkFWGVYo.mjs +0 -201
  40. package/dist/report-CEN8VoUb.mjs +0 -140
  41. package/dist/send-CaZlGG5a.mjs +0 -163
  42. package/dist/ui/assets/app-QoHVi-mb.css +0 -1
@@ -1,5 +1,5 @@
1
- import { $t as serversIn, At as endedWith, Dr as integrationFor, Er as TOOL_PREFIX, Ht as envReadsOf, It as lowerChild, Kt as declaredOutputOf, Lt as startAt, M as sessionRefusals, Mt as stoppedEnd, Nr as AGENT_DRIVERS, Nt as calledWorkflows, Ot as runScope, Pt as slotClock, Qt as mcpNotText, Tr as INTEGRATIONS, Wt as sessionDriver, Zt as MCP_FILE, an as endFor, bn as renderTemplate, br as isToolNode, cr as programsRunBy, fr as markValue, gt as heldTimeout, jt as failedWith, kt as declaredProblem, lt as reportSchemaFor, nn as outcomeEnd, rn as END_DETAIL_LIMIT, rt as planReason, sr as beforeOf, st as planNodeSession, tn as endOf, vn as evaluate, wr as timeoutMs, yn as parseTemplate, yr as isClaudeNode } from "./src-BeR6f70A.mjs";
2
- import { t as runOnce } from "./run-CQIfq918.mjs";
1
+ import { $t as countedTokens, Cn as MCP_FILE, Dn as endOf, Ft as reportSchemaFor, Gn as parseTemplate, Gr as isToolNode, Ht as heldTimeout, Kn as renderTemplate, Lr as markValue, Mr as beforeOf, Nr as programsRunBy, Nt as planNodeSession, On as outcomeEnd, Qr as integrationFor, Tn as serversIn, Wn as evaluate, Wr as isClaudeNode, Xr as INTEGRATIONS, Yr as timeoutMs, Zr as TOOL_PREFIX, an as stoppedEnd, en as runScope, gn as sessionDriver, ii as AGENT_DRIVERS, in as failedWith, jn as endFor, kn as END_DETAIL_LIMIT, kt as planReason, ln as lowerChild, mn as envReadsOf, nt as sessionRefusals, on as declaredProblem, rn as endedWith, sn as slotClock, tn as calledWorkflows, un as startAt, vn as declaredOutputOf, wn as mcpNotText } from "./src-tsuZRu0H.mjs";
2
+ import { t as runOnce } from "./run-D8divGCs.mjs";
3
3
  import path from "node:path";
4
4
  import { accessSync, constants, lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { spawn } from "node:child_process";
@@ -538,43 +538,36 @@ const afterLateTurn = (first, later) => {
538
538
  errors: first.errors
539
539
  };
540
540
  };
541
+ /** The least of the limits that are set; `undefined` with none. */
542
+ const least = (...limits) => {
543
+ const known = limits.filter((limit) => limit !== void 0);
544
+ return known.length === 0 ? void 0 : Math.min(...known);
545
+ };
546
+ /** A cap less what this activation used, beside what a quota says is left. */
547
+ const leftOf = (caps, used, quotaLeft) => least(...caps.map((cap) => cap === void 0 ? void 0 : cap - used), quotaLeft);
541
548
  /**
542
- * What another turn of a session may use. `spent` and `turns` are this
543
- * activation's, which neither the node's cap nor the run's budget has counted.
544
- * The quota has: the ledger took it as the session finished, so what it says is
545
- * left is left. `null` when nothing is, and the turn does not start.
549
+ * What another turn of a session may use. `spent`, `tokens` (counted, RFC-015)
550
+ * and `turns` are this activation's, which neither the node's cap nor the run's
551
+ * budget has counted. The quota has: the ledger took it as the session
552
+ * finished, so what it says is left is left. The run's tokens left have too:
553
+ * only the node's own `budgetTokens` takes `tokens` off. `null` when nothing
554
+ * is, and the turn does not start.
546
555
  */
547
- const lateLimits = (spec, { spent, turns }, runLeft, quotaLeft) => {
548
- const budgets = [...[spec.budgetUsd, runLeft].flatMap((limit) => limit === void 0 ? [] : [limit - spent]), ...quotaLeft === void 0 ? [] : [quotaLeft]];
549
- const budgetUsd = budgets.length === 0 ? void 0 : Math.min(...budgets);
550
- if (budgetUsd !== void 0 && budgetUsd <= 0) return null;
551
- const maxTurns = spec.maxTurns === void 0 ? void 0 : spec.maxTurns - turns;
552
- if (maxTurns !== void 0 && maxTurns <= 0) return null;
556
+ const lateLimits = (spec, used, runLeft, quotaLeft, tokensLeft = {}) => {
557
+ const budgetUsd = leftOf([spec.budgetUsd, runLeft], used.spent, quotaLeft);
558
+ const budgetTokens = leftOf([spec.budgetTokens], used.tokens ?? 0, least(tokensLeft.run, tokensLeft.quota));
559
+ const maxTurns = spec.maxTurns === void 0 ? void 0 : spec.maxTurns - used.turns;
560
+ if ([
561
+ budgetUsd,
562
+ budgetTokens,
563
+ maxTurns
564
+ ].some((left) => (left ?? 1) <= 0)) return null;
553
565
  return {
554
566
  ...budgetUsd === void 0 ? {} : { budgetUsd },
567
+ ...budgetTokens === void 0 ? {} : { budgetTokens },
555
568
  ...maxTurns === void 0 ? {} : { maxTurns }
556
569
  };
557
570
  };
558
- /** What a session may spend as it starts: the least of the budget and any quota. */
559
- const budgetNow = (context) => {
560
- if (context.budgetLeftNow === void 0) return context.budgetLeftUsd;
561
- const known = [context.budgetLeftNow(), context.quota?.().leftUsd].flatMap((left) => left === void 0 ? [] : [left]);
562
- return known.length === 0 ? void 0 : Math.min(...known);
563
- };
564
- /**
565
- * What a node's first session may spend, set aside from the run's budget in the
566
- * same step that reads it (ADR-077), so a node starting beside it is never
567
- * offered the same part: the least of what is left and the node's own
568
- * `budgetUsd`. With nothing left, it waits for a worker running beside it to
569
- * finish, as long as one may.
570
- */
571
- const budgetToStart = async (context) => {
572
- const left = budgetNow(context);
573
- if (left !== void 0 && left <= 0) return await context.budgetFreed?.(context.signal) === true ? budgetToStart(context) : left;
574
- const own = isClaudeNode(context.node) ? context.node.budgetUsd ?? context.workflow.defaults?.budgetUsd : void 0;
575
- context.reserveBudget?.(own === void 0 || left === void 0 ? left : Math.min(own, left));
576
- return left;
577
- };
578
571
  /**
579
572
  * Whether a limit across runs, rather than the node's work, ended a node: a
580
573
  * session whose cap was a quota's and that ran into it, or one its plan refused
@@ -587,7 +580,7 @@ const limitWatch = (context) => {
587
580
  why = reason;
588
581
  },
589
582
  session: (asked) => {
590
- const left = context.quota?.().leftUsd;
583
+ const { leftUsd: left, leftTokens } = context.quota?.() ?? {};
591
584
  let refused;
592
585
  return {
593
586
  saw: (event) => {
@@ -595,7 +588,7 @@ const limitWatch = (context) => {
595
588
  },
596
589
  ended: (outcome) => {
597
590
  if (refused !== void 0 && (outcome?.report ?? null) === null) why = refused;
598
- const capped = left !== void 0 && asked.budgetUsd !== void 0 && asked.budgetUsd >= left;
591
+ const capped = left !== void 0 && asked.budgetUsd !== void 0 && asked.budgetUsd >= left || leftTokens !== void 0 && asked.budgetTokens !== void 0 && asked.budgetTokens >= leftTokens;
599
592
  const spent = outcome !== null && outcomeEnd(outcome).kind === "budget_limit";
600
593
  if (capped && spent) why = context.quota?.().blocked ?? "a session reached what was left of a quota";
601
594
  }
@@ -657,6 +650,61 @@ const sessionDriverOf = (context, plan, options) => {
657
650
  const unfit = plan.pinned === void 0 ? void 0 : refusalOn(context, driver, options.drivers ?? AGENT_DRIVERS);
658
651
  return unfit === void 0 ? driver : notStarted("invalid_node", unfit);
659
652
  };
653
+ // ../nodes/src/start-budgets.ts
654
+ /** What a session may spend as it starts: the least of the budget and any quota. */
655
+ const budgetNow = (context) => context.budgetLeftNow === void 0 ? context.budgetLeftUsd : least(context.budgetLeftNow(), context.quota?.().leftUsd);
656
+ /** The counted tokens a session may use as it starts: the run's and a quota's. */
657
+ const tokensNow = (context) => least(context.tokensLeftNow?.(), context.quota?.().leftTokens);
658
+ /**
659
+ * What is `left` now, with `own` (the node's cap) set aside through `reserve`
660
+ * in the same step; with nothing left, it waits for a worker's reserve to
661
+ * free.
662
+ */
663
+ const startWith = async (context, left, own, reserve) => {
664
+ const now = left();
665
+ if (now !== void 0 && now <= 0) {
666
+ context.reserveBudget?.(0);
667
+ context.reserveTokens?.(0);
668
+ return await context.budgetFreed?.(context.signal) === true ? startWith(context, left, own, reserve) : now;
669
+ }
670
+ reserve?.(own === void 0 || now === void 0 ? now : Math.min(own, now));
671
+ return now;
672
+ };
673
+ /**
674
+ * What a node's first session may spend, set aside from the run's budget in the
675
+ * same step that reads it (ADR-077), so a node starting beside it is never
676
+ * offered the same part: the least of what is left and the node's own
677
+ * `budgetUsd`. With nothing left, it waits for a worker running beside it to
678
+ * finish, as long as one may.
679
+ */
680
+ const budgetToStart = (context) => startWith(context, () => budgetNow(context), isClaudeNode(context.node) ? context.node.budgetUsd ?? context.workflow.defaults?.budgetUsd : void 0, context.reserveBudget);
681
+ /** `budgetToStart` in counted tokens: the run's `budgetTokens` and a quota's. */
682
+ const tokensToStart = (context) => startWith(context, () => tokensNow(context), isClaudeNode(context.node) ? context.node.budgetTokens ?? context.workflow.defaults?.budgetTokens : void 0, context.reserveTokens);
683
+ /**
684
+ * What a node's first session may spend, in dollars and in counted tokens, set
685
+ * into `planOptions`; the node's result instead when nothing is left of
686
+ * either.
687
+ */
688
+ const startBudgets = async (context, planOptions) => {
689
+ const usd = await budgetToStart(context);
690
+ if (usd !== void 0 && usd <= 0) return notStarted("budget_limit", "nothing is left of the run's budget");
691
+ if (usd !== void 0) planOptions.budgetLeftUsd = usd;
692
+ const waited = { done: false };
693
+ const tokens = await tokensToStart({
694
+ ...context,
695
+ budgetFreed: async (signal) => {
696
+ waited.done = true;
697
+ return await context.budgetFreed?.(signal) ?? false;
698
+ }
699
+ });
700
+ if (tokens !== void 0 && tokens <= 0) return notStarted("budget_limit", "nothing is left of the run's tokens");
701
+ if (waited.done) {
702
+ context.reserveTokens?.(0);
703
+ return startBudgets(context, planOptions);
704
+ }
705
+ if (tokens !== void 0) planOptions.budgetLeftTokens = tokens;
706
+ return null;
707
+ };
660
708
  // ../nodes/src/tool-output.ts
661
709
  /**
662
710
  * @file A tool node's structured result (`output: { format: json }`): the file
@@ -1742,9 +1790,8 @@ const claudeExecutor = (options) => async (context) => {
1742
1790
  }
1743
1791
  const blocked = context.quota?.().blocked;
1744
1792
  if (blocked !== void 0) return notStarted("plan_limit", blocked, blocked);
1745
- const budget = await budgetToStart(context);
1746
- if (budget !== void 0 && budget <= 0) return notStarted("budget_limit", "nothing is left of the run's budget");
1747
- if (budget !== void 0) planOptions.budgetLeftUsd = budget;
1793
+ const refused = await startBudgets(context, planOptions);
1794
+ if (refused !== null) return refused;
1748
1795
  const plan = planNodeSession(context.workflow, context.id, planOptions);
1749
1796
  if (!plan.ok) return notStarted("invalid_node", plan.error);
1750
1797
  const servers = namedMcpEntries(context.projectRoot, plan.mcp);
@@ -1775,6 +1822,7 @@ const claudeExecutor = (options) => async (context) => {
1775
1822
  const run = async (asked) => {
1776
1823
  const watching = watch.session(asked);
1777
1824
  context.reserveBudget?.(asked.budgetUsd);
1825
+ context.reserveTokens?.(asked.budgetTokens);
1778
1826
  const session = runtime.start(asked, ports);
1779
1827
  const stop = () => {
1780
1828
  session.interrupt();
@@ -1793,6 +1841,7 @@ const claudeExecutor = (options) => async (context) => {
1793
1841
  }
1794
1842
  } finally {
1795
1843
  context.reserveBudget?.(0);
1844
+ context.reserveTokens?.(0);
1796
1845
  disconnect?.();
1797
1846
  context.signal.removeEventListener("abort", stop);
1798
1847
  }
@@ -1808,8 +1857,12 @@ const claudeExecutor = (options) => async (context) => {
1808
1857
  }
1809
1858
  return lateLimits(spec, {
1810
1859
  spent: context.ownSpend?.(sessionId, usage.costUsd) ?? usage.costUsd,
1811
- turns: usage.turns
1812
- }, context.budgetLeftNow?.(), quota?.leftUsd);
1860
+ turns: usage.turns,
1861
+ tokens: countedTokens(usage)
1862
+ }, context.budgetLeftNow?.(), quota?.leftUsd, {
1863
+ run: context.tokensLeftNow?.(),
1864
+ quota: quota?.leftTokens
1865
+ });
1813
1866
  };
1814
1867
  const resumeWith = (outcome, prompt, limits) => run({
1815
1868
  ...spec,
@@ -1,4 +1,4 @@
1
- import { En as MASK, It as lowerChild, Lt as startAt, Nr as AGENT_DRIVERS, On as maskSecrets, an as endFor, cn as FEEDBACK_TARGETS, ct as parseReport, in as END_KINDS, nn as outcomeEnd, on as FEEDBACK_KINDS, sn as FEEDBACK_SEVERITIES } from "./src-BeR6f70A.mjs";
1
+ import { $n as maskSecrets, $t as countedTokens, An as END_KINDS, Mn as FEEDBACK_KINDS, Nn as FEEDBACK_SEVERITIES, On as outcomeEnd, Pn as FEEDBACK_TARGETS, Pt as parseReport, Zn as MASK, ii as AGENT_DRIVERS, jn as endFor, ln as lowerChild, un as startAt } from "./src-tsuZRu0H.mjs";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
4
  import { existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
@@ -448,7 +448,8 @@ const classifyProviderError = (text, status) => {
448
448
  * it can see (a turn cap, an exit code, a provider's error), and the report
449
449
  * the session handed in decides the rest. The runner then writes the end in
450
450
  * the node's numbers, its detail scrubbed with the feedback redactor. A
451
- * session the driver could not run ends as `driverFailure` says.
451
+ * session the driver could not run ends as `driverFailure` says, and one that
452
+ * reached its token cap as `tokenCapOutcome` says.
452
453
  */
453
454
  /** The end a report gives: done, a reported failure, or none handed in. */
454
455
  const reportEnd = (report) => {
@@ -479,6 +480,36 @@ const settleEnd = (outcome, facts, scrub) => {
479
480
  };
480
481
  };
481
482
  /**
483
+ * What a session's end is written with: the node's limits and program, and its
484
+ * folder and the home folder taken out of the detail.
485
+ */
486
+ const scopeOf = (spec, bin, env) => ({
487
+ facts: {
488
+ ...spec.maxTurns === void 0 ? {} : { maxTurns: spec.maxTurns },
489
+ ...spec.budgetUsd === void 0 ? {} : { budgetUsd: spec.budgetUsd },
490
+ ...spec.budgetTokens === void 0 ? {} : { budgetTokens: spec.budgetTokens },
491
+ program: path.basename(bin)
492
+ },
493
+ scrub: {
494
+ roots: [spec.cwd],
495
+ home: env["HOME"] ?? homedir()
496
+ }
497
+ });
498
+ /**
499
+ * The outcome of a session stopped once its counted tokens (RFC-015) reach
500
+ * `budgetTokens`: `budget_limit`, with what it used so far; `undefined` under
501
+ * the cap, or with none.
502
+ */
503
+ const tokenCapOutcome = ({ budgetTokens }, usage, sessionId) => budgetTokens === void 0 || usage === void 0 || countedTokens(usage) < budgetTokens ? void 0 : {
504
+ subtype: "error_max_budget_tokens",
505
+ end: endFor("budget_limit"),
506
+ sessionId,
507
+ report: null,
508
+ text: "",
509
+ usage,
510
+ errors: [`Reached the token budget (${String(budgetTokens)})`]
511
+ };
512
+ /**
482
513
  * The kind of a failure to run the driver: its program missing, else what a
483
514
  * provider's words in it say, else a crash.
484
515
  */
@@ -534,6 +565,15 @@ const settled = (outcome, input) => settleEnd(input.reported === void 0 ? outcom
534
565
  ...outcome,
535
566
  report: input.reported()
536
567
  }, input.scope.facts, input.scope.scrub);
568
+ /** The session's end at its token cap, its earlier legs' usage included. */
569
+ const tokenCapEnd = (input, used, sessionId) => {
570
+ const usage = used === void 0 || input.carried === void 0 ? used : input.adapter.usage.combine(input.carried, used);
571
+ const capped = tokenCapOutcome(input.scope.facts, usage, sessionId);
572
+ return capped === void 0 ? void 0 : {
573
+ type: "session.finished",
574
+ outcome: settled(capped, input)
575
+ };
576
+ };
537
577
  /** The kinds of a session that ended well, so a queued message continues it. */
538
578
  const wentWell = (kind) => kind === "done" || kind === "reported_failure" || kind === "no_report";
539
579
  /**
@@ -639,16 +679,26 @@ const runLeg = async function* (input) {
639
679
  for (const line of input.pending.splice(0)) write(line);
640
680
  if (input.adapter.command.input === void 0) child.stdin.end();
641
681
  let outcome;
682
+ let sessionId = "";
642
683
  const { decoder } = input;
643
684
  const decode = decoder?.decode ?? input.adapter.decode;
644
685
  try {
645
686
  for await (const line of createInterface({ input: child.stdout })) {
646
- const { events, ended } = legStep(decode(line), input);
687
+ const decoded = decode(line);
688
+ const { events, ended } = legStep(decoded, input);
689
+ for (const event of decoded.events) if (event.type === "session.started") sessionId = event.sessionId;
647
690
  yield* events;
648
691
  if (ended !== void 0) {
649
692
  child.stdin.end();
650
693
  outcome = ended;
651
694
  }
695
+ const capped = outcome === void 0 ? tokenCapEnd(input, decoded.usage, sessionId) : void 0;
696
+ if (capped !== void 0) {
697
+ stopped = true;
698
+ child.kill("SIGINT");
699
+ yield capped;
700
+ return { kind: "finished" };
701
+ }
652
702
  }
653
703
  const end = await Promise.race([exited, spawnError.then((error) => error)]);
654
704
  if (outcome !== void 0) return outcome;
@@ -1034,33 +1084,6 @@ const orkTools = (context) => ORK_TOOLS.flatMap((tool) => tool.bind(context) ??
1034
1084
  /** Their Claude Code names, for the session's allowed tools. */
1035
1085
  const preapprovedTools = (tools) => tools.filter((t) => t.preapproved).map((t) => toolId(t.name));
1036
1086
  // ../runtime/src/session-runner.ts
1037
- /**
1038
- * @file The session runner: one agent CLI session, driven through an adapter's
1039
- * strategies (`adapter.ts`), written once for every driver. It serves Ork's
1040
- * tools on a local MCP endpoint, wires the session to it, and runs the tool
1041
- * as one or more processes (legs). A paused node's tool call is held, and
1042
- * past the hold window deferred; the next leg resumes the session once the
1043
- * node continues (ADR-041). A tool that takes no message mid-session gets one
1044
- * at the next leg, which resumes the session with it. A driver that cannot
1045
- * hold the final answer to the report schema gets Ork's `report` tool, and
1046
- * the session ends with the report it accepted. Interrupting stops the
1047
- * running process.
1048
- */
1049
- /**
1050
- * What a session's end is written with: the node's limits and program, and its
1051
- * folder and the home folder taken out of the detail.
1052
- */
1053
- const scopeOf = (spec, bin, env) => ({
1054
- facts: {
1055
- ...spec.maxTurns === void 0 ? {} : { maxTurns: spec.maxTurns },
1056
- ...spec.budgetUsd === void 0 ? {} : { budgetUsd: spec.budgetUsd },
1057
- program: path.basename(bin)
1058
- },
1059
- scrub: {
1060
- roots: [spec.cwd],
1061
- home: env["HOME"] ?? homedir()
1062
- }
1063
- });
1064
1087
  /** A spec that resumes `sessionId`; a fork's later legs continue the fork. */
1065
1088
  const resumed = (spec, sessionId) => ({
1066
1089
  ...spec,
@@ -1391,9 +1414,23 @@ const REPORT_TOOL = "StructuredOutput";
1391
1414
  const claudeDecoder = () => {
1392
1415
  const calls = /* @__PURE__ */ new Map();
1393
1416
  let accepted;
1417
+ /** Each model response's usage, by message id: its blocks repeat it. */
1418
+ const responses = /* @__PURE__ */ new Map();
1419
+ /** The leg's usage so far, when `message` is a model response. */
1420
+ const usageSoFar = (message) => {
1421
+ const inner = get$1(message, "message");
1422
+ const id = get$1(inner, "id");
1423
+ if (get$1(message, "type") !== "assistant" || typeof id !== "string") return;
1424
+ responses.set(id, {
1425
+ ...usageOf({ usage: get$1(inner, "usage") }),
1426
+ turns: 1
1427
+ });
1428
+ return [...responses.values()].reduce((a, b) => addUsage(a, b));
1429
+ };
1394
1430
  return {
1395
1431
  decode: (line) => {
1396
1432
  const message = parseLine$2(line);
1433
+ const usage = usageSoFar(message);
1397
1434
  for (const block of blocks(message)) {
1398
1435
  if (block["type"] === "tool_use" && block["name"] === REPORT_TOOL) calls.set(str$2(block["id"]), block["input"]);
1399
1436
  const id = str$2(block["tool_use_id"]);
@@ -1408,7 +1445,8 @@ const claudeDecoder = () => {
1408
1445
  } : message;
1409
1446
  return {
1410
1447
  events: normalize(kept),
1411
- deferred: isDeferredResult(message)
1448
+ deferred: isDeferredResult(message),
1449
+ ...usage === void 0 ? {} : { usage }
1412
1450
  };
1413
1451
  },
1414
1452
  exit: () => []
@@ -1941,6 +1979,26 @@ const commandCodeDecoder = () => {
1941
1979
  let sessionId = "";
1942
1980
  let turns = 0;
1943
1981
  let held;
1982
+ /** The leg's tokens so far, from each `model_request_end` frame. */
1983
+ let used;
1984
+ /** A model request's usage, added to the leg's so far. */
1985
+ const requestEnded = (usage) => {
1986
+ const said = isObject$1(usage) ? usage : {};
1987
+ const own = {
1988
+ costUsd: 0,
1989
+ inputTokens: num$1(said["inputTokens"]),
1990
+ outputTokens: num$1(said["outputTokens"]),
1991
+ cacheReadTokens: num$1(said["cacheReadTokens"]),
1992
+ cacheWriteTokens: num$1(said["cacheWriteTokens"]),
1993
+ turns,
1994
+ durationMs: 0
1995
+ };
1996
+ used = used === void 0 ? own : {
1997
+ ...addUsage(used, own),
1998
+ turns
1999
+ };
2000
+ return used;
2001
+ };
1944
2002
  const frame = (event) => {
1945
2003
  const type = event["type"];
1946
2004
  const id = str$1(event["toolCallId"]);
@@ -2017,7 +2075,11 @@ const commandCodeDecoder = () => {
2017
2075
  return { events: [] };
2018
2076
  }
2019
2077
  const event = message["event"];
2020
- return { events: message["type"] === "event" && isObject$1(event) ? frame(event) : [] };
2078
+ if (message["type"] !== "event" || !isObject$1(event)) return { events: [] };
2079
+ return {
2080
+ events: frame(event),
2081
+ ...event["type"] === "model_request_end" ? { usage: requestEnded(event["usage"]) } : {}
2082
+ };
2021
2083
  },
2022
2084
  exit: (code) => {
2023
2085
  if (held !== void 0) return [result(held, code)];
@@ -2584,7 +2646,10 @@ const openCodeDecoder = (spec) => {
2584
2646
  switch (message["type"]) {
2585
2647
  case "step_finish":
2586
2648
  usage = sumUsage(usage, stepUsage(message["part"]));
2587
- break;
2649
+ return {
2650
+ events,
2651
+ usage
2652
+ };
2588
2653
  case "error":
2589
2654
  errors.push(errorText(message["error"]));
2590
2655
  failed ??= errorLineEnd(message["error"]);