@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.
- package/README.md +10 -4
- package/dist/{__build_info__-Bsb1kjgF.mjs → __build_info__-DcRdK0Du.mjs} +25 -6
- package/dist/apply-Mb0fNpCX.mjs +17 -0
- package/dist/{attach-CPespxlQ.mjs → attach-7lB5_jpr.mjs} +5 -5
- package/dist/{attach-BHO3X3cf.mjs → attach-B-FeE1dJ.mjs} +46 -8
- package/dist/bin.mjs +238 -369
- package/dist/{brief-za0ozN_E.mjs → brief-BvMtIFio.mjs} +4 -4
- package/dist/{builtin-files-2bHf76x-.mjs → builtin-files-BRSOObAv.mjs} +1 -1
- package/dist/command-tree-BkSYWmYJ.mjs +63 -0
- package/dist/{completion-CQ2UYKuJ.mjs → completion-Cgb2qPPh.mjs} +45 -6
- package/dist/{context-BNqDw9z1.mjs → context-BN9A2Nnu.mjs} +3 -3
- package/dist/{find-BYLlszqj.mjs → find-mLYsR0uC.mjs} +2 -2
- package/dist/flags-CdgIH1L6.mjs +230 -0
- package/dist/{follow-ups-B4GJ3Vzw.mjs → follow-ups-DxCsOby1.mjs} +59 -24
- package/dist/{format-C8MsFj8_.mjs → format-hiDvtNGM.mjs} +17 -4
- package/dist/{help-groups-CQ98mLjF.mjs → help-groups-hB6kY34d.mjs} +1 -63
- package/dist/{in-a-run-Bp0k0J09.mjs → in-a-run-a447hxq8.mjs} +1 -1
- package/dist/{install-DWtXTq4y.mjs → install-23g_1ngs.mjs} +1 -1
- package/dist/{limits-B1qzS4cX.mjs → limits-C_Xz_t3C.mjs} +1 -1
- package/dist/{mcp-BOrVkyhN.mjs → mcp-CWng9I9n.mjs} +6 -6
- package/dist/{once-C1fL3GVo.mjs → once-lvwcWfVf.mjs} +3 -3
- package/dist/{pause-DOMK-R8V.mjs → pause-Cy11KrXK.mjs} +3 -3
- package/dist/{post-BzA6Qobq.mjs → post-C9soCx7i.mjs} +5 -3
- package/dist/print-BaWiWf4v.mjs +19 -0
- package/dist/project-records-BRiyQzUo.mjs +230 -0
- package/dist/{relay-OWuu4fib.mjs → relay-C0M1TWMB.mjs} +1 -1
- package/dist/{run--4-1xAF2.mjs → run-B-j3Qxqc.mjs} +26 -13
- package/dist/{run-CiPsW9Kf.mjs → run-D_u9V_Ve.mjs} +1 -1
- package/dist/{send-CnNNW-ot.mjs → send-qBxj0JbJ.mjs} +2 -2
- package/dist/{src-B9zWIVqo.mjs → src-CXBmaGwS.mjs} +93 -72
- package/dist/{src-DcrhgipG.mjs → src-DHDQ0itC.mjs} +91 -42
- package/dist/{src-Dr_fTSu-.mjs → src-DN-RPsil.mjs} +781 -502
- package/dist/{src-DQKz2eOH.mjs → src-nXLIwDrt.mjs} +9 -12
- package/dist/{store-oKJ-cCf3.mjs → store-Dyr1mP8K.mjs} +1 -1
- package/dist/ui/assets/app-BPSCxb_l.css +1 -0
- package/dist/ui/assets/{app-Drp8Dm9y.js → app-CnNRowFn.js} +40 -40
- package/dist/ui/assets/{index-BtwkftKe.js → index-CVzwYlz3.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-CzKlsCnm.mjs → ui-DLrSt5ny.mjs} +3 -3
- package/dist/{usage-DHGNk2JL.mjs → usage-CxmpYYO4.mjs} +61 -107
- package/dist/{validate-Bs_gg22g.mjs → validate-BVrT43GO.mjs} +35 -8
- package/dist/{watch-CCCfw3n7.mjs → watch-mkLm4t9D.mjs} +5 -5
- package/dist/{workflows-CW28ZU-z.mjs → workflows-Bvl2Fquu.mjs} +2 -2
- package/dist/{worktrees-CyE8aF6j.mjs → worktrees-DOOCoS2w.mjs} +49 -20
- package/package.json +8 -4
- package/dist/ui/assets/app-Bdc6OBeZ.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
460
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
|
|
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(
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
|
657
|
-
usage:
|
|
658
|
-
last
|
|
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
|
-
|
|
662
|
-
|
|
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)
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
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 { $
|
|
2
|
-
import { t as runOnce } from "./run-
|
|
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
|
|
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),
|
|
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),
|
|
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, {
|