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

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 (51) hide show
  1. package/dist/__build_info__-Bsb1kjgF.mjs +340 -0
  2. package/dist/{store-CM3cERGl.mjs → attach-BHO3X3cf.mjs} +36 -91
  3. package/dist/{attach-E6NNyA3T.mjs → attach-CPespxlQ.mjs} +6 -5
  4. package/dist/bin.mjs +148 -1444
  5. package/dist/brief-za0ozN_E.mjs +58 -0
  6. package/dist/{builtin-files-n6o6oWpB.mjs → builtin-files-2bHf76x-.mjs} +1 -1
  7. package/dist/{chips-CyeB_rhR.mjs → chips-BqcT58qr.mjs} +1 -1
  8. package/dist/{completion-SNNNxnv9.mjs → completion-CQ2UYKuJ.mjs} +52 -11
  9. package/dist/context-BNqDw9z1.mjs +106 -0
  10. package/dist/{find-CjsNYAAf.mjs → find-BYLlszqj.mjs} +1 -1
  11. package/dist/follow-ups-B4GJ3Vzw.mjs +461 -0
  12. package/dist/{format-C60TFpRe.mjs → format-C8MsFj8_.mjs} +30 -16
  13. package/dist/{help-groups-CyfPWlST.mjs → help-groups-CQ98mLjF.mjs} +20 -6
  14. package/dist/{in-a-run-BYjTXaST.mjs → in-a-run-Bp0k0J09.mjs} +1 -1
  15. package/dist/{__build_info__-Da73_nBs.mjs → install-DWtXTq4y.mjs} +3 -227
  16. package/dist/{limits-C5QKZNLT.mjs → limits-B1qzS4cX.mjs} +1 -1
  17. package/dist/{mcp-bcctR-wc.mjs → mcp-BOrVkyhN.mjs} +6 -6
  18. package/dist/{once-DPQu5Q3H.mjs → once-C1fL3GVo.mjs} +4 -4
  19. package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
  20. package/dist/{output-CnRzrbUM.mjs → output-B1kwo0YU.mjs} +17 -1
  21. package/dist/{pause-BPvlDIvz.mjs → pause-DOMK-R8V.mjs} +4 -4
  22. package/dist/post-BzA6Qobq.mjs +44 -0
  23. package/dist/{relay-C8W0t_zc.mjs → relay-OWuu4fib.mjs} +2 -2
  24. package/dist/run--4-1xAF2.mjs +1451 -0
  25. package/dist/{run-CQIfq918.mjs → run-CiPsW9Kf.mjs} +1 -1
  26. package/dist/send-CnNNW-ot.mjs +105 -0
  27. package/dist/{src-CqRqsb2t.mjs → src-B9zWIVqo.mjs} +226 -44
  28. package/dist/{src-CGJGL1sZ.mjs → src-DQKz2eOH.mjs} +326 -81
  29. package/dist/{src-D93tHZ5B.mjs → src-DcrhgipG.mjs} +107 -47
  30. package/dist/{src-BeR6f70A.mjs → src-Dr_fTSu-.mjs} +5858 -3746
  31. package/dist/store-oKJ-cCf3.mjs +16 -0
  32. package/dist/ui/assets/app-Bdc6OBeZ.css +1 -0
  33. package/dist/ui/assets/app-Drp8Dm9y.js +111 -0
  34. package/dist/ui/assets/{index-CWwm8HTG.js → index-BtwkftKe.js} +2 -2
  35. package/dist/ui/index.html +1 -1
  36. package/dist/{ui-D2Ydofq0.mjs → ui-CzKlsCnm.mjs} +4 -4
  37. package/dist/{usage-6WIowblz.mjs → usage-DHGNk2JL.mjs} +13 -141
  38. package/dist/{context-DHudLA5e.mjs → validate-Bs_gg22g.mjs} +17 -107
  39. package/dist/{verdict-CIy8tAyX.mjs → verdict-CMvPCpWC.mjs} +2 -2
  40. package/dist/{watch-uOU_WeBI.mjs → watch-CCCfw3n7.mjs} +24 -8
  41. package/dist/{workflows-7tQdMnrA.mjs → workflows-CW28ZU-z.mjs} +3 -3
  42. package/dist/{worktrees-ZCII298F.mjs → worktrees-CyE8aF6j.mjs} +3 -5
  43. package/package.json +2 -2
  44. package/dist/clean-Bh4KZgzc.mjs +0 -91
  45. package/dist/decide-DuIHACvH.mjs +0 -50
  46. package/dist/list-BUb5Uc3H.mjs +0 -117
  47. package/dist/remove-BkFWGVYo.mjs +0 -201
  48. package/dist/report-CEN8VoUb.mjs +0 -140
  49. package/dist/send-CaZlGG5a.mjs +0 -163
  50. package/dist/ui/assets/app-DYRC0V4p.js +0 -111
  51. package/dist/ui/assets/app-QoHVi-mb.css +0 -1
@@ -1,4 +1,4 @@
1
- import { Rt as git, zt as gitOk } from "./src-BeR6f70A.mjs";
1
+ import { Jt as gitOk, qt as git } from "./src-Dr_fTSu-.mjs";
2
2
  import path from "node:path";
3
3
  import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { createHash, randomUUID } from "node:crypto";
@@ -0,0 +1,105 @@
1
+ import { U as openStore, at as scriptRefusal, it as TOO_LONG, ot as sendFromScript } from "./src-Dr_fTSu-.mjs";
2
+ import { l as writeErr } from "./output-B1kwo0YU.mjs";
3
+ import { u as inheritedScrubber } from "./src-DcrhgipG.mjs";
4
+ import { defineCommand } from "citty";
5
+ // src/commands/send.ts
6
+ /**
7
+ * @file `ork send` (RFC-005): a program in a tool node writes to a session on
8
+ * its run's bus. It prints nothing when it works, as what a program prints is
9
+ * its node's output, and says why on stderr, with exit 1, when it is refused:
10
+ * before reading a message it could not send, and for anything wrong with the
11
+ * command itself, which would otherwise go to stdout as usage.
12
+ */
13
+ const refuse = (message) => {
14
+ writeErr(message);
15
+ process.exitCode = 1;
16
+ };
17
+ /** Stdin, up to the limit: past it, the rest is not worth holding. */
18
+ const readBounded = async () => {
19
+ const chunks = [];
20
+ let size = 0;
21
+ for await (const chunk of process.stdin) {
22
+ const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
23
+ size += part.length;
24
+ if (size > 65536) return null;
25
+ chunks.push(part);
26
+ }
27
+ return Buffer.concat(chunks);
28
+ };
29
+ /** The message, or why it can't go. */
30
+ const bodyOf = async (given) => {
31
+ if (given !== void 0) return { body: given };
32
+ const read = await readBounded();
33
+ if (read === null) return { refused: TOO_LONG };
34
+ try {
35
+ return { body: new TextDecoder("utf-8", { fatal: true }).decode(read) };
36
+ } catch {
37
+ return { refused: "The message is not UTF-8 text." };
38
+ }
39
+ };
40
+ const deliver = async (store, to, given) => {
41
+ const sender = {
42
+ run: process.env["ORK_RUN"],
43
+ node: process.env["ORK_NODE"],
44
+ to
45
+ };
46
+ const refused = scriptRefusal(store, sender);
47
+ if (refused !== null) {
48
+ refuse(refused);
49
+ return;
50
+ }
51
+ const message = await bodyOf(given);
52
+ if ("refused" in message) {
53
+ refuse(message.refused);
54
+ return;
55
+ }
56
+ if (message.body.replaceAll(/[\p{Cc}\s]/gu, "") === "") {
57
+ refuse("The message is empty.");
58
+ return;
59
+ }
60
+ const sent = await sendFromScript(store, {
61
+ ...sender,
62
+ body: inheritedScrubber(process.env)(message.body)
63
+ });
64
+ if (!sent.ok) refuse(sent.error);
65
+ };
66
+ const send = defineCommand({
67
+ meta: {
68
+ name: "send",
69
+ description: "From a tool node's program: write to a session in its run"
70
+ },
71
+ args: {
72
+ to: {
73
+ type: "positional",
74
+ description: "The node to write to: the owner, or a channel",
75
+ required: false
76
+ },
77
+ message: {
78
+ type: "string",
79
+ alias: "m",
80
+ description: "The message"
81
+ },
82
+ stdin: {
83
+ type: "boolean",
84
+ description: "Read the message from stdin",
85
+ default: false
86
+ }
87
+ },
88
+ run: async ({ args }) => {
89
+ if (args.to === void 0 || args._.length !== 1) {
90
+ refuse("Give one node to write to: ork send <to> -m <body>.");
91
+ return;
92
+ }
93
+ if (args.message === void 0 === !args.stdin) {
94
+ refuse("Give the message with -m or --stdin, one of them.");
95
+ return;
96
+ }
97
+ const store = openStore();
98
+ try {
99
+ await deliver(store, args.to, args.message);
100
+ } finally {
101
+ store.close();
102
+ }
103
+ }
104
+ });
105
+ export { send };
@@ -1,4 +1,5 @@
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 { 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";
2
+ import { n as holdGuard, r as orphanGuard } from "./orphan-guard-BFKg8VSd.mjs";
2
3
  import path from "node:path";
3
4
  import { z } from "zod";
4
5
  import { existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
@@ -8,6 +9,13 @@ import { createInterface } from "node:readline";
8
9
  import { execFile, spawn } from "node:child_process";
9
10
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
10
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
+ ];
11
19
  // ../runtime/src/hold.ts
12
20
  /**
13
21
  * Waits at a tool call while `hold` is held: "go" once it is released within
@@ -448,7 +456,8 @@ const classifyProviderError = (text, status) => {
448
456
  * it can see (a turn cap, an exit code, a provider's error), and the report
449
457
  * the session handed in decides the rest. The runner then writes the end in
450
458
  * the node's numbers, its detail scrubbed with the feedback redactor. A
451
- * session the driver could not run ends as `driverFailure` says.
459
+ * session the driver could not run ends as `driverFailure` says, and one that
460
+ * reached its token cap as `tokenCapOutcome` says.
452
461
  */
453
462
  /** The end a report gives: done, a reported failure, or none handed in. */
454
463
  const reportEnd = (report) => {
@@ -479,6 +488,36 @@ const settleEnd = (outcome, facts, scrub) => {
479
488
  };
480
489
  };
481
490
  /**
491
+ * What a session's end is written with: the node's limits and program, and its
492
+ * folder and the home folder taken out of the detail.
493
+ */
494
+ const scopeOf = (spec, bin, env) => ({
495
+ facts: {
496
+ ...spec.maxTurns === void 0 ? {} : { maxTurns: spec.maxTurns },
497
+ ...spec.budgetUsd === void 0 ? {} : { budgetUsd: spec.budgetUsd },
498
+ ...spec.budgetTokens === void 0 ? {} : { budgetTokens: spec.budgetTokens },
499
+ program: path.basename(bin)
500
+ },
501
+ scrub: {
502
+ roots: [spec.cwd],
503
+ home: env["HOME"] ?? homedir()
504
+ }
505
+ });
506
+ /**
507
+ * The outcome of a session stopped once its counted tokens (RFC-015) reach
508
+ * `budgetTokens`: `budget_limit`, with what it used so far; `undefined` under
509
+ * the cap, or with none.
510
+ */
511
+ const tokenCapOutcome = ({ budgetTokens }, usage, sessionId) => budgetTokens === void 0 || usage === void 0 || countedTokens(usage) < budgetTokens ? void 0 : {
512
+ subtype: "error_max_budget_tokens",
513
+ end: endFor("budget_limit"),
514
+ sessionId,
515
+ report: null,
516
+ text: "",
517
+ usage,
518
+ errors: [`Reached the token budget (${String(budgetTokens)})`]
519
+ };
520
+ /**
482
521
  * The kind of a failure to run the driver: its program missing, else what a
483
522
  * provider's words in it say, else a crash.
484
523
  */
@@ -512,13 +551,62 @@ const driverFailure = (message, kind, scope) => ({
512
551
  errors: [message]
513
552
  }
514
553
  });
554
+ // ../runtime/src/leg-spawn.ts
555
+ /**
556
+ * @file Starting one process (leg) of a session: at the project's priority, and
557
+ * off Windows in its own process group beside the orphan guard, so it and
558
+ * what it started end with `ork`, whatever ends `ork`. The process's pid, and
559
+ * the SIGINT that stops it, stay the driver's own.
560
+ */
561
+ /** The fd of the orphan guard's pipe from `ork`. */
562
+ const WATCH_FD = 3;
563
+ /**
564
+ * Starts the orphan guard, then becomes the driver. The pipe is closed by a
565
+ * bare `exec`: macOS's `sh` keeps a copy of a descriptor an `exec` command
566
+ * closes, which the driver and its leftovers would then inherit.
567
+ */
568
+ const GUARDED = `${orphanGuard(WATCH_FD)}exec ${String(WATCH_FD)}<&-; exec "$0" "$@"`;
569
+ const piped = (child) => child.stdin !== null && child.stdout !== null && child.stderr !== null;
570
+ /**
571
+ * Spawns the leg's process. A program that isn't there is spawned bare, so it
572
+ * fails as spawn's ENOENT; the guard is released once the driver exits, so a
573
+ * session that ends normally leaves its leftovers alone.
574
+ */
575
+ const spawnLeg = (start) => {
576
+ const [bin, args] = startAt(start.priority, start.bin, start.args, start.env);
577
+ const guarded = process.platform !== "win32" && programFound(start.bin, start.env);
578
+ const child = spawn(guarded ? "/bin/sh" : bin, guarded ? [
579
+ "-c",
580
+ GUARDED,
581
+ bin,
582
+ ...args
583
+ ] : args, {
584
+ cwd: start.cwd,
585
+ detached: guarded,
586
+ env: { ...start.env },
587
+ stdio: [
588
+ "pipe",
589
+ "pipe",
590
+ "pipe",
591
+ guarded ? "pipe" : "ignore"
592
+ ]
593
+ });
594
+ if (!piped(child)) throw new Error(`${start.bin} was started without pipes`);
595
+ if (guarded) {
596
+ const release = holdGuard(child.stdio[WATCH_FD]);
597
+ child.once("exit", release);
598
+ }
599
+ return child;
600
+ };
515
601
  // ../runtime/src/session-leg.ts
516
602
  /**
517
603
  * @file One process (leg) of a session the runner drives: it writes the queued
518
604
  * stdin lines, yields the adapter's decoded events, and says how it ended. A
519
605
  * session is one leg, or several when a paused node's tool call was deferred
520
606
  * or a message waits for a tool that takes none mid-session; a resumed leg
521
- * carries what the legs before it spent, and its start is not news.
607
+ * carries what the legs before it spent, and its start is not news. Off
608
+ * Windows the process ends with `ork`, whatever ends `ork` (the orphan
609
+ * guard).
522
610
  */
523
611
  const REASON_LIMIT = 500;
524
612
  /** Marks a session's start with the driver that runs it. */
@@ -534,6 +622,15 @@ const settled = (outcome, input) => settleEnd(input.reported === void 0 ? outcom
534
622
  ...outcome,
535
623
  report: input.reported()
536
624
  }, input.scope.facts, input.scope.scrub);
625
+ /** The session's end at its token cap, its earlier legs' usage included. */
626
+ 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);
629
+ return capped === void 0 ? void 0 : {
630
+ type: "session.finished",
631
+ outcome: settled(capped, input)
632
+ };
633
+ };
537
634
  /** The kinds of a session that ended well, so a queued message continues it. */
538
635
  const wentWell = (kind) => kind === "done" || kind === "reported_failure" || kind === "no_report";
539
636
  /**
@@ -598,16 +695,7 @@ const failureReason = (name, stderr, code) => {
598
695
  };
599
696
  /** Runs one process of a session, yielding its events; returns how it ended. */
600
697
  const runLeg = async function* (input) {
601
- const [bin, args] = startAt(input.priority, input.bin, input.args, input.env);
602
- const child = spawn(bin, args, {
603
- cwd: input.cwd,
604
- env: { ...input.env },
605
- stdio: [
606
- "pipe",
607
- "pipe",
608
- "pipe"
609
- ]
610
- });
698
+ const child = spawnLeg(input);
611
699
  lowerChild(input.priority, child.pid);
612
700
  const exited = new Promise((resolve) => {
613
701
  child.on("close", (code, signal) => {
@@ -639,16 +727,26 @@ const runLeg = async function* (input) {
639
727
  for (const line of input.pending.splice(0)) write(line);
640
728
  if (input.adapter.command.input === void 0) child.stdin.end();
641
729
  let outcome;
730
+ let sessionId = "";
642
731
  const { decoder } = input;
643
732
  const decode = decoder?.decode ?? input.adapter.decode;
644
733
  try {
645
734
  for await (const line of createInterface({ input: child.stdout })) {
646
- const { events, ended } = legStep(decode(line), input);
735
+ const decoded = decode(line);
736
+ const { events, ended } = legStep(decoded, input);
737
+ for (const event of decoded.events) if (event.type === "session.started") sessionId = event.sessionId;
647
738
  yield* events;
648
739
  if (ended !== void 0) {
649
740
  child.stdin.end();
650
741
  outcome = ended;
651
742
  }
743
+ const capped = outcome === void 0 ? tokenCapEnd(input, decoded.usage, sessionId) : void 0;
744
+ if (capped !== void 0) {
745
+ stopped = true;
746
+ child.kill("SIGINT");
747
+ yield capped;
748
+ return { kind: "finished" };
749
+ }
652
750
  }
653
751
  const end = await Promise.race([exited, spawnError.then((error) => error)]);
654
752
  if (outcome !== void 0) return outcome;
@@ -834,6 +932,74 @@ const feedbackTool = defineTool({
834
932
  return Promise.resolve(text(sent.ok ? "Recorded. Thank you; carry on with your task." : `Not recorded: ${sent.error}.`));
835
933
  }
836
934
  });
935
+ // ../runtime/src/tools/follow-up.ts
936
+ /**
937
+ * @file `follow_up`: a session's way to leave work the change leaves for later
938
+ * (a change for a later run, a note for another project, a message a person
939
+ * must send, a task or a check for the maintainer). Ork keeps it where the
940
+ * project's `followUps` rule says, merges duplicates, caps it per session,
941
+ * and never sends or runs anything on its own.
942
+ */
943
+ const isFor = (value) => /^(?:agent|me|project:\S.*|person:\S.*)$/u.test(value);
944
+ const isDue = (value) => /^(?:now|after-merge|after-deploy|\d{4}-\d{2}-\d{2})$/u.test(value);
945
+ const followUpTool = defineTool({
946
+ name: "follow_up",
947
+ description: "Leave work this change leaves for later and that is not yours to do now: a change for a later run (for: agent), something for another registered project (for: project:<name>), a message a person must get (for: person:<name>, kind: message, with a draft), or a task or check for the maintainer (for: me, with steps). Ork keeps it, merges duplicates, and sends or runs nothing itself. Not for problems with Ork or the workflow (use feedback), nor for what you can finish now.",
948
+ shape: {
949
+ title: z.string().min(1).max(200).describe("One line."),
950
+ body: z.string().max(4e3).optional().describe("What and why, briefly."),
951
+ for: z.string().refine(isFor, "agent, me, project:<name> or person:<name>").describe("Who acts: agent, me, project:<name> or person:<name>."),
952
+ kind: z.enum(FOLLOW_UP_KINDS),
953
+ due: z.string().refine(isDue, "now, after-merge, after-deploy or YYYY-MM-DD").describe("now, after-merge, after-deploy, or a YYYY-MM-DD date."),
954
+ where: z.array(z.string()).max(20).optional().describe("Files it is about, relative to the project root."),
955
+ ref: z.string().min(1).optional().describe("The pull request or branch an after-merge follow-up waits on, when you know it."),
956
+ draft: z.strictObject({
957
+ to: z.string().min(1),
958
+ channel: z.string().optional().describe("github (to: an issue or PR URL), email, slack… A hint for the maintainer."),
959
+ text: z.string().min(1).max(4e3)
960
+ }).optional().describe("For a message: what the maintainer will send."),
961
+ steps: z.array(z.strictObject({
962
+ text: z.string().min(1),
963
+ command: z.string().optional().describe("Shown to the maintainer to copy; Ork never runs it.")
964
+ })).max(20).optional().describe("For a task or check: what to do, in order.")
965
+ },
966
+ available: ({ ports }) => ports.followUps !== void 0,
967
+ run: async ({ ports }, args) => {
968
+ const port = ports.followUps;
969
+ if (port === void 0) return text("No follow-ups here.");
970
+ if (!isFor(args.for) || !isDue(args.due)) return text("Not kept.");
971
+ const input = {
972
+ title: args.title,
973
+ for: args.for,
974
+ kind: args.kind,
975
+ due: args.due,
976
+ ...args.body === void 0 ? {} : { body: args.body },
977
+ ...args.where === void 0 ? {} : { where: args.where },
978
+ ...args.ref === void 0 ? {} : { ref: args.ref },
979
+ ...args.draft === void 0 ? {} : { draft: {
980
+ to: args.draft.to,
981
+ text: args.draft.text,
982
+ ...args.draft.channel === void 0 ? {} : { channel: args.draft.channel }
983
+ } },
984
+ ...args.steps === void 0 ? {} : { steps: args.steps.map((step) => ({
985
+ text: step.text,
986
+ ...step.command === void 0 ? {} : { command: step.command }
987
+ })) }
988
+ };
989
+ const result = await port.add(input);
990
+ if (!result.ok) return {
991
+ ...text(`Not kept: ${result.error}.`),
992
+ isError: true
993
+ };
994
+ const { followUp, merged, note } = result.added;
995
+ return text([
996
+ merged ? `Merged into follow-up ${followUp.id}, which was already kept.` : `Kept as follow-up ${followUp.id}.`,
997
+ ...note === void 0 ? [] : [note],
998
+ ...result.relayed === void 0 ? [] : [`Also sent to ${result.relayed.run}/${result.relayed.node}, a live run of that project.`],
999
+ "Carry on with your task."
1000
+ ].join(" "));
1001
+ }
1002
+ });
837
1003
  // ../runtime/src/tools/input.ts
838
1004
  /**
839
1005
  * @file `read_input`: what the workflow sent a node back with, whole. A loop's
@@ -1024,6 +1190,7 @@ const ORK_TOOLS = [
1024
1190
  claimTool,
1025
1191
  releaseTool,
1026
1192
  feedbackTool,
1193
+ followUpTool,
1027
1194
  {
1028
1195
  name: REPORT_TOOL$1,
1029
1196
  bind: (context) => context.report === void 0 || checksItsReport(context.driver) ? null : bound(context.report)
@@ -1034,33 +1201,6 @@ const orkTools = (context) => ORK_TOOLS.flatMap((tool) => tool.bind(context) ??
1034
1201
  /** Their Claude Code names, for the session's allowed tools. */
1035
1202
  const preapprovedTools = (tools) => tools.filter((t) => t.preapproved).map((t) => toolId(t.name));
1036
1203
  // ../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
1204
  /** A spec that resumes `sessionId`; a fork's later legs continue the fork. */
1065
1205
  const resumed = (spec, sessionId) => ({
1066
1206
  ...spec,
@@ -1391,9 +1531,23 @@ const REPORT_TOOL = "StructuredOutput";
1391
1531
  const claudeDecoder = () => {
1392
1532
  const calls = /* @__PURE__ */ new Map();
1393
1533
  let accepted;
1534
+ /** Each model response's usage, by message id: its blocks repeat it. */
1535
+ const responses = /* @__PURE__ */ new Map();
1536
+ /** The leg's usage so far, when `message` is a model response. */
1537
+ const usageSoFar = (message) => {
1538
+ const inner = get$1(message, "message");
1539
+ const id = get$1(inner, "id");
1540
+ if (get$1(message, "type") !== "assistant" || typeof id !== "string") return;
1541
+ responses.set(id, {
1542
+ ...usageOf({ usage: get$1(inner, "usage") }),
1543
+ turns: 1
1544
+ });
1545
+ return [...responses.values()].reduce((a, b) => addUsage(a, b));
1546
+ };
1394
1547
  return {
1395
1548
  decode: (line) => {
1396
1549
  const message = parseLine$2(line);
1550
+ const usage = usageSoFar(message);
1397
1551
  for (const block of blocks(message)) {
1398
1552
  if (block["type"] === "tool_use" && block["name"] === REPORT_TOOL) calls.set(str$2(block["id"]), block["input"]);
1399
1553
  const id = str$2(block["tool_use_id"]);
@@ -1408,7 +1562,8 @@ const claudeDecoder = () => {
1408
1562
  } : message;
1409
1563
  return {
1410
1564
  events: normalize(kept),
1411
- deferred: isDeferredResult(message)
1565
+ deferred: isDeferredResult(message),
1566
+ ...usage === void 0 ? {} : { usage }
1412
1567
  };
1413
1568
  },
1414
1569
  exit: () => []
@@ -1941,6 +2096,26 @@ const commandCodeDecoder = () => {
1941
2096
  let sessionId = "";
1942
2097
  let turns = 0;
1943
2098
  let held;
2099
+ /** The leg's tokens so far, from each `model_request_end` frame. */
2100
+ let used;
2101
+ /** A model request's usage, added to the leg's so far. */
2102
+ const requestEnded = (usage) => {
2103
+ const said = isObject$1(usage) ? usage : {};
2104
+ const own = {
2105
+ costUsd: 0,
2106
+ inputTokens: num$1(said["inputTokens"]),
2107
+ outputTokens: num$1(said["outputTokens"]),
2108
+ cacheReadTokens: num$1(said["cacheReadTokens"]),
2109
+ cacheWriteTokens: num$1(said["cacheWriteTokens"]),
2110
+ turns,
2111
+ durationMs: 0
2112
+ };
2113
+ used = used === void 0 ? own : {
2114
+ ...addUsage(used, own),
2115
+ turns
2116
+ };
2117
+ return used;
2118
+ };
1944
2119
  const frame = (event) => {
1945
2120
  const type = event["type"];
1946
2121
  const id = str$1(event["toolCallId"]);
@@ -2017,7 +2192,11 @@ const commandCodeDecoder = () => {
2017
2192
  return { events: [] };
2018
2193
  }
2019
2194
  const event = message["event"];
2020
- return { events: message["type"] === "event" && isObject$1(event) ? frame(event) : [] };
2195
+ if (message["type"] !== "event" || !isObject$1(event)) return { events: [] };
2196
+ return {
2197
+ events: frame(event),
2198
+ ...event["type"] === "model_request_end" ? { usage: requestEnded(event["usage"]) } : {}
2199
+ };
2021
2200
  },
2022
2201
  exit: (code) => {
2023
2202
  if (held !== void 0) return [result(held, code)];
@@ -2584,7 +2763,10 @@ const openCodeDecoder = (spec) => {
2584
2763
  switch (message["type"]) {
2585
2764
  case "step_finish":
2586
2765
  usage = sumUsage(usage, stepUsage(message["part"]));
2587
- break;
2766
+ return {
2767
+ events,
2768
+ usage
2769
+ };
2588
2770
  case "error":
2589
2771
  errors.push(errorText(message["error"]));
2590
2772
  failed ??= errorLineEnd(message["error"]);