@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.
- package/dist/__build_info__-Bsb1kjgF.mjs +340 -0
- package/dist/{store-CM3cERGl.mjs → attach-BHO3X3cf.mjs} +36 -91
- package/dist/{attach-E6NNyA3T.mjs → attach-CPespxlQ.mjs} +6 -5
- package/dist/bin.mjs +148 -1444
- package/dist/brief-za0ozN_E.mjs +58 -0
- package/dist/{builtin-files-n6o6oWpB.mjs → builtin-files-2bHf76x-.mjs} +1 -1
- package/dist/{chips-CyeB_rhR.mjs → chips-BqcT58qr.mjs} +1 -1
- package/dist/{completion-SNNNxnv9.mjs → completion-CQ2UYKuJ.mjs} +52 -11
- package/dist/context-BNqDw9z1.mjs +106 -0
- package/dist/{find-CjsNYAAf.mjs → find-BYLlszqj.mjs} +1 -1
- package/dist/follow-ups-B4GJ3Vzw.mjs +461 -0
- package/dist/{format-C60TFpRe.mjs → format-C8MsFj8_.mjs} +30 -16
- package/dist/{help-groups-CyfPWlST.mjs → help-groups-CQ98mLjF.mjs} +20 -6
- package/dist/{in-a-run-BYjTXaST.mjs → in-a-run-Bp0k0J09.mjs} +1 -1
- package/dist/{__build_info__-Da73_nBs.mjs → install-DWtXTq4y.mjs} +3 -227
- package/dist/{limits-C5QKZNLT.mjs → limits-B1qzS4cX.mjs} +1 -1
- package/dist/{mcp-bcctR-wc.mjs → mcp-BOrVkyhN.mjs} +6 -6
- package/dist/{once-DPQu5Q3H.mjs → once-C1fL3GVo.mjs} +4 -4
- package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
- package/dist/{output-CnRzrbUM.mjs → output-B1kwo0YU.mjs} +17 -1
- package/dist/{pause-BPvlDIvz.mjs → pause-DOMK-R8V.mjs} +4 -4
- package/dist/post-BzA6Qobq.mjs +44 -0
- package/dist/{relay-C8W0t_zc.mjs → relay-OWuu4fib.mjs} +2 -2
- package/dist/run--4-1xAF2.mjs +1451 -0
- package/dist/{run-CQIfq918.mjs → run-CiPsW9Kf.mjs} +1 -1
- package/dist/send-CnNNW-ot.mjs +105 -0
- package/dist/{src-CqRqsb2t.mjs → src-B9zWIVqo.mjs} +226 -44
- package/dist/{src-CGJGL1sZ.mjs → src-DQKz2eOH.mjs} +326 -81
- package/dist/{src-D93tHZ5B.mjs → src-DcrhgipG.mjs} +107 -47
- package/dist/{src-BeR6f70A.mjs → src-Dr_fTSu-.mjs} +5858 -3746
- package/dist/store-oKJ-cCf3.mjs +16 -0
- package/dist/ui/assets/app-Bdc6OBeZ.css +1 -0
- package/dist/ui/assets/app-Drp8Dm9y.js +111 -0
- package/dist/ui/assets/{index-CWwm8HTG.js → index-BtwkftKe.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-D2Ydofq0.mjs → ui-CzKlsCnm.mjs} +4 -4
- package/dist/{usage-6WIowblz.mjs → usage-DHGNk2JL.mjs} +13 -141
- package/dist/{context-DHudLA5e.mjs → validate-Bs_gg22g.mjs} +17 -107
- package/dist/{verdict-CIy8tAyX.mjs → verdict-CMvPCpWC.mjs} +2 -2
- package/dist/{watch-uOU_WeBI.mjs → watch-CCCfw3n7.mjs} +24 -8
- package/dist/{workflows-7tQdMnrA.mjs → workflows-CW28ZU-z.mjs} +3 -3
- package/dist/{worktrees-ZCII298F.mjs → worktrees-CyE8aF6j.mjs} +3 -5
- package/package.json +2 -2
- package/dist/clean-Bh4KZgzc.mjs +0 -91
- package/dist/decide-DuIHACvH.mjs +0 -50
- package/dist/list-BUb5Uc3H.mjs +0 -117
- package/dist/remove-BkFWGVYo.mjs +0 -201
- package/dist/report-CEN8VoUb.mjs +0 -140
- package/dist/send-CaZlGG5a.mjs +0 -163
- package/dist/ui/assets/app-DYRC0V4p.js +0 -111
- package/dist/ui/assets/app-QoHVi-mb.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2766
|
+
return {
|
|
2767
|
+
events,
|
|
2768
|
+
usage
|
|
2769
|
+
};
|
|
2588
2770
|
case "error":
|
|
2589
2771
|
errors.push(errorText(message["error"]));
|
|
2590
2772
|
failed ??= errorLineEnd(message["error"]);
|