@krapht/ork 1.0.0-alpha.10 → 1.0.0-alpha.12
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__-DFF18K8y.mjs → __build_info__-BJbE3Zvp.mjs} +4 -72
- package/dist/attach-BmV5ckb9.mjs +89 -0
- package/dist/bin.mjs +467 -864
- package/dist/{decide-3l9q8UtX.mjs → builtin-files-w0YULJ4c.mjs} +2 -50
- package/dist/chips-CyeB_rhR.mjs +43 -0
- package/dist/{clean-DVaEH9e7.mjs → clean-D8GI_2K-.mjs} +1 -1
- package/dist/{completion-Dv5kf598.mjs → completion-CzElB8nS.mjs} +24 -16
- package/dist/{context-CEaKOVmG.mjs → context-BAwgVSX5.mjs} +43 -31
- package/dist/decide-DlbyctOE.mjs +50 -0
- package/dist/{find-DEjjOlTG.mjs → find-p05wlbmu.mjs} +1 -1
- package/dist/format-Dni98cbh.mjs +206 -0
- package/dist/{help-groups-BwYoAWsh.mjs → help-groups-DYP_zMRP.mjs} +4 -0
- package/dist/{in-a-run-BS9Nc-JW.mjs → in-a-run-BI-hLIl_.mjs} +4 -2
- package/dist/{limits-BSwEjQST.mjs → limits-CElmhowV.mjs} +1 -1
- package/dist/{list-Qxr6rwsY.mjs → list-zW2xKuGr.mjs} +1 -1
- package/dist/{mcp-CslnG5wf.mjs → mcp-CM3b65AZ.mjs} +6 -6
- package/dist/output-CnRzrbUM.mjs +423 -0
- package/dist/{pause-D7OVB5Ac.mjs → pause-BuS83WAI.mjs} +4 -4
- package/dist/{relay-EceRIebO.mjs → relay-DAsJA8wF.mjs} +2 -2
- package/dist/{remove-0IQZVWmI.mjs → remove-Dh7D_hVn.mjs} +3 -3
- package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
- package/dist/{send-C0uwfUYi.mjs → send-MFEQZH31.mjs} +3 -3
- package/dist/{src-cmmXKHn1.mjs → src-BfxkJOYm.mjs} +234 -150
- package/dist/{src-Df6KOIDC.mjs → src-CdDTQLdQ.mjs} +2081 -1035
- package/dist/{src-oaJYIddp.mjs → src-D1QedD5y.mjs} +523 -81
- package/dist/{src-i5k2Y-kf.mjs → src-S9yK2OHU.mjs} +79 -11
- package/dist/store-B2OaE4ID.mjs +1356 -0
- package/dist/ui/assets/{app-qs8Lofv6.js → app-CAoZtku5.js} +16 -16
- package/dist/ui/assets/app-QoHVi-mb.css +1 -0
- package/dist/ui/assets/{index-DV1ZaOk-.js → index-BL7Ad0OA.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-CBkHJdqk.mjs → ui-C4Ys26oO.mjs} +4 -4
- package/dist/{usage-Cd8pEYyK.mjs → usage-C2zx-X6q.mjs} +43 -32
- package/dist/verdict-CIy8tAyX.mjs +84 -0
- package/dist/{watch-DW5R2OPO.mjs → watch-DzCeqM05.mjs} +28 -16
- package/dist/{workflows-BT1gDRIV.mjs → workflows-Rr5w90A0.mjs} +3 -3
- package/dist/{worktrees-g1MQMGie.mjs → worktrees-f6SAbum5.mjs} +11 -10
- package/package.json +1 -1
- package/dist/format-Cb5_m-Qd.mjs +0 -191
- package/dist/output-Bqljtmp_.mjs +0 -40
- package/dist/ui/assets/app-C8uhHVYp.css +0 -1
|
@@ -3,7 +3,7 @@ import { LineCounter, YAMLMap, YAMLSeq, isAlias, isCollection, isMap, isNode, is
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { closeSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { constants as constants$1, homedir, setPriority, tmpdir } from "node:os";
|
|
7
7
|
import { createConnection, createServer } from "node:net";
|
|
8
8
|
import { createInterface } from "node:readline";
|
|
9
9
|
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
@@ -86,6 +86,7 @@ const aws = defineIntegration({
|
|
|
86
86
|
region: z.string().optional()
|
|
87
87
|
},
|
|
88
88
|
args: ({ profile, region }) => [...profile === void 0 ? [] : ["--profile", profile], ...region === void 0 ? [] : ["--region", region]],
|
|
89
|
+
signedOut: ({ stderr }) => /Unable to locate credentials|ExpiredToken|InvalidClientTokenId|Token has expired|aws sso login|SSO session .* expired/iu.test(stderr),
|
|
89
90
|
doctor: async (exec, env) => {
|
|
90
91
|
if (!(await exec("aws", ["--version"])).ok) return {
|
|
91
92
|
name: "aws",
|
|
@@ -227,7 +228,7 @@ const installedCli = (id, program, login, rest) => ({
|
|
|
227
228
|
const unproven = rest.proven ? "" : ", unproven: not yet proven on a real session";
|
|
228
229
|
if (!version.ok) return {
|
|
229
230
|
name,
|
|
230
|
-
status:
|
|
231
|
+
status: "skip",
|
|
231
232
|
detail: `${program} CLI not found${unproven}`,
|
|
232
233
|
...rest.proven ? { hint: `Install ${program} to use the ${id} driver.` } : {}
|
|
233
234
|
};
|
|
@@ -247,7 +248,7 @@ const AGENT_DRIVERS = [
|
|
|
247
248
|
claudeCode,
|
|
248
249
|
installedCli("open-code", "opencode", "opencode auth login", {
|
|
249
250
|
label: "the `opencode` CLI",
|
|
250
|
-
proven:
|
|
251
|
+
proven: true,
|
|
251
252
|
onePerCheckout: false,
|
|
252
253
|
capabilities: {
|
|
253
254
|
questions: true,
|
|
@@ -255,13 +256,13 @@ const AGENT_DRIVERS = [
|
|
|
255
256
|
price: true,
|
|
256
257
|
midTurnMessages: false,
|
|
257
258
|
reportSchema: false,
|
|
258
|
-
turnLimit:
|
|
259
|
+
turnLimit: true,
|
|
259
260
|
mcpServers: true
|
|
260
261
|
}
|
|
261
262
|
}),
|
|
262
263
|
installedCli("command-code", "cmd", "cmd login", {
|
|
263
264
|
label: "the `cmd` CLI of Command Code",
|
|
264
|
-
proven:
|
|
265
|
+
proven: true,
|
|
265
266
|
onePerCheckout: true,
|
|
266
267
|
capabilities: {
|
|
267
268
|
questions: true,
|
|
@@ -299,6 +300,7 @@ const gh = defineIntegration({
|
|
|
299
300
|
program: "gh",
|
|
300
301
|
options: {},
|
|
301
302
|
args: () => [],
|
|
303
|
+
signedOut: ({ exitCode, stderr }) => exitCode === 4 || /\bgh auth login\b/u.test(stderr),
|
|
302
304
|
doctor: async (exec) => {
|
|
303
305
|
if (!(await exec("gh", ["--version"])).ok) return {
|
|
304
306
|
name: "gh",
|
|
@@ -542,13 +544,14 @@ const DriverSchema = z.enum(DRIVER_IDS, { error: (issue) => retiredDriverHint(is
|
|
|
542
544
|
/** `opus`, `sonnet`, `haiku`, or a full model id such as `claude-opus-5-5`. */
|
|
543
545
|
const ModelSchema = z.string().min(1);
|
|
544
546
|
/** How hard the model thinks; Claude Code's own default when unset. */
|
|
545
|
-
const
|
|
547
|
+
const EFFORTS = [
|
|
546
548
|
"low",
|
|
547
549
|
"medium",
|
|
548
550
|
"high",
|
|
549
551
|
"xhigh",
|
|
550
552
|
"max"
|
|
551
|
-
]
|
|
553
|
+
];
|
|
554
|
+
const EffortSchema = z.enum(EFFORTS).describe("Reasoning effort. Default: Claude Code's for the model.");
|
|
552
555
|
const IsolationSchema = z.enum([
|
|
553
556
|
"worktree",
|
|
554
557
|
"continue",
|
|
@@ -718,13 +721,20 @@ const TOOL_NODE_SCHEMAS = INTEGRATIONS.map((i) => toolNodeSchema(i));
|
|
|
718
721
|
const base = nodeBaseFields;
|
|
719
722
|
/** A Claude node's output field that is not a list: a rule or JSON Schema. */
|
|
720
723
|
const NotListSchema = z.unknown().refine((value) => typeof value !== "object" || value === null || !("list" in value), { message: LIST_RULE_HINT });
|
|
724
|
+
const NODE_EFFORT_HINT = "expected low, medium, high, xhigh or max, or {{item.<field>}}";
|
|
725
|
+
/**
|
|
726
|
+
* A Claude node's effort: a level, or on a `forEach` node a template reading
|
|
727
|
+
* its item (`{{item.effort}}`), which the compiler checks and each item
|
|
728
|
+
* renders.
|
|
729
|
+
*/
|
|
730
|
+
const NodeEffortSchema = z.union([EffortSchema, z.string().regex(/\{\{.*\}\}/su, { error: NODE_EFFORT_HINT })], { error: NODE_EFFORT_HINT }).describe("Reasoning effort, or {{item.<field>}} on a forEach node. Default: Claude Code's for the model.");
|
|
721
731
|
/**
|
|
722
732
|
* What an agent brings to the node it fills, and what a plain Claude node sets
|
|
723
733
|
* itself. A node's own value wins over its agent's.
|
|
724
734
|
*/
|
|
725
735
|
const agentSettings = {
|
|
726
736
|
model: ModelSchema.optional(),
|
|
727
|
-
effort:
|
|
737
|
+
effort: NodeEffortSchema.optional(),
|
|
728
738
|
driver: DriverSchema.optional(),
|
|
729
739
|
skills: z.array(z.string()).optional().describe("Node-layer skills (highest precedence)."),
|
|
730
740
|
context: z.array(z.string()).optional().describe("Node-layer context files."),
|
|
@@ -1076,6 +1086,8 @@ const RulesSchema = z.strictObject({
|
|
|
1076
1086
|
permissions: ToolRulesSchema.optional().describe("Tool rules every Claude node gets on top of its own, e.g. deny: ['Bash(git push *)']."),
|
|
1077
1087
|
links: z.enum(["on", "off"]).optional().describe("Default: on, the user can link a running node to a node of another run. off: its nodes get no message tools unless they have someone to write to, and cannot be linked.")
|
|
1078
1088
|
});
|
|
1089
|
+
/** The OS priority Ork starts a run's sessions and tool commands at. */
|
|
1090
|
+
const PrioritySchema = z.enum(["low", "normal"]);
|
|
1079
1091
|
const ProjectSchema = z.strictObject({
|
|
1080
1092
|
schemaVersion: z.literal(1),
|
|
1081
1093
|
...RulesSchema.shape,
|
|
@@ -1083,7 +1095,9 @@ const ProjectSchema = z.strictObject({
|
|
|
1083
1095
|
quotas: QuotasSchema.optional().describe("Most this project may spend on this machine. The user's own caps apply too."),
|
|
1084
1096
|
calls: z.strictObject({ accept: z.array(z.string()).describe("The registered projects whose workflows may call this one's.") }).optional().describe("Default: any registered project may call this one's workflows."),
|
|
1085
1097
|
context: z.array(z.string()).optional().describe("Context files every node reads, relative to the project root; they come before the workflow's."),
|
|
1086
|
-
experimental: z.array(z.string()).optional().describe("Experimental features this project turns on.")
|
|
1098
|
+
experimental: z.array(z.string()).optional().describe("Experimental features this project turns on."),
|
|
1099
|
+
heavy: z.array(z.string().min(1)).optional().describe("The project's full-suite commands, as Bash permission patterns without Bash(…), e.g. pnpm check. A node with verify: may not run them in its session; vitest run does not cover vitest run <files>."),
|
|
1100
|
+
priority: PrioritySchema.optional().describe("Default: low, sessions and tool commands start at a lower OS priority. normal: as the ork process.")
|
|
1087
1101
|
});
|
|
1088
1102
|
/** `~/.ork/settings.yaml`: the user's own settings, and never a credential. */
|
|
1089
1103
|
const UserSettingsSchema = z.strictObject({
|
|
@@ -3213,7 +3227,7 @@ const filesOf = (dir, name) => ({
|
|
|
3213
3227
|
markdown: path.join(dir, `${name}.md`),
|
|
3214
3228
|
sidecar: path.join(dir, `${name}${AGENT_SIDECAR_SUFFIX}`)
|
|
3215
3229
|
});
|
|
3216
|
-
const write = (name, agent, options, mode) => {
|
|
3230
|
+
const write$1 = (name, agent, options, mode) => {
|
|
3217
3231
|
if (!AgentNameSchema.safeParse(name).success) return {
|
|
3218
3232
|
ok: false,
|
|
3219
3233
|
error: `\`${name}\` is not an agent name (lowercase letters, digits, '-' and '_')`
|
|
@@ -3253,7 +3267,7 @@ const write = (name, agent, options, mode) => {
|
|
|
3253
3267
|
};
|
|
3254
3268
|
};
|
|
3255
3269
|
/** Creates an agent in Ork's folder for the scope; never overwrites. */
|
|
3256
|
-
const createAgent = (name, agent, options) => write(name, agent, options, "create");
|
|
3270
|
+
const createAgent = (name, agent, options) => write$1(name, agent, options, "create");
|
|
3257
3271
|
/** Deletes an agent from Ork's folder for the scope. */
|
|
3258
3272
|
const deleteAgent = (name, options) => {
|
|
3259
3273
|
const dir = agentFolder(options);
|
|
@@ -4824,6 +4838,359 @@ const FEEDBACK_SEVERITIES = [
|
|
|
4824
4838
|
"medium",
|
|
4825
4839
|
"high"
|
|
4826
4840
|
];
|
|
4841
|
+
// ../core/src/runtime/ends.ts
|
|
4842
|
+
/**
|
|
4843
|
+
* @file How a node ended, as one of a closed set of kinds (specs/runtime/
|
|
4844
|
+
* session-ends.md): the registry that gives each kind its label, Ork's own
|
|
4845
|
+
* message, who should act and what to do, and `endFor`, which builds an end
|
|
4846
|
+
* with its raw detail scrubbed and capped. ends-legacy.ts reads one from runs
|
|
4847
|
+
* stored before ends existed.
|
|
4848
|
+
*/
|
|
4849
|
+
/** The most of a detail kept, in characters. */
|
|
4850
|
+
const END_DETAIL_LIMIT = 4096;
|
|
4851
|
+
const RETRY = "This may pass: `ork resume` tries again.";
|
|
4852
|
+
const usd$1 = (n) => `$${Number.isInteger(n) ? String(n) : n.toFixed(2)}`;
|
|
4853
|
+
const paren = (value) => value === void 0 ? "" : ` (${value})`;
|
|
4854
|
+
const colon = (reason) => reason === void 0 ? "" : `: ${reason}`;
|
|
4855
|
+
const info = (label, actor, message, hint, flags = {}) => ({
|
|
4856
|
+
label,
|
|
4857
|
+
message,
|
|
4858
|
+
actor,
|
|
4859
|
+
transient: flags.transient ?? false,
|
|
4860
|
+
stopsRun: flags.stopsRun ?? false,
|
|
4861
|
+
hint,
|
|
4862
|
+
docsAnchor: label.toLowerCase().replaceAll(" ", "-")
|
|
4863
|
+
});
|
|
4864
|
+
/** Each kind's label, message, actor, flags, hint and docs anchor. */
|
|
4865
|
+
const END_KINDS = {
|
|
4866
|
+
done: info("Done", null, () => "finished", ""),
|
|
4867
|
+
reported_failure: info("Reported failure", "agent", () => "reported that it failed", "Read its summary, fix what it names, and run it again."),
|
|
4868
|
+
no_report: info("No report", "agent", () => "ended without handing in a report", "Check that its brief says what to report; its session is in the Logs panel."),
|
|
4869
|
+
invalid_report: info("Invalid report", "agent", (f) => `handed in a report that breaks its declared output${colon(f.reason)}`, "Check that its brief asks for every field its `output.schema` declares."),
|
|
4870
|
+
turn_limit: info("Turn limit", "workflow", (f) => `reached its turn limit${paren(f.maxTurns === void 0 ? void 0 : String(f.maxTurns))}`, "Raise its `maxTurns`, or give it a smaller task."),
|
|
4871
|
+
budget_limit: info("Budget limit", "workflow", (f) => `reached its budget${paren(f.budgetUsd === void 0 ? void 0 : usd$1(f.budgetUsd))}`, "Raise `budgetUsd` on the node or `limits.budgetUsd` on the workflow, then `ork resume`."),
|
|
4872
|
+
timed_out: info("Timed out", "workflow", (f) => `reached its timeout${paren(f.timeout)}`, "Raise its `timeout`, or give it a smaller task."),
|
|
4873
|
+
context_too_long: info("Context too long", "workflow", () => "outgrew its model's context", "Give it a smaller task or less context, or a model with a larger context."),
|
|
4874
|
+
plan_limit: info("Plan limit", "account", (f) => `ran into a usage limit${paren(f.window?.replaceAll("_", " "))}`, `Wait until the limit resets. ${RETRY}`, {
|
|
4875
|
+
transient: true,
|
|
4876
|
+
stopsRun: true
|
|
4877
|
+
}),
|
|
4878
|
+
rate_limited: info("Rate limited", "provider", () => "was asked by its provider to slow down", RETRY, { transient: true }),
|
|
4879
|
+
provider_unavailable: info("Provider unavailable", "provider", () => "could not reach its provider, or the provider was overloaded", RETRY, { transient: true }),
|
|
4880
|
+
auth: info("Not signed in", "account", () => "could not sign in to its provider", "Sign in to the driver's tool again (`ork doctor` says how), then `ork resume`."),
|
|
4881
|
+
out_of_credits: info("Out of credits", "account", () => "found no credit left on its account", "Add credit to the account, or use another, then `ork resume`."),
|
|
4882
|
+
model_unavailable: info("Model unavailable", "account", () => "asked for something its model or provider can't serve", "Use another model or driver for the node."),
|
|
4883
|
+
interrupted: info("Interrupted", "user", () => "was stopped", "`ork resume` goes on from where it stopped."),
|
|
4884
|
+
tool_missing: info("Tool missing", "account", (f) => `could not start ${f.program ?? "its program"}: it isn't installed`, "Install it (`ork doctor` checks the drivers), then `ork resume`."),
|
|
4885
|
+
command_failed: info("Command failed", "workflow", (f) => `${f.program ?? "its command"} exited with ${f.exitCode === void 0 ? "an error" : String(f.exitCode)}`, "Its output is under Details."),
|
|
4886
|
+
invalid_output: info("Invalid output", "workflow", (f) => `gave JSON that breaks its declared output${colon(f.reason)}`, "Make the command print what its `output.schema` declares."),
|
|
4887
|
+
invalid_node: info("Invalid node", "workflow", (f) => `could not start${colon(f.reason)}`, "Fix the node in the workflow file (`ork validate` checks it)."),
|
|
4888
|
+
driver_crashed: info("Driver crashed", "ork", () => "lost its driver in a way Ork can't read", "Its last output is under Details; Ork files it as a bug."),
|
|
4889
|
+
unknown: info("Unknown", "ork", () => "ended for a reason Ork doesn't recognize", "Its detail is under Details; Ork files it as a bug, so a rule can be added.")
|
|
4890
|
+
};
|
|
4891
|
+
/**
|
|
4892
|
+
* An end of `kind`, its message from `facts`. The detail is scrubbed (with the
|
|
4893
|
+
* feedback redactor when `scrub` is given, secrets masked always) and capped.
|
|
4894
|
+
*/
|
|
4895
|
+
const endFor = (kind, facts = {}, detail, scrub) => {
|
|
4896
|
+
const end = {
|
|
4897
|
+
kind,
|
|
4898
|
+
message: END_KINDS[kind].message(facts)
|
|
4899
|
+
};
|
|
4900
|
+
if (detail === void 0 || detail.trim() === "") return end;
|
|
4901
|
+
const clean = scrub === void 0 ? maskSecrets(detail) : redact(detail, scrub);
|
|
4902
|
+
end.detail = clean.length > 4096 ? `${clean.slice(0, 4095)}…` : clean;
|
|
4903
|
+
return end;
|
|
4904
|
+
};
|
|
4905
|
+
// ../core/src/runtime/ends-legacy.ts
|
|
4906
|
+
/**
|
|
4907
|
+
* @file The legacy readers of how a node ended: a run stored before ends
|
|
4908
|
+
* existed gets a kind from the text it kept (specs/runtime/session-ends.md),
|
|
4909
|
+
* and the stored event is never rewritten.
|
|
4910
|
+
*/
|
|
4911
|
+
/**
|
|
4912
|
+
* Text Ork stored before ends existed, to a kind, in the plan's precedence:
|
|
4913
|
+
* interrupt, plan limit, budget, timeout, turn limit, provider error, missing
|
|
4914
|
+
* tool, failed command, crash. Frozen: old runs' text never changes, so this is
|
|
4915
|
+
* not the live provider classifier.
|
|
4916
|
+
*/
|
|
4917
|
+
const LEGACY_TEXT = [
|
|
4918
|
+
["interrupted", /\bstopped by the user\b|\bthe node was stopped\b|\bstopped while watching\b|\bwas interrupted\b|\bended interrupted\b|\bthe gate was withdrawn\b/u],
|
|
4919
|
+
["plan_limit", /\bClaude plan's .* limit was reached\b|\bquota of \$[\d.]+ is spent\b|\bended quota_exhausted\b/u],
|
|
4920
|
+
["budget_limit", /\(error_max_budget_usd\)|\bmaximum budget\b|\bnothing is left of the run's budget\b|\bbudget of \$[\d.]+ is spent\b|\bended budget_exhausted\b/u],
|
|
4921
|
+
["timed_out", /\btimed out after\b|\bno one answered the gate\b|\btimeout of \S+ passed\b|\bended timed_out\b/u],
|
|
4922
|
+
["turn_limit", /\(error_max_turns\)|\bmaximum number of turns\b/iu],
|
|
4923
|
+
["context_too_long", /\bprompt is too long\b|\bcontext[ _](?:length|window)\b/iu],
|
|
4924
|
+
["rate_limited", /\brate_limit_error\b|\brate.?limit(?:ed)?\b|\b(?:status|HTTP)\W{0,3}429\b/iu],
|
|
4925
|
+
["out_of_credits", /\bcredit balance\b|\binsufficient[ _](?:credit|funds|quota)\b/iu],
|
|
4926
|
+
["auth", /\bauthentication_error\b|\binvalid (?:x-)?api.?key\b|\bnot logged in\b|\/login\b|\b(?:status|HTTP)\W{0,3}401\b/iu],
|
|
4927
|
+
["provider_unavailable", /\boverloaded(?:_error)?\b|\b(?:status|HTTP)\W{0,3}5\d\d\b|\bapi_error\b|\bECONNRESET\b|\bETIMEDOUT\b|\bENOTFOUND\b|\bfetch failed\b/u],
|
|
4928
|
+
["model_unavailable", /\bassistant message prefill\b|\bInvalid `signature` in `thinking` block\b|\bnot_found_error\b|\binvalid_request_error\b/u],
|
|
4929
|
+
["tool_missing", /\bENOENT\b|\bcommand not found\b|\bno agent driver is ready\b/u],
|
|
4930
|
+
["invalid_output", /\$ORK_OUTPUT\b|\bthe JSON result is\b/u],
|
|
4931
|
+
["command_failed", /\bexited with \d+|^[\w.-]+ [\w-]+ failed: /u],
|
|
4932
|
+
["driver_crashed", /\bthe session ended without a result\b/u]
|
|
4933
|
+
];
|
|
4934
|
+
const textKind = (text) => LEGACY_TEXT.find(([, pattern]) => pattern.test(text))?.[0] ?? null;
|
|
4935
|
+
/** What `fieldProblems` writes (run/declared.ts). */
|
|
4936
|
+
const DECLARED_PROBLEM = /\bis missing, expected\b|\bis .+, expected\b|^the result isn't a JSON object$/u;
|
|
4937
|
+
const WITHOUT_REPORT = /^ended without a report \(([\w-]+)\): ([\s\S]*)$/u;
|
|
4938
|
+
/** A driver's subtype, read with the session's error text. */
|
|
4939
|
+
const subtypeKind = (subtype, text) => {
|
|
4940
|
+
if (subtype === "error_max_turns") return "turn_limit";
|
|
4941
|
+
if (subtype === "error_max_budget_usd") return "budget_limit";
|
|
4942
|
+
if (subtype === "error_max_structured_output_retries") return "invalid_report";
|
|
4943
|
+
const said = textKind(text);
|
|
4944
|
+
if (said !== null) return said;
|
|
4945
|
+
if (subtype === "error_driver") return "driver_crashed";
|
|
4946
|
+
return text === "no valid report" || subtype === "success" ? "no_report" : "unknown";
|
|
4947
|
+
};
|
|
4948
|
+
/**
|
|
4949
|
+
* How a finished node ended: its own `end`, or, for an event stored before ends
|
|
4950
|
+
* existed, a kind read from its `error` text (`unknown` when nothing matches),
|
|
4951
|
+
* with that text as the detail.
|
|
4952
|
+
*/
|
|
4953
|
+
const endOf = (finished) => {
|
|
4954
|
+
if (finished.end !== void 0) return finished.end;
|
|
4955
|
+
if (finished.ok) return endFor("done");
|
|
4956
|
+
const error = finished.error ?? "";
|
|
4957
|
+
return endFor(legacyKind(error, finished.output), {}, error);
|
|
4958
|
+
};
|
|
4959
|
+
const legacyKind = (error, output) => {
|
|
4960
|
+
if ("artifacts" in output) return DECLARED_PROBLEM.test(error) ? "invalid_report" : "reported_failure";
|
|
4961
|
+
const without = WITHOUT_REPORT.exec(error);
|
|
4962
|
+
if (without !== null) return subtypeKind(without[1] ?? "", without[2] ?? "");
|
|
4963
|
+
return textKind(error) ?? "unknown";
|
|
4964
|
+
};
|
|
4965
|
+
/**
|
|
4966
|
+
* How a session ended: its `end`, or, for an outcome stored before ends existed
|
|
4967
|
+
* (or a driver that does not set one yet), one read from its subtype, report
|
|
4968
|
+
* and errors.
|
|
4969
|
+
*/
|
|
4970
|
+
const outcomeEnd = (outcome) => {
|
|
4971
|
+
if (outcome.end !== void 0) return outcome.end;
|
|
4972
|
+
const { report } = outcome;
|
|
4973
|
+
if (report !== null && outcome.subtype === "success") return endFor(report.status === "fail" ? "reported_failure" : "done");
|
|
4974
|
+
const text = outcome.errors[0] ?? (report === null ? "no valid report" : "");
|
|
4975
|
+
return endFor(subtypeKind(outcome.subtype, text), {}, outcome.errors.join("\n"));
|
|
4976
|
+
};
|
|
4977
|
+
// ../core/src/feedback/end-signals.ts
|
|
4978
|
+
/**
|
|
4979
|
+
* @file The feedback a node's end files (specs/feedback.md,
|
|
4980
|
+
* specs/runtime/session-ends.md), by who should act on it: Ork's own bug, the
|
|
4981
|
+
* workflow's, or the agent's context. An account's, a provider's or the
|
|
4982
|
+
* user's end is a setup hint on the screen, not feedback.
|
|
4983
|
+
*/
|
|
4984
|
+
/**
|
|
4985
|
+
* Ends that are a node's work saying no, not a problem to act on: an agent that
|
|
4986
|
+
* reports `fail`, a command that exits non-zero. Neither is filed.
|
|
4987
|
+
*/
|
|
4988
|
+
const ANSWERS = /* @__PURE__ */ new Set(["reported_failure", "command_failed"]);
|
|
4989
|
+
/**
|
|
4990
|
+
* The feedback a node's end files, by who should act on it: Ork's own bug (with
|
|
4991
|
+
* the scrubbed detail, so a rule can be added), the workflow's, or the agent's
|
|
4992
|
+
* context. An account's, a provider's or the user's is not feedback.
|
|
4993
|
+
*/
|
|
4994
|
+
const endSignal = (node, end) => {
|
|
4995
|
+
const info = END_KINDS[end.kind];
|
|
4996
|
+
if (ANSWERS.has(end.kind)) return null;
|
|
4997
|
+
const base = {
|
|
4998
|
+
code: `end.${end.kind}`,
|
|
4999
|
+
title: `${node} ${end.message}`,
|
|
5000
|
+
body: info.hint,
|
|
5001
|
+
...end.detail === void 0 ? {} : { evidence: [end.detail] }
|
|
5002
|
+
};
|
|
5003
|
+
if (info.actor === "ork") return {
|
|
5004
|
+
...base,
|
|
5005
|
+
kind: "bug",
|
|
5006
|
+
target: "ork",
|
|
5007
|
+
severity: "medium"
|
|
5008
|
+
};
|
|
5009
|
+
if (info.actor === "workflow") return {
|
|
5010
|
+
...base,
|
|
5011
|
+
kind: "workflow",
|
|
5012
|
+
target: "workflow",
|
|
5013
|
+
severity: "low"
|
|
5014
|
+
};
|
|
5015
|
+
return info.actor === "agent" ? {
|
|
5016
|
+
...base,
|
|
5017
|
+
kind: "context-gap",
|
|
5018
|
+
target: "workflow",
|
|
5019
|
+
severity: "low"
|
|
5020
|
+
} : null;
|
|
5021
|
+
};
|
|
5022
|
+
// ../core/src/feedback/signals.ts
|
|
5023
|
+
const ORK_TOOL = "mcp__ork__";
|
|
5024
|
+
/**
|
|
5025
|
+
* An observer for one run's events: give it each event, and record what it
|
|
5026
|
+
* returns, in order (the event, its origin filled in when it is feedback, then
|
|
5027
|
+
* any feedback it signals).
|
|
5028
|
+
*/
|
|
5029
|
+
const createSignals = (options = {}) => {
|
|
5030
|
+
let workflow;
|
|
5031
|
+
const nodeTypes = /* @__PURE__ */ new Map();
|
|
5032
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
5033
|
+
const tools = /* @__PURE__ */ new Map();
|
|
5034
|
+
const asked = /* @__PURE__ */ new Map();
|
|
5035
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5036
|
+
const originOf = (node) => {
|
|
5037
|
+
const origin = {};
|
|
5038
|
+
if (workflow !== void 0) origin.workflow = workflow;
|
|
5039
|
+
if (options.workflowHash !== void 0) origin.workflowHash = options.workflowHash;
|
|
5040
|
+
const type = node === void 0 ? void 0 : nodeTypes.get(node);
|
|
5041
|
+
if (type !== void 0) origin.nodeType = type;
|
|
5042
|
+
const session = node === void 0 ? void 0 : sessions.get(node);
|
|
5043
|
+
if (session !== void 0) origin.model = session.model;
|
|
5044
|
+
if (session?.driver !== void 0) origin.driver = session.driver;
|
|
5045
|
+
if (options.version !== void 0) origin.version = options.version;
|
|
5046
|
+
return origin;
|
|
5047
|
+
};
|
|
5048
|
+
const record = (node, signal) => {
|
|
5049
|
+
const print = fingerprint(signal);
|
|
5050
|
+
if (seen.has(print)) return [];
|
|
5051
|
+
seen.add(print);
|
|
5052
|
+
return [{
|
|
5053
|
+
type: "feedback.recorded",
|
|
5054
|
+
...node === void 0 ? {} : { node },
|
|
5055
|
+
feedback: {
|
|
5056
|
+
source: "system",
|
|
5057
|
+
kind: signal.kind,
|
|
5058
|
+
target: signal.target,
|
|
5059
|
+
severity: signal.severity,
|
|
5060
|
+
title: signal.title,
|
|
5061
|
+
body: signal.body,
|
|
5062
|
+
evidence: signal.evidence ?? [],
|
|
5063
|
+
code: signal.code,
|
|
5064
|
+
fingerprint: print,
|
|
5065
|
+
origin: originOf(node)
|
|
5066
|
+
}
|
|
5067
|
+
}];
|
|
5068
|
+
};
|
|
5069
|
+
const answered = (question, answer) => {
|
|
5070
|
+
if (question.kind === "permission") {
|
|
5071
|
+
if (answer !== "deny") return null;
|
|
5072
|
+
const tool = question.question.replace(/^Allow /u, "").replace(/\?$/u, "");
|
|
5073
|
+
return [question.node, {
|
|
5074
|
+
code: "permission.denied",
|
|
5075
|
+
kind: "friction",
|
|
5076
|
+
target: "workflow",
|
|
5077
|
+
severity: "low",
|
|
5078
|
+
title: `${question.node} was denied ${tool}`,
|
|
5079
|
+
body: "Allow it in the node's `tools.allow` if it needs it, or tell it in its prompt not to use it.",
|
|
5080
|
+
evidence: [tool]
|
|
5081
|
+
}];
|
|
5082
|
+
}
|
|
5083
|
+
if (question.recommended === void 0 || answer === question.recommended) return null;
|
|
5084
|
+
return [question.node, {
|
|
5085
|
+
code: "recommendation.overridden",
|
|
5086
|
+
kind: "context-gap",
|
|
5087
|
+
target: "workflow",
|
|
5088
|
+
severity: "low",
|
|
5089
|
+
title: `The answer to ${question.node}'s question was not the one it recommended`,
|
|
5090
|
+
body: `Asked: ${question.question} Recommended: ${question.recommended}. Answered: ${answer}.`
|
|
5091
|
+
}];
|
|
5092
|
+
};
|
|
5093
|
+
/** The feedback an event signals, if any, and the node it is about. */
|
|
5094
|
+
const signalOf = (event) => {
|
|
5095
|
+
if (event.type === "loop.exhausted") return [event.from, {
|
|
5096
|
+
code: "loop.exhausted",
|
|
5097
|
+
kind: "workflow",
|
|
5098
|
+
target: "workflow",
|
|
5099
|
+
severity: "medium",
|
|
5100
|
+
title: `The loop ${event.from} -> ${event.to} ran out of its ${String(event.maxRounds)} rounds`,
|
|
5101
|
+
body: "Raise `maxRounds`, or make what the loop's nodes are asked to fix clearer so it settles sooner."
|
|
5102
|
+
}];
|
|
5103
|
+
if (event.type === "run.finished" && event.result.status === "budget_exhausted") return [void 0, {
|
|
5104
|
+
code: "budget.exhausted",
|
|
5105
|
+
kind: "workflow",
|
|
5106
|
+
target: "workflow",
|
|
5107
|
+
severity: "medium",
|
|
5108
|
+
title: "The run's budget ran out",
|
|
5109
|
+
body: `The run stopped after spending $${event.result.costUsd.toFixed(4)}. Raise \`limits.budgetUsd\`, or give nodes cheaper models or smaller budgets.`
|
|
5110
|
+
}];
|
|
5111
|
+
if (event.type === "decision.answered") {
|
|
5112
|
+
const question = asked.get(event.decision);
|
|
5113
|
+
return question === void 0 ? null : answered(question, event.answer);
|
|
5114
|
+
}
|
|
5115
|
+
if (event.type === "node.event" && event.event.type === "tool.result") {
|
|
5116
|
+
const name = tools.get(`${event.id}\0${event.event.id}`);
|
|
5117
|
+
if (!event.event.isError || name?.startsWith(ORK_TOOL) !== true) return null;
|
|
5118
|
+
const tool = name.slice(10);
|
|
5119
|
+
if (tool === "report") return [event.id, {
|
|
5120
|
+
code: "report.rejected",
|
|
5121
|
+
kind: "context-gap",
|
|
5122
|
+
target: "workflow",
|
|
5123
|
+
severity: "low",
|
|
5124
|
+
title: `${event.id}'s report was not accepted`,
|
|
5125
|
+
body: END_KINDS.invalid_report.hint,
|
|
5126
|
+
evidence: [name]
|
|
5127
|
+
}];
|
|
5128
|
+
return [event.id, {
|
|
5129
|
+
code: "tool.error",
|
|
5130
|
+
kind: "bug",
|
|
5131
|
+
target: "ork",
|
|
5132
|
+
severity: "medium",
|
|
5133
|
+
title: `Ork's ${tool} tool returned an error`,
|
|
5134
|
+
body: `A call to \`${tool}\` failed; the node's events show the call.`,
|
|
5135
|
+
evidence: [name]
|
|
5136
|
+
}];
|
|
5137
|
+
}
|
|
5138
|
+
if (event.type === "node.finished" && nodeTypes.get(event.id) === "human.gate" && event.output["answeredBy"] === "timeout") return [event.id, {
|
|
5139
|
+
code: "gate.timeout",
|
|
5140
|
+
kind: "workflow",
|
|
5141
|
+
target: "workflow",
|
|
5142
|
+
severity: "low",
|
|
5143
|
+
title: `The gate ${event.id} timed out and took its default`,
|
|
5144
|
+
body: "No one answered in time. Lengthen its `timeout`, or keep it if the default was right."
|
|
5145
|
+
}];
|
|
5146
|
+
if (event.type === "node.finished" && !event.ok) {
|
|
5147
|
+
const signal = endSignal(event.id, endOf(event));
|
|
5148
|
+
return signal === null ? null : [event.id, signal];
|
|
5149
|
+
}
|
|
5150
|
+
return null;
|
|
5151
|
+
};
|
|
5152
|
+
/** Keeps what later events and origins need. */
|
|
5153
|
+
const observe = (event) => {
|
|
5154
|
+
if (event.type === "run.started") workflow = event.workflow;
|
|
5155
|
+
if (event.type === "node.started") nodeTypes.set(event.id, event.nodeType);
|
|
5156
|
+
if (event.type === "decision.opened") {
|
|
5157
|
+
const question = {
|
|
5158
|
+
node: event.node,
|
|
5159
|
+
kind: event.kind,
|
|
5160
|
+
question: event.question
|
|
5161
|
+
};
|
|
5162
|
+
if (event.recommended !== void 0) question.recommended = event.recommended;
|
|
5163
|
+
asked.set(event.decision, question);
|
|
5164
|
+
}
|
|
5165
|
+
if (event.type !== "node.event") return;
|
|
5166
|
+
const inner = event.event;
|
|
5167
|
+
if (inner.type === "session.started" && inner.fork !== true) sessions.set(event.id, {
|
|
5168
|
+
model: inner.model,
|
|
5169
|
+
...inner.driver === void 0 ? {} : { driver: inner.driver }
|
|
5170
|
+
});
|
|
5171
|
+
if (inner.type === "tool.called") tools.set(`${event.id}\0${inner.id}`, inner.name);
|
|
5172
|
+
};
|
|
5173
|
+
return (event) => {
|
|
5174
|
+
observe(event);
|
|
5175
|
+
if (event.type === "feedback.recorded") {
|
|
5176
|
+
if (seen.has(event.feedback.fingerprint)) return [];
|
|
5177
|
+
seen.add(event.feedback.fingerprint);
|
|
5178
|
+
const origin = {
|
|
5179
|
+
...originOf(event.node),
|
|
5180
|
+
...event.feedback.origin
|
|
5181
|
+
};
|
|
5182
|
+
return [{
|
|
5183
|
+
...event,
|
|
5184
|
+
feedback: {
|
|
5185
|
+
...event.feedback,
|
|
5186
|
+
origin
|
|
5187
|
+
}
|
|
5188
|
+
}];
|
|
5189
|
+
}
|
|
5190
|
+
const signal = signalOf(event);
|
|
5191
|
+
return signal === null ? [event] : [event, ...record(...signal)];
|
|
5192
|
+
};
|
|
5193
|
+
};
|
|
4827
5194
|
// ../core/src/project/mcp-file.ts
|
|
4828
5195
|
/**
|
|
4829
5196
|
* @file The project's `.mcp.json`, the file Claude Code reads too, so a server
|
|
@@ -4937,6 +5304,48 @@ const checkMcpServers = (workflow, options) => {
|
|
|
4937
5304
|
});
|
|
4938
5305
|
});
|
|
4939
5306
|
};
|
|
5307
|
+
// ../core/src/project/heavy.ts
|
|
5308
|
+
/**
|
|
5309
|
+
* @file A project's rules for the machine its runs share: the heavy commands
|
|
5310
|
+
* (its full-suite checks), denied to the session of a node with `verify:`
|
|
5311
|
+
* since Ork runs that check, and the OS priority, each with Ork's default.
|
|
5312
|
+
*/
|
|
5313
|
+
/** A `heavy` pattern as the Bash rule that denies it to a session. */
|
|
5314
|
+
const heavyRule = (pattern) => `Bash(${pattern})`;
|
|
5315
|
+
/**
|
|
5316
|
+
* The nodes, with the heavy commands denied to each Claude node that has
|
|
5317
|
+
* `verify:`; other nodes are left as they are. A pattern without `*` matches
|
|
5318
|
+
* only that command, so `vitest run` leaves `vitest run <files>` allowed.
|
|
5319
|
+
*/
|
|
5320
|
+
const withoutHeavy = (nodes, heavy) => {
|
|
5321
|
+
if (heavy.length === 0) return nodes;
|
|
5322
|
+
const rules = heavy.map((pattern) => heavyRule(pattern));
|
|
5323
|
+
return Object.fromEntries(Object.entries(nodes).map(([id, node]) => isClaudeNode(node) && "verify" in node && node.verify !== void 0 ? [id, {
|
|
5324
|
+
...node,
|
|
5325
|
+
tools: {
|
|
5326
|
+
...node.tools,
|
|
5327
|
+
deny: [.../* @__PURE__ */ new Set([...node.tools?.deny ?? [], ...rules])]
|
|
5328
|
+
}
|
|
5329
|
+
}] : [id, node]));
|
|
5330
|
+
};
|
|
5331
|
+
const words = (text) => text.trim().replaceAll(/\s+/gu, " ");
|
|
5332
|
+
/**
|
|
5333
|
+
* Whether `command` matches a `heavy` pattern, in the permission patterns'
|
|
5334
|
+
* syntax: `*` is any text, a trailing ` *` (or `:*`) also matches nothing, and
|
|
5335
|
+
* a pattern without `*` matches only that command.
|
|
5336
|
+
*/
|
|
5337
|
+
const matchesPattern = (pattern, command) => {
|
|
5338
|
+
const tail = words(pattern).replace(/:\*$/u, " *");
|
|
5339
|
+
const open = tail.endsWith(" *");
|
|
5340
|
+
const body = (open ? tail.slice(0, -2) : tail).split("*").map((part) => part.replaceAll(/[.+?^${}()|[\]\\]/gu, "\\$&")).join(".*");
|
|
5341
|
+
return new RegExp(`^${body}${open ? "(?: .*)?" : ""}$`, "su").test(words(command));
|
|
5342
|
+
};
|
|
5343
|
+
/** Whether `command` is one of the project's heavy commands. */
|
|
5344
|
+
const isHeavy = (heavy, command) => heavy.some((pattern) => matchesPattern(pattern, command));
|
|
5345
|
+
const machineRulesOf = (project) => ({
|
|
5346
|
+
heavy: project?.heavy ?? [],
|
|
5347
|
+
priority: project?.priority ?? "low"
|
|
5348
|
+
});
|
|
4940
5349
|
// ../core/src/project/templates.ts
|
|
4941
5350
|
/**
|
|
4942
5351
|
* @file Git name templates (`ork/{run}/{node}`, `{inputs.type}/{inputs.slug}`):
|
|
@@ -5028,7 +5437,8 @@ const dropUndefined = (value) => Object.fromEntries(Object.entries(value).filter
|
|
|
5028
5437
|
/**
|
|
5029
5438
|
* The workflow with its project's rules under it: git rules key by key and
|
|
5030
5439
|
* defaults key by key, practices one by one (the workflow's win), permissions
|
|
5031
|
-
* added together,
|
|
5440
|
+
* added together, the project's context files before the workflow's, and its
|
|
5441
|
+
* heavy commands denied to nodes with `verify:`.
|
|
5032
5442
|
*/
|
|
5033
5443
|
const applyProject = (workflow, options) => {
|
|
5034
5444
|
const { project } = options;
|
|
@@ -5062,6 +5472,7 @@ const applyProject = (workflow, options) => {
|
|
|
5062
5472
|
});
|
|
5063
5473
|
return dropUndefined({
|
|
5064
5474
|
...workflow,
|
|
5475
|
+
nodes: withoutHeavy(workflow.nodes, project.heavy ?? []),
|
|
5065
5476
|
defaults: Object.keys(defaults).length === 0 ? void 0 : defaults,
|
|
5066
5477
|
rules: Object.keys(rules).length === 0 ? void 0 : rules,
|
|
5067
5478
|
layers: Object.keys(layers).length === 0 ? void 0 : layers
|
|
@@ -6046,6 +6457,122 @@ const gatherProblems = (id, node) => {
|
|
|
6046
6457
|
return isList(rule) ? [] : [error("output.gather", `\`${field}\` isn't declared as a list (\`{ type: array }\` or \`{ list: … }\`), so its rounds can't be added together`, [...path, index])];
|
|
6047
6458
|
});
|
|
6048
6459
|
};
|
|
6460
|
+
// ../core/src/graph/item-settings.ts
|
|
6461
|
+
/**
|
|
6462
|
+
* @file A fanned node's `model` and `effort` from its item (`{{item.model}}`):
|
|
6463
|
+
* the compiler's checks (only on a `forEach` node, reading only `item`, and
|
|
6464
|
+
* every written item's value), and the rendering each item's session plan
|
|
6465
|
+
* uses, which checks an item from `from:` when it runs.
|
|
6466
|
+
*/
|
|
6467
|
+
const FIELDS$1 = ["model", "effort"];
|
|
6468
|
+
/**
|
|
6469
|
+
* The parts of a template that reads something, or `null` for a plain value (or
|
|
6470
|
+
* one that does not parse, which the compiler reports).
|
|
6471
|
+
*/
|
|
6472
|
+
const templateOf$1 = (text) => {
|
|
6473
|
+
const parsed = parseTemplate(text);
|
|
6474
|
+
if (!parsed.ok) return null;
|
|
6475
|
+
return parsed.parts.some((part) => typeof part !== "string") ? parsed.parts : null;
|
|
6476
|
+
};
|
|
6477
|
+
const isEffort = (text) => EFFORTS.includes(text);
|
|
6478
|
+
/** Checks what one item's template rendered to. */
|
|
6479
|
+
const checkRendered = (field, template, rendered) => {
|
|
6480
|
+
const text = rendered.trim();
|
|
6481
|
+
if (text === "") return {
|
|
6482
|
+
ok: true,
|
|
6483
|
+
value: void 0
|
|
6484
|
+
};
|
|
6485
|
+
return (field === "effort" ? isEffort(text) : /^[^\s{}"]+$/u.test(text)) ? {
|
|
6486
|
+
ok: true,
|
|
6487
|
+
value: text
|
|
6488
|
+
} : {
|
|
6489
|
+
ok: false,
|
|
6490
|
+
error: `\`${field}: ${template}\` gave \`${text}\`, which is not ${field === "effort" ? `an effort level (${EFFORTS.join(", ")})` : "a model name"}`
|
|
6491
|
+
};
|
|
6492
|
+
};
|
|
6493
|
+
/**
|
|
6494
|
+
* A node's `model` or `effort` for one item: a plain value as written, a
|
|
6495
|
+
* template rendered with the item. Without an item, a template renders empty.
|
|
6496
|
+
*/
|
|
6497
|
+
const itemSetting = (field, value, scope) => {
|
|
6498
|
+
if (value === void 0) return {
|
|
6499
|
+
ok: true,
|
|
6500
|
+
value: void 0
|
|
6501
|
+
};
|
|
6502
|
+
const parts = templateOf$1(value);
|
|
6503
|
+
if (parts === null) return {
|
|
6504
|
+
ok: true,
|
|
6505
|
+
value
|
|
6506
|
+
};
|
|
6507
|
+
return checkRendered(field, value, renderTemplate(parts, scope));
|
|
6508
|
+
};
|
|
6509
|
+
/** `itemSetting` for `effort`, as an effort level. */
|
|
6510
|
+
const itemEffort = (value, scope) => {
|
|
6511
|
+
const given = itemSetting("effort", value, scope);
|
|
6512
|
+
if (!given.ok) return given;
|
|
6513
|
+
if (given.value === void 0) return {
|
|
6514
|
+
ok: true,
|
|
6515
|
+
value: void 0
|
|
6516
|
+
};
|
|
6517
|
+
return isEffort(given.value) ? {
|
|
6518
|
+
ok: true,
|
|
6519
|
+
value: given.value
|
|
6520
|
+
} : {
|
|
6521
|
+
ok: false,
|
|
6522
|
+
error: `\`effort\` is \`${given.value}\``
|
|
6523
|
+
};
|
|
6524
|
+
};
|
|
6525
|
+
/** A `model` or `effort` template's problems on one node. */
|
|
6526
|
+
const checkNodeField = (workflow, says, id, field) => {
|
|
6527
|
+
const node = workflow.nodes[id];
|
|
6528
|
+
const value = node === void 0 ? void 0 : Reflect.get(node, field);
|
|
6529
|
+
if (node === void 0 || typeof value !== "string") return [];
|
|
6530
|
+
const path = [
|
|
6531
|
+
"nodes",
|
|
6532
|
+
id,
|
|
6533
|
+
field
|
|
6534
|
+
];
|
|
6535
|
+
const parsed = parseTemplate(value);
|
|
6536
|
+
if (!parsed.ok) return [error("template.syntax", `${parsed.message} at column ${parsed.offset + 1}`, path)];
|
|
6537
|
+
if (parsed.parts.every((part) => typeof part === "string")) return [];
|
|
6538
|
+
const each = forEachOf$1(node);
|
|
6539
|
+
if (each === void 0) return [error(`${field}.template`, `\`${field}\` may be a template only on a node with \`forEach\`, reading its item, e.g. \`{{item.${field}}}\``, path)];
|
|
6540
|
+
const reads = parsed.parts.flatMap((part) => typeof part === "string" ? [] : collectPaths(part.expr));
|
|
6541
|
+
const outside = reads.filter(([root]) => root !== "item");
|
|
6542
|
+
if (outside.length > 0) return outside.map((segments) => error(`${field}.template`, `\`${describePath(segments)}\`: a \`${field}\` template reads only the item, e.g. \`{{item.${field}}}\``, path));
|
|
6543
|
+
const scope = { fields: itemFieldsOf(node, says) };
|
|
6544
|
+
const fields = reads.flatMap((segments) => checkItemRead(segments, scope, path));
|
|
6545
|
+
if (fields.length > 0 || !("items" in each)) return fields;
|
|
6546
|
+
return each.items.flatMap((item, index) => {
|
|
6547
|
+
const given = checkRendered(field, value, renderTemplate(parsed.parts, {
|
|
6548
|
+
inputs: {},
|
|
6549
|
+
nodes: {},
|
|
6550
|
+
item,
|
|
6551
|
+
index
|
|
6552
|
+
}));
|
|
6553
|
+
return given.ok ? [] : [error(`${field}.item`, `item ${String(index)}: ${given.error}`, [
|
|
6554
|
+
"nodes",
|
|
6555
|
+
id,
|
|
6556
|
+
"forEach",
|
|
6557
|
+
"items",
|
|
6558
|
+
index
|
|
6559
|
+
])];
|
|
6560
|
+
});
|
|
6561
|
+
};
|
|
6562
|
+
/**
|
|
6563
|
+
* `model` and `effort` templates: only on a `forEach` node, reading only
|
|
6564
|
+
* `item`, and every written item's value a model name or an effort level.
|
|
6565
|
+
* `defaults:` belong to no node, so they take no template.
|
|
6566
|
+
*/
|
|
6567
|
+
const checkItemSettings = (workflow, edges) => {
|
|
6568
|
+
const says = nodeSays(workflow.nodes, edges);
|
|
6569
|
+
const defaults = FIELDS$1.flatMap((field) => {
|
|
6570
|
+
const value = workflow.defaults?.[field];
|
|
6571
|
+
return value !== void 0 && templateOf$1(value) !== null ? [error(`${field}.template`, `\`defaults.${field}\` applies to every node, so it can't read an item; set the template on the \`forEach\` node`, ["defaults", field])] : [];
|
|
6572
|
+
});
|
|
6573
|
+
const nodes = Object.entries(workflow.nodes).flatMap(([id, node]) => isClaudeNode(node) ? FIELDS$1.flatMap((field) => checkNodeField(workflow, says, id, field)) : []);
|
|
6574
|
+
return [...defaults, ...nodes];
|
|
6575
|
+
};
|
|
6049
6576
|
// ../core/src/graph/script.ts
|
|
6050
6577
|
/**
|
|
6051
6578
|
* @file Checks on the programs a workflow declares (RFC-006): a tool node's and
|
|
@@ -6111,7 +6638,7 @@ const statusProblems = (id, node) => {
|
|
|
6111
6638
|
};
|
|
6112
6639
|
/** Per-type checks that need the rest of the graph. */
|
|
6113
6640
|
const checkNodes = (workflow, edges) => {
|
|
6114
|
-
const diagnostics = checkScripts(workflow);
|
|
6641
|
+
const diagnostics = [...checkScripts(workflow), ...checkItemSettings(workflow, edges)];
|
|
6115
6642
|
for (const [id, node] of Object.entries(workflow.nodes)) {
|
|
6116
6643
|
const path = ["nodes", id];
|
|
6117
6644
|
const incoming = edges.filter((e) => e.to === id);
|
|
@@ -6646,6 +7173,140 @@ const compileWorkflow = (authored, options = {}) => {
|
|
|
6646
7173
|
diagnostics
|
|
6647
7174
|
};
|
|
6648
7175
|
};
|
|
7176
|
+
// ../core/src/machine/priority.ts
|
|
7177
|
+
/**
|
|
7178
|
+
* @file Starting a child at the project's priority (`priority:`): `low`, the
|
|
7179
|
+
* default, starts it below Ork's own (under `nice`; on macOS also utility QoS
|
|
7180
|
+
* through `taskpolicy`), so the user's own apps come first; `normal` leaves
|
|
7181
|
+
* it. Only children: the `ork` process itself stays at normal priority.
|
|
7182
|
+
*/
|
|
7183
|
+
/** MacOS's tool for a program's QoS. */
|
|
7184
|
+
const TASKPOLICY = "/usr/sbin/taskpolicy";
|
|
7185
|
+
/** The POSIX tool that starts a program at a lower priority. */
|
|
7186
|
+
const NICE = "/usr/bin/nice";
|
|
7187
|
+
/** How far `nice` lowers a `low` child: the same as `PRIORITY_BELOW_NORMAL`. */
|
|
7188
|
+
const NICE_STEP = String(constants$1.priority.PRIORITY_BELOW_NORMAL);
|
|
7189
|
+
/**
|
|
7190
|
+
* Whether `bin` is a program `spawn` would find, so a wrapper can't hide its
|
|
7191
|
+
* absence.
|
|
7192
|
+
*/
|
|
7193
|
+
const found = (bin, env) => bin.includes("/") ? existsSync(bin) : (env["PATH"] ?? "").split(path.delimiter).some((dir) => dir !== "" && existsSync(path.join(dir, bin)));
|
|
7194
|
+
/**
|
|
7195
|
+
* What to spawn for `bin args` at `priority`. For `low`, when the program is
|
|
7196
|
+
* there: under `nice`, so it and what it starts run lower from the first
|
|
7197
|
+
* instruction, and on macOS under utility QoS as well; else the program itself,
|
|
7198
|
+
* which {@link lowerChild} lowers once it has started.
|
|
7199
|
+
*/
|
|
7200
|
+
const startAt = (priority, bin, args, env = process.env) => {
|
|
7201
|
+
if (priority !== "low" || !found(bin, env)) return [bin, [...args]];
|
|
7202
|
+
const niced = existsSync(NICE) ? [
|
|
7203
|
+
NICE,
|
|
7204
|
+
"-n",
|
|
7205
|
+
NICE_STEP,
|
|
7206
|
+
bin
|
|
7207
|
+
] : [bin];
|
|
7208
|
+
const [first = bin, ...rest] = process.platform === "darwin" && existsSync(TASKPOLICY) ? [
|
|
7209
|
+
TASKPOLICY,
|
|
7210
|
+
"-c",
|
|
7211
|
+
"utility",
|
|
7212
|
+
...niced
|
|
7213
|
+
] : niced;
|
|
7214
|
+
return [first, [...rest, ...args]];
|
|
7215
|
+
};
|
|
7216
|
+
/**
|
|
7217
|
+
* Lowers a started child's priority for `low` where {@link startAt} had no
|
|
7218
|
+
* `nice` to start it under (Windows), best effort; what it starts later
|
|
7219
|
+
* inherits it. Under `nice` it does nothing: `nice` lowers relative to the
|
|
7220
|
+
* priority it finds, so lowering here too could land first and the two add up.
|
|
7221
|
+
*/
|
|
7222
|
+
const lowerChild = (priority, pid) => {
|
|
7223
|
+
if (priority !== "low" || pid === void 0 || existsSync(NICE)) return;
|
|
7224
|
+
try {
|
|
7225
|
+
setPriority(pid, constants$1.priority.PRIORITY_BELOW_NORMAL);
|
|
7226
|
+
} catch {}
|
|
7227
|
+
};
|
|
7228
|
+
// ../core/src/run/machine.ts
|
|
7229
|
+
/**
|
|
7230
|
+
* @file A node's share of the machine (the `machine` of its context): the
|
|
7231
|
+
* project's priority for its commands, and a machine-wide slot around each
|
|
7232
|
+
* heavy one. A wait is never silent: `slot.waiting` names who holds the
|
|
7233
|
+
* slots, `slot.taken` ends it, and the node's clock (its owners' and the
|
|
7234
|
+
* run's too) stands still meanwhile, so a timeout counts from the slot.
|
|
7235
|
+
*/
|
|
7236
|
+
/** A wait for a slot, as `ork logs`, `ork tree` and the UI say it. */
|
|
7237
|
+
const slotWaitLine = (holders) => holders.length === 0 ? "waiting for a check slot" : `waiting for a check slot, held by ${holders.join(", ")}`;
|
|
7238
|
+
const none = () => {};
|
|
7239
|
+
/** The port for one activation; none for a run that shares nothing. */
|
|
7240
|
+
const machinePort = (options, runId, id, clock) => {
|
|
7241
|
+
const { machine } = options;
|
|
7242
|
+
if (machine === void 0) return void 0;
|
|
7243
|
+
const { rules, ledger } = machine;
|
|
7244
|
+
return {
|
|
7245
|
+
priority: rules.priority,
|
|
7246
|
+
slot: async (command, signal) => {
|
|
7247
|
+
if (ledger === void 0 || !isHeavy(rules.heavy, command)) return none;
|
|
7248
|
+
let waited = false;
|
|
7249
|
+
const release = await clock.waitFor(id, () => ledger.take({
|
|
7250
|
+
run: runId,
|
|
7251
|
+
node: id,
|
|
7252
|
+
pid: process.pid
|
|
7253
|
+
}, {
|
|
7254
|
+
signal,
|
|
7255
|
+
onWait: (holders) => {
|
|
7256
|
+
waited = true;
|
|
7257
|
+
options.onEvent({
|
|
7258
|
+
type: "slot.waiting",
|
|
7259
|
+
id,
|
|
7260
|
+
holders: holders.map((held) => `${held.run}/${held.node}`)
|
|
7261
|
+
});
|
|
7262
|
+
}
|
|
7263
|
+
}));
|
|
7264
|
+
if (release === null) return none;
|
|
7265
|
+
if (waited) options.onEvent({
|
|
7266
|
+
type: "slot.taken",
|
|
7267
|
+
id
|
|
7268
|
+
});
|
|
7269
|
+
return release;
|
|
7270
|
+
}
|
|
7271
|
+
};
|
|
7272
|
+
};
|
|
7273
|
+
/**
|
|
7274
|
+
* A step's clock that also stands still while its command waits for a slot, and
|
|
7275
|
+
* the machine port that tells it so; for a step's own timeout.
|
|
7276
|
+
*/
|
|
7277
|
+
const slotClock = (base, machine) => {
|
|
7278
|
+
let waiting = 0;
|
|
7279
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
7280
|
+
const tell = () => {
|
|
7281
|
+
for (const listener of listeners) listener();
|
|
7282
|
+
};
|
|
7283
|
+
return {
|
|
7284
|
+
clock: {
|
|
7285
|
+
held: () => waiting > 0 || base?.held() === true,
|
|
7286
|
+
watch: (listener) => {
|
|
7287
|
+
listeners.add(listener);
|
|
7288
|
+
const unwatch = base?.watch(listener);
|
|
7289
|
+
return () => {
|
|
7290
|
+
listeners.delete(listener);
|
|
7291
|
+
unwatch?.();
|
|
7292
|
+
};
|
|
7293
|
+
}
|
|
7294
|
+
},
|
|
7295
|
+
machine: machine === void 0 ? void 0 : {
|
|
7296
|
+
...machine,
|
|
7297
|
+
slot: async (command, signal) => {
|
|
7298
|
+
waiting += 1;
|
|
7299
|
+
tell();
|
|
7300
|
+
try {
|
|
7301
|
+
return await machine.slot(command, signal);
|
|
7302
|
+
} finally {
|
|
7303
|
+
waiting -= 1;
|
|
7304
|
+
tell();
|
|
7305
|
+
}
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7308
|
+
};
|
|
7309
|
+
};
|
|
6649
7310
|
// ../core/src/project/calls.ts
|
|
6650
7311
|
/**
|
|
6651
7312
|
* @file `ork validate`'s check of `workflow.call` nodes against what they call
|
|
@@ -7055,6 +7716,66 @@ const createLoops = (options) => {
|
|
|
7055
7716
|
overList
|
|
7056
7717
|
};
|
|
7057
7718
|
};
|
|
7719
|
+
// ../core/src/run/ended.ts
|
|
7720
|
+
/**
|
|
7721
|
+
* @file A node's result as it ended (specs/runtime/session-ends.md): one place
|
|
7722
|
+
* that sets `end`, makes a failure's `error` its message and marks a
|
|
7723
|
+
* transient kind, and that the engine's modules build ends through.
|
|
7724
|
+
*/
|
|
7725
|
+
/**
|
|
7726
|
+
* The result as it ended: a failure's `error` is the end's message (what the
|
|
7727
|
+
* run summary lists), and a transient kind marks it `transient`.
|
|
7728
|
+
*/
|
|
7729
|
+
const endedWith = (result, end) => ({
|
|
7730
|
+
...result,
|
|
7731
|
+
end,
|
|
7732
|
+
...result.ok ? {} : { error: end.message },
|
|
7733
|
+
...END_KINDS[end.kind].transient ? { transient: true } : {}
|
|
7734
|
+
});
|
|
7735
|
+
/** The reason a run's signal is aborted with when `timeout` passed. */
|
|
7736
|
+
const runTimedOut = (timeout) => timeout === void 0 ? { kind: "timed_out" } : {
|
|
7737
|
+
kind: "timed_out",
|
|
7738
|
+
timeout
|
|
7739
|
+
};
|
|
7740
|
+
const isTimedOut = (reason) => typeof reason === "object" && reason !== null && "kind" in reason && reason.kind === "timed_out";
|
|
7741
|
+
/**
|
|
7742
|
+
* The end of a node its run's signal stopped: `timed_out` when the run's
|
|
7743
|
+
* timeout aborted it, `interrupted` otherwise.
|
|
7744
|
+
*/
|
|
7745
|
+
const stoppedEnd = (signal, detail) => {
|
|
7746
|
+
const reason = signal.reason;
|
|
7747
|
+
if (!isTimedOut(reason)) return endFor("interrupted", {}, detail);
|
|
7748
|
+
const facts = reason.timeout === void 0 ? {} : { timeout: reason.timeout };
|
|
7749
|
+
return endFor("timed_out", facts, detail);
|
|
7750
|
+
};
|
|
7751
|
+
/** A failure that ended as `end`, its message the output's summary. */
|
|
7752
|
+
const failedWith = (end, output) => endedWith({
|
|
7753
|
+
ok: false,
|
|
7754
|
+
output: output ?? {
|
|
7755
|
+
status: "fail",
|
|
7756
|
+
summary: end.message
|
|
7757
|
+
}
|
|
7758
|
+
}, end);
|
|
7759
|
+
/**
|
|
7760
|
+
* The result with its end's detail scrubbed with the feedback redactor (the
|
|
7761
|
+
* run's `roots` and the home folder), as a session's is
|
|
7762
|
+
* (specs/runtime/session-ends.md), whichever node type set it.
|
|
7763
|
+
*/
|
|
7764
|
+
const scrubbedEnd = (result, roots) => {
|
|
7765
|
+
const { end } = result;
|
|
7766
|
+
if (end?.detail === void 0) return result;
|
|
7767
|
+
const detail = redact(end.detail, {
|
|
7768
|
+
roots,
|
|
7769
|
+
home: homedir()
|
|
7770
|
+
});
|
|
7771
|
+
return {
|
|
7772
|
+
...result,
|
|
7773
|
+
end: {
|
|
7774
|
+
...end,
|
|
7775
|
+
detail
|
|
7776
|
+
}
|
|
7777
|
+
};
|
|
7778
|
+
};
|
|
7058
7779
|
// ../core/src/run/declared.ts
|
|
7059
7780
|
/**
|
|
7060
7781
|
* @file Holding a node to what it declares of its output (RFC-010): a missing
|
|
@@ -7127,903 +7848,923 @@ const declaredProblem = (declared, value) => {
|
|
|
7127
7848
|
* still masks to the record is the file's, with its real values.
|
|
7128
7849
|
*/
|
|
7129
7850
|
const unmasked = (forEach, recorded) => "items" in forEach && JSON.stringify(forEach.items, maskReplacer) === JSON.stringify(recorded) ? [...forEach.items] : recorded;
|
|
7130
|
-
// ../core/src/run/
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
/**
|
|
7139
|
-
const
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
parent: node.parent
|
|
7146
|
-
});
|
|
7147
|
-
/** Whom a session may write to, by how it knows them. */
|
|
7148
|
-
const contactsOf = (id, nodes, channels) => {
|
|
7149
|
-
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
7851
|
+
// ../core/src/run/slots.ts
|
|
7852
|
+
/**
|
|
7853
|
+
* @file Session slots: at most `limits.concurrency` Claude sessions run at
|
|
7854
|
+
* once. Queued activations take a free slot when the scheduler pumps;
|
|
7855
|
+
* delegated children and leaders returning from `delegate` wait for one and
|
|
7856
|
+
* are served first, so a leader waiting on its children never holds a slot
|
|
7857
|
+
* they need.
|
|
7858
|
+
*/
|
|
7859
|
+
/** Only a Claude session takes a slot; tools and gates are not limited. */
|
|
7860
|
+
const needsSlot = (node) => isClaudeNode(node);
|
|
7861
|
+
/** A call spends through its run, so it needs budget as a session does. */
|
|
7862
|
+
const spendsBudget = (node) => needsSlot(node) || node.type === "workflow.call";
|
|
7863
|
+
const createSlots = (limit, onFree) => {
|
|
7864
|
+
let used = 0;
|
|
7865
|
+
const waiting = [];
|
|
7150
7866
|
return {
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7867
|
+
limit,
|
|
7868
|
+
tryTake: () => {
|
|
7869
|
+
if (used >= limit || waiting.length > 0) return false;
|
|
7870
|
+
used += 1;
|
|
7871
|
+
return true;
|
|
7872
|
+
},
|
|
7873
|
+
take: () => {
|
|
7874
|
+
if (used < limit && waiting.length === 0) {
|
|
7875
|
+
used += 1;
|
|
7876
|
+
return Promise.resolve();
|
|
7877
|
+
}
|
|
7878
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
7879
|
+
waiting.push(resolve);
|
|
7880
|
+
return promise;
|
|
7881
|
+
},
|
|
7882
|
+
release: () => {
|
|
7883
|
+
const next = waiting.shift();
|
|
7884
|
+
if (next === void 0) {
|
|
7885
|
+
used -= 1;
|
|
7886
|
+
onFree();
|
|
7887
|
+
} else next();
|
|
7888
|
+
}
|
|
7154
7889
|
};
|
|
7155
7890
|
};
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7891
|
+
// ../core/src/run/fan-out.ts
|
|
7892
|
+
/** The node's `forEach`, whatever its type; `undefined` when it has none. */
|
|
7893
|
+
const forEachOf = (node) => node !== void 0 && "forEach" in node ? node.forEach : void 0;
|
|
7894
|
+
/** The list a node runs over, or why there isn't one. */
|
|
7895
|
+
const listOf = (forEach, scope) => {
|
|
7896
|
+
if ("items" in forEach) return {
|
|
7897
|
+
ok: true,
|
|
7898
|
+
list: forEach.items
|
|
7899
|
+
};
|
|
7900
|
+
const read = templateValue(forEach.from, scope);
|
|
7901
|
+
if (!read.ok) return read;
|
|
7902
|
+
const { value } = read;
|
|
7903
|
+
if (Array.isArray(value)) return {
|
|
7904
|
+
ok: true,
|
|
7905
|
+
list: value
|
|
7906
|
+
};
|
|
7907
|
+
const name = String(templatePath(forEach.from)?.at(-1) ?? forEach.from);
|
|
7908
|
+
return {
|
|
7909
|
+
ok: false,
|
|
7910
|
+
error: declaredProblem({ [name]: { list: {} } }, { [name]: value }) ?? `${name} is not a list`
|
|
7911
|
+
};
|
|
7161
7912
|
};
|
|
7162
|
-
/**
|
|
7163
|
-
const
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
if (total <= 0) return 0;
|
|
7168
|
-
const before = reported.get(key) ?? 0;
|
|
7169
|
-
return total < before ? total : total - before;
|
|
7913
|
+
/** The output a `from` list comes from, whose freshness resume asks about. */
|
|
7914
|
+
const sourceOf = (forEach, scope) => {
|
|
7915
|
+
if (!("from" in forEach)) return void 0;
|
|
7916
|
+
const [root, id] = templatePath(forEach.from) ?? [];
|
|
7917
|
+
return root === "nodes" ? scope.nodes[String(id)]?.output : void 0;
|
|
7170
7918
|
};
|
|
7919
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7171
7920
|
/**
|
|
7172
|
-
* What
|
|
7173
|
-
* `
|
|
7174
|
-
*
|
|
7921
|
+
* What `results` keeps of an item: its declared fields (a program's are under
|
|
7922
|
+
* `json`), its summary, Ork's own branch and worktree, and how a program
|
|
7923
|
+
* failed. The full output stays in the item's own `node.finished`.
|
|
7175
7924
|
*/
|
|
7176
|
-
const
|
|
7177
|
-
const
|
|
7178
|
-
|
|
7179
|
-
|
|
7925
|
+
const entryOf = (declared, { ok, output, error }) => {
|
|
7926
|
+
const json = isRecord$1(output["json"]) ? output["json"] : {};
|
|
7927
|
+
const fields = Object.keys(declared ?? {}).map((field) => [field, Object.hasOwn(json, field) ? json[field] : output[field]]);
|
|
7928
|
+
const own = [
|
|
7929
|
+
"summary",
|
|
7930
|
+
"branch",
|
|
7931
|
+
"worktree",
|
|
7932
|
+
"merge"
|
|
7933
|
+
].map((field) => [field, output[field]]);
|
|
7934
|
+
const failed = !ok && typeof output["exitCode"] === "number" ? [["exitCode", output["exitCode"]], ["error", error]] : [];
|
|
7935
|
+
return Object.fromEntries([
|
|
7936
|
+
...fields,
|
|
7937
|
+
...own,
|
|
7938
|
+
...failed
|
|
7939
|
+
].filter(([, value]) => value !== void 0));
|
|
7940
|
+
};
|
|
7941
|
+
/** Whether an item passed but its work could not reach its owner. */
|
|
7942
|
+
const mergeFailed = ({ output }) => isRecord$1(output["merge"]) && output["merge"]["ok"] === false;
|
|
7943
|
+
const statusOf$2 = (result) => {
|
|
7944
|
+
if (mergeFailed(result)) return "fail";
|
|
7945
|
+
const { ok, output } = result;
|
|
7946
|
+
const status = output["status"];
|
|
7947
|
+
if (typeof status === "string") return status;
|
|
7948
|
+
return ok ? "pass" : "fail";
|
|
7949
|
+
};
|
|
7950
|
+
/** Why a fan-out did not succeed, naming the first item that failed. */
|
|
7951
|
+
const whyNot = (count, failures, skipped) => {
|
|
7952
|
+
const parts = [`${String(failures.length)} of ${String(count)} items failed`];
|
|
7953
|
+
const [first] = failures;
|
|
7954
|
+
if (first !== void 0) parts[0] = `${parts[0] ?? ""}; first, ${first}`;
|
|
7955
|
+
if (skipped > 0) parts.push(`${String(skipped)} not started`);
|
|
7956
|
+
return parts.join("; ");
|
|
7180
7957
|
};
|
|
7181
7958
|
/**
|
|
7182
|
-
*
|
|
7183
|
-
*
|
|
7184
|
-
* `ork chat` made it outside any node: a session cut off before its node
|
|
7185
|
-
* finished has recorded nothing, so its next total counts the part that was cut
|
|
7186
|
-
* off. A leader's fork is never resumed, so its report counts when it comes,
|
|
7187
|
-
* whether or not its leader finished.
|
|
7959
|
+
* Waits until the item may start: not while it is held, and without keeping a
|
|
7960
|
+
* session slot through a pause, which would stop other nodes.
|
|
7188
7961
|
*/
|
|
7189
|
-
const
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
total += event.usage?.costUsd ?? 0;
|
|
7197
|
-
const last = open.get(event.id);
|
|
7198
|
-
if (last !== null && last !== void 0) ownSpend(reported, last.key, last.cost);
|
|
7199
|
-
open.delete(event.id);
|
|
7200
|
-
} else if (event.type === "node.event" && event.event.type === "session.finished") {
|
|
7201
|
-
const { sessionId, usage } = event.event.outcome;
|
|
7202
|
-
const key = `${event.id}/${sessionId}`;
|
|
7203
|
-
if (event.event.fork === true) {
|
|
7204
|
-
const own = ownSpend(reported, key, usage.costUsd);
|
|
7205
|
-
total += own;
|
|
7206
|
-
forks.set(event.id, (forks.get(event.id) ?? 0) + own);
|
|
7207
|
-
} else if (open.has(event.id)) open.set(event.id, {
|
|
7208
|
-
key,
|
|
7209
|
-
cost: usage.costUsd
|
|
7210
|
-
});
|
|
7211
|
-
else ownSpend(reported, key, usage.costUsd);
|
|
7962
|
+
const awaitTurn = async (hold, slots, signal) => {
|
|
7963
|
+
for (;;) {
|
|
7964
|
+
await hold.wait("start", signal);
|
|
7965
|
+
if (slots === null) return;
|
|
7966
|
+
await slots.take();
|
|
7967
|
+
if (!hold.held() || signal.aborted) return;
|
|
7968
|
+
slots.release();
|
|
7212
7969
|
}
|
|
7213
|
-
return {
|
|
7214
|
-
total,
|
|
7215
|
-
reported,
|
|
7216
|
-
forks
|
|
7217
|
-
};
|
|
7218
7970
|
};
|
|
7219
|
-
|
|
7220
|
-
const
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
const
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
usd,
|
|
7240
|
-
waitable
|
|
7971
|
+
const createFanOut = (deps) => {
|
|
7972
|
+
const { holds, slots, stops, nodeOf, perform, options } = deps;
|
|
7973
|
+
return async (activation, forEach, round) => {
|
|
7974
|
+
const { id, input } = activation;
|
|
7975
|
+
const scope = deps.scope();
|
|
7976
|
+
const recorded = options.replay?.listed?.(id, round, sourceOf(forEach, scope)) ?? null;
|
|
7977
|
+
const listed = recorded === null ? listOf(forEach, scope) : {
|
|
7978
|
+
ok: true,
|
|
7979
|
+
list: unmasked(forEach, recorded)
|
|
7980
|
+
};
|
|
7981
|
+
if (!listed.ok) return failedWith(endFor("invalid_node", { reason: listed.error }));
|
|
7982
|
+
let { list } = listed;
|
|
7983
|
+
const recordList = () => {
|
|
7984
|
+
options.onEvent({
|
|
7985
|
+
type: "each.listed",
|
|
7986
|
+
node: id,
|
|
7987
|
+
round,
|
|
7988
|
+
count: list.length,
|
|
7989
|
+
list,
|
|
7990
|
+
...recorded === null && options.replay !== void 0 ? { anew: true } : {}
|
|
7241
7991
|
});
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
freed: (id, signal) => {
|
|
7253
|
-
if (![...held].some(([other, reserve]) => other !== id && reserve.waitable) || signal.aborted) return Promise.resolve(false);
|
|
7254
|
-
return new Promise((resolve) => {
|
|
7255
|
-
const done = (freed) => () => {
|
|
7256
|
-
waiters.delete(onFree);
|
|
7257
|
-
signal.removeEventListener("abort", onAbort);
|
|
7258
|
-
resolve(freed);
|
|
7259
|
-
};
|
|
7260
|
-
const onFree = done(true);
|
|
7261
|
-
const onAbort = done(false);
|
|
7262
|
-
waiters.add(onFree);
|
|
7263
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
7992
|
+
};
|
|
7993
|
+
recordList();
|
|
7994
|
+
const signal = stops.signalFor(id);
|
|
7995
|
+
if ("maxItems" in forEach && list.length > forEach.maxItems) {
|
|
7996
|
+
options.onEvent({
|
|
7997
|
+
type: "each.exceeded",
|
|
7998
|
+
node: id,
|
|
7999
|
+
round,
|
|
8000
|
+
count: list.length,
|
|
8001
|
+
maxItems: forEach.maxItems
|
|
7264
8002
|
});
|
|
8003
|
+
const chosen = await deps.loops.overList(id, list, forEach.maxItems, round, signal);
|
|
8004
|
+
if (chosen === null) {
|
|
8005
|
+
const reason = `the list of ${id} has ${String(list.length)} items, over maxItems ${String(forEach.maxItems)}`;
|
|
8006
|
+
return stops.ended(id, {
|
|
8007
|
+
...failedWith(endFor("invalid_node", { reason }), {
|
|
8008
|
+
status: "fail",
|
|
8009
|
+
summary: reason
|
|
8010
|
+
}),
|
|
8011
|
+
limited: reason,
|
|
8012
|
+
stopsAs: "failed"
|
|
8013
|
+
});
|
|
8014
|
+
}
|
|
8015
|
+
const narrowed = chosen.length !== list.length;
|
|
8016
|
+
list = chosen;
|
|
8017
|
+
if (narrowed) recordList();
|
|
7265
8018
|
}
|
|
8019
|
+
const node = nodeOf(activation);
|
|
8020
|
+
const session = node !== void 0 && needsSlot(node);
|
|
8021
|
+
const spends = node !== void 0 && spendsBudget(node);
|
|
8022
|
+
const declared = node === void 0 ? void 0 : declaredOutputOf(node);
|
|
8023
|
+
const results = list.map(() => ({}));
|
|
8024
|
+
const statuses = list.map(() => "skipped");
|
|
8025
|
+
const failures = [];
|
|
8026
|
+
/** How the first item that failed ended: the fanned node's own end. */
|
|
8027
|
+
let firstEnd;
|
|
8028
|
+
const mayStart = () => !signal.aborted && !deps.isStopped() && (forEach.onFailure !== "stop" || failures.length === 0);
|
|
8029
|
+
const runItem = async (index) => {
|
|
8030
|
+
const itemId = `${id}.${String(index)}`;
|
|
8031
|
+
const hold = holds.forNode(itemId, id);
|
|
8032
|
+
await awaitTurn(hold, session ? slots : null, signal);
|
|
8033
|
+
try {
|
|
8034
|
+
if (!mayStart() || spends && deps.budgetSpent()) return;
|
|
8035
|
+
const result = await perform({
|
|
8036
|
+
id: itemId,
|
|
8037
|
+
input,
|
|
8038
|
+
template: id,
|
|
8039
|
+
item: {
|
|
8040
|
+
of: id,
|
|
8041
|
+
index,
|
|
8042
|
+
value: list[index]
|
|
8043
|
+
},
|
|
8044
|
+
round
|
|
8045
|
+
});
|
|
8046
|
+
results[index] = entryOf(declared, result);
|
|
8047
|
+
statuses[index] = statusOf$2(result);
|
|
8048
|
+
if (!result.ok) {
|
|
8049
|
+
firstEnd ??= endOf(result);
|
|
8050
|
+
failures.push(`${itemId}: ${result.error ?? "failed"}`);
|
|
8051
|
+
} else if (mergeFailed(result)) failures.push(`${itemId}: its work could not be merged`);
|
|
8052
|
+
} finally {
|
|
8053
|
+
if (session) slots.release();
|
|
8054
|
+
}
|
|
8055
|
+
};
|
|
8056
|
+
let next = 0;
|
|
8057
|
+
const worker = async () => {
|
|
8058
|
+
while (next < list.length && mayStart()) {
|
|
8059
|
+
const index = next;
|
|
8060
|
+
next += 1;
|
|
8061
|
+
await runItem(index);
|
|
8062
|
+
}
|
|
8063
|
+
};
|
|
8064
|
+
const lanes = Math.min(forEach.maxParallel ?? slots.limit, list.length);
|
|
8065
|
+
await Promise.all(Array.from({ length: lanes }, () => worker()));
|
|
8066
|
+
const skipped = statuses.filter((status) => status === "skipped").length;
|
|
8067
|
+
const ok = failures.length === 0 && skipped === 0;
|
|
8068
|
+
const gathered = {
|
|
8069
|
+
ok,
|
|
8070
|
+
output: {
|
|
8071
|
+
items: list,
|
|
8072
|
+
results,
|
|
8073
|
+
statuses,
|
|
8074
|
+
count: list.length
|
|
8075
|
+
},
|
|
8076
|
+
...ok ? {} : { error: whyNot(list.length, failures, skipped) },
|
|
8077
|
+
...ok || firstEnd === void 0 ? {} : { end: firstEnd },
|
|
8078
|
+
...!ok && deps.isStopped() ? { limited: deps.reasons[0] ?? "the run stopped" } : {}
|
|
8079
|
+
};
|
|
8080
|
+
return {
|
|
8081
|
+
...stops.ended(id, gathered),
|
|
8082
|
+
output: gathered.output
|
|
8083
|
+
};
|
|
7266
8084
|
};
|
|
7267
8085
|
};
|
|
7268
|
-
// ../core/src/run/
|
|
7269
|
-
/**
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
usage: own,
|
|
7284
|
-
output: "usage" in result.output ? {
|
|
7285
|
-
...result.output,
|
|
7286
|
-
usage: own
|
|
7287
|
-
} : result.output
|
|
7288
|
-
};
|
|
7289
|
-
};
|
|
7290
|
-
/** A run's spending so far; a resumed run starts from what it had spent. */
|
|
7291
|
-
const createSpend = (budgetUsd, earlier, limits) => {
|
|
7292
|
-
const reported = new Map(earlier?.reported);
|
|
7293
|
-
let spent = earlier?.total ?? 0;
|
|
7294
|
-
/** What running sessions last reported, not yet in `spent`, by `node/session`. */
|
|
7295
|
-
const inFlight = /* @__PURE__ */ new Map();
|
|
7296
|
-
const committed = () => spent + [...inFlight.values()].reduce((sum, cost) => sum + cost, 0);
|
|
7297
|
-
/** Caps running sessions were given (ADR-077): out of what is offered. */
|
|
7298
|
-
const reserves = createReserves();
|
|
7299
|
-
const budgetLeft = () => budgetUsd === void 0 ? void 0 : budgetUsd - committed() - reserves.total();
|
|
7300
|
-
return {
|
|
7301
|
-
total: () => spent,
|
|
7302
|
-
/** A node sets aside its session's cap; nothing or 0 lets it go. */
|
|
7303
|
-
reserve: reserves.reserve,
|
|
7304
|
-
/** Waits until another node's reserve frees; `false` when none can. */
|
|
7305
|
-
freed: reserves.freed,
|
|
7306
|
-
/** What a session has spent in this activation, from its total so far. */
|
|
7307
|
-
ownOf: (id, sessionId, total) => unreported(reported, `${id}/${sessionId}`, total),
|
|
7308
|
-
/** A running session said what it has spent so far. */
|
|
7309
|
-
report: (id, sessionId, total) => {
|
|
7310
|
-
const key = `${id}/${sessionId}`;
|
|
7311
|
-
inFlight.set(key, unreported(reported, key, total));
|
|
7312
|
-
},
|
|
7313
|
-
/** The node ended: `count` has what its sessions last reported. */
|
|
7314
|
-
settled: (id) => {
|
|
7315
|
-
for (const key of inFlight.keys()) if (key.startsWith(`${id}/`)) inFlight.delete(key);
|
|
7316
|
-
reserves.drop(id);
|
|
7317
|
-
},
|
|
7318
|
-
/** What a session may spend: the least of the budget and any quota. */
|
|
7319
|
-
left: () => {
|
|
7320
|
-
const known = [budgetLeft(), limits?.().leftUsd].filter((left) => left !== void 0);
|
|
7321
|
-
return known.length === 0 ? void 0 : Math.min(...known);
|
|
7322
|
-
},
|
|
7323
|
-
budgetLeft,
|
|
7324
|
-
/** What the limits across runs allow now. */
|
|
7325
|
-
quota: () => limits?.() ?? {},
|
|
7326
|
-
/**
|
|
7327
|
-
* What stops anything new from starting: a quota first, as a budget can be
|
|
7328
|
-
* raised.
|
|
7329
|
-
*/
|
|
7330
|
-
spentOut: () => {
|
|
7331
|
-
const blocked = limits?.().blocked;
|
|
7332
|
-
if (blocked !== void 0) return {
|
|
7333
|
-
status: "quota_exhausted",
|
|
7334
|
-
reason: blocked
|
|
7335
|
-
};
|
|
7336
|
-
return budgetUsd !== void 0 && committed() >= budgetUsd ? {
|
|
7337
|
-
status: "budget_exhausted",
|
|
7338
|
-
reason: `the run's budget of $${String(budgetUsd)} is spent`
|
|
7339
|
-
} : null;
|
|
7340
|
-
},
|
|
7341
|
-
/**
|
|
7342
|
-
* Counts a result, and gives it back with only its own spend. A replayed
|
|
7343
|
-
* result is as stored, and already in what the run started from.
|
|
7344
|
-
*/
|
|
7345
|
-
count: (id, result, replayed) => {
|
|
7346
|
-
if (replayed) return result;
|
|
7347
|
-
const own = withOwnSpend(id, result, reported);
|
|
7348
|
-
spent += own.usage?.costUsd ?? 0;
|
|
7349
|
-
return own;
|
|
8086
|
+
// ../core/src/run/delegation.ts
|
|
8087
|
+
/** Node types a leader can hand work to. */
|
|
8088
|
+
const DELEGABLE = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
|
|
8089
|
+
/** A child a leader delegated to: waits for a slot, runs, reports back. */
|
|
8090
|
+
const createRunChild = (hooks) => async (child, brief) => {
|
|
8091
|
+
await hooks.slots.take();
|
|
8092
|
+
try {
|
|
8093
|
+
await hooks.holds.forNode(child.id, child.template).wait("start", hooks.signal);
|
|
8094
|
+
if (hooks.isStopped() || hooks.budgetSpent()) {
|
|
8095
|
+
const why = `not started: ${hooks.reasons[0] ?? "the run stopped"}`;
|
|
8096
|
+
const end = hooks.budgetSpent() ? endFor("budget_limit", {}, why) : stoppedEnd(hooks.signal, why);
|
|
8097
|
+
return failedWith(end, {
|
|
8098
|
+
status: "fail",
|
|
8099
|
+
summary: why
|
|
8100
|
+
});
|
|
7350
8101
|
}
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
const joined = (before, now) => {
|
|
7359
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7360
|
-
const out = [];
|
|
7361
|
-
for (const entry of [...asList(before), ...asList(now)]) {
|
|
7362
|
-
const key = JSON.stringify(entry);
|
|
7363
|
-
if (seen.has(key)) continue;
|
|
7364
|
-
seen.add(key);
|
|
7365
|
-
out.push(entry);
|
|
8102
|
+
return await hooks.perform({
|
|
8103
|
+
...child,
|
|
8104
|
+
input: void 0,
|
|
8105
|
+
brief
|
|
8106
|
+
});
|
|
8107
|
+
} finally {
|
|
8108
|
+
hooks.slots.release();
|
|
7366
8109
|
}
|
|
7367
|
-
return out;
|
|
7368
|
-
};
|
|
7369
|
-
/**
|
|
7370
|
-
* `result` with each gathered field holding every round's entries so far.
|
|
7371
|
-
* `previous` is the node's output from its last round, already gathered.
|
|
7372
|
-
*/
|
|
7373
|
-
const joinRounds = (fields, previous, result) => {
|
|
7374
|
-
if (fields.length === 0 || previous === void 0) return result;
|
|
7375
|
-
const output = { ...result.output };
|
|
7376
|
-
for (const field of fields) output[field] = joined(previous[field], result.output[field]);
|
|
7377
|
-
return {
|
|
7378
|
-
...result,
|
|
7379
|
-
output
|
|
7380
|
-
};
|
|
7381
|
-
};
|
|
7382
|
-
/** `result` with `node`'s `output.gather` fields joined to its earlier rounds. */
|
|
7383
|
-
const gatherRounds = (node, previous, result) => joinRounds(gatheredFieldsOf(node), previous, result);
|
|
7384
|
-
/**
|
|
7385
|
-
* What `activation`'s rounds reported so far, to gather onto. A spawned
|
|
7386
|
-
* instance gathers nothing earlier: its id (`impl-2`) is reused by a different
|
|
7387
|
-
* worker in the leader's next round. A delegated child that hasn't run since a
|
|
7388
|
-
* resume starts from what it reported before it, since its leader's recorded
|
|
7389
|
-
* rounds are handed back whole and never run it again.
|
|
7390
|
-
*/
|
|
7391
|
-
const previousOf = (activation, current, replay) => {
|
|
7392
|
-
const { brief, template, id } = activation;
|
|
7393
|
-
if (brief !== void 0 && template !== void 0) return void 0;
|
|
7394
|
-
if (current !== void 0 || brief === void 0) return current;
|
|
7395
|
-
return replay?.lastOutput?.(id);
|
|
7396
|
-
};
|
|
7397
|
-
/** Gathers each activation's result onto what its node reported before. */
|
|
7398
|
-
const createGatherer = (outputs, replay, nodeOf) => (activation, result) => gatherRounds(nodeOf(activation), previousOf(activation, outputs[activation.id], replay), result);
|
|
7399
|
-
// ../core/src/run/results.ts
|
|
7400
|
-
/** Control nodes pass their input through; a join's input is its output. */
|
|
7401
|
-
const passThrough = (input) => ({
|
|
7402
|
-
output: input ?? {},
|
|
7403
|
-
ok: true
|
|
7404
|
-
});
|
|
7405
|
-
const recordRun = (entry, result) => {
|
|
7406
|
-
entry.runs += 1;
|
|
7407
|
-
entry.ok = result.ok;
|
|
7408
|
-
entry.output = result.output;
|
|
7409
|
-
entry.costUsd += result.usage?.costUsd ?? 0;
|
|
7410
|
-
if (result.error === void 0) delete entry.error;
|
|
7411
|
-
else entry.error = result.error;
|
|
7412
|
-
};
|
|
7413
|
-
const startedEvent = (activation, nodeType, round, agent) => {
|
|
7414
|
-
const event = {
|
|
7415
|
-
type: "node.started",
|
|
7416
|
-
id: activation.id,
|
|
7417
|
-
nodeType,
|
|
7418
|
-
round
|
|
7419
|
-
};
|
|
7420
|
-
if (activation.brief !== void 0) event.owner = activation.brief.from;
|
|
7421
|
-
if (agent !== void 0) event.agent = agent;
|
|
7422
|
-
const { item } = activation;
|
|
7423
|
-
if (item !== void 0) event.item = {
|
|
7424
|
-
of: item.of,
|
|
7425
|
-
index: item.index
|
|
7426
|
-
};
|
|
7427
|
-
return event;
|
|
7428
|
-
};
|
|
7429
|
-
const finishedEvent = (id, round, result) => {
|
|
7430
|
-
const event = {
|
|
7431
|
-
type: "node.finished",
|
|
7432
|
-
id,
|
|
7433
|
-
round,
|
|
7434
|
-
ok: result.ok,
|
|
7435
|
-
output: result.output
|
|
7436
|
-
};
|
|
7437
|
-
if (result.usage !== void 0) event.usage = result.usage;
|
|
7438
|
-
if (result.error !== void 0) event.error = result.error;
|
|
7439
|
-
return event;
|
|
7440
8110
|
};
|
|
7441
|
-
const
|
|
8111
|
+
const rejected = (child, error) => ({
|
|
8112
|
+
child,
|
|
8113
|
+
ok: false,
|
|
7442
8114
|
output: {
|
|
7443
8115
|
status: "fail",
|
|
7444
8116
|
summary: error
|
|
7445
8117
|
},
|
|
7446
|
-
ok: false,
|
|
7447
8118
|
error
|
|
7448
8119
|
});
|
|
7449
|
-
|
|
7450
|
-
const
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
id,
|
|
7455
|
-
runs: 0,
|
|
7456
|
-
ok: null,
|
|
7457
|
-
output: null,
|
|
7458
|
-
costUsd: 0
|
|
8120
|
+
const reportOf = (child, result) => {
|
|
8121
|
+
const report = {
|
|
8122
|
+
child,
|
|
8123
|
+
ok: result.ok,
|
|
8124
|
+
output: result.output
|
|
7459
8125
|
};
|
|
7460
|
-
|
|
7461
|
-
return
|
|
8126
|
+
if (result.error !== void 0) report.error = result.error;
|
|
8127
|
+
return report;
|
|
7462
8128
|
};
|
|
7463
|
-
|
|
7464
|
-
const
|
|
7465
|
-
const
|
|
7466
|
-
const
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
8129
|
+
const createOwnership = (hooks) => {
|
|
8130
|
+
const { graph, leader } = hooks;
|
|
8131
|
+
const owner = graph.nodes.get(leader);
|
|
8132
|
+
const isFanned = (id) => forEachOf(graph.nodes.get(id)?.node) !== void 0;
|
|
8133
|
+
const children = (owner?.children ?? []).flatMap((id) => {
|
|
8134
|
+
const node = graph.nodes.get(id)?.node;
|
|
8135
|
+
if (node === void 0 || !DELEGABLE.has(node.type) || isFanned(id)) return [];
|
|
8136
|
+
const child = {
|
|
8137
|
+
id,
|
|
8138
|
+
type: node.type
|
|
8139
|
+
};
|
|
8140
|
+
if (node.description !== void 0) child.description = node.description;
|
|
8141
|
+
return [child];
|
|
8142
|
+
});
|
|
8143
|
+
const templates = children.filter((c) => c.type === "claude.worker").map((c) => c.id);
|
|
8144
|
+
const leaderNode = owner?.node;
|
|
8145
|
+
const maxWorkers = leaderNode?.type === "claude.leader" ? leaderNode.maxWorkers ?? 0 : 0;
|
|
8146
|
+
const spawned = [];
|
|
8147
|
+
/** Runs the accepted jobs in parallel; the slot goes to them meanwhile. */
|
|
8148
|
+
const runJobs = async (jobs) => {
|
|
8149
|
+
const accepted = jobs.filter((j) => "brief" in j);
|
|
8150
|
+
const notes = [];
|
|
8151
|
+
if (accepted.length > 0) {
|
|
8152
|
+
const problem = await hooks.checkpoint();
|
|
8153
|
+
if (problem !== null) notes.push(`Your uncommitted work could not be committed (${problem}); the children started from your branch's last commit.`);
|
|
8154
|
+
const released = hooks.releaseClaims().map((r) => `\`${r}\``);
|
|
8155
|
+
if (released.length > 0) notes.push(`Your claims on ${released.join(", ")} were released while your children worked; claim again before you edit.`);
|
|
8156
|
+
}
|
|
8157
|
+
hooks.lend();
|
|
8158
|
+
try {
|
|
8159
|
+
return {
|
|
8160
|
+
reports: await Promise.all(jobs.map(async (job) => {
|
|
8161
|
+
if (!("brief" in job)) return job;
|
|
8162
|
+
const child = { id: job.id };
|
|
8163
|
+
if (job.template !== void 0) child.template = job.template;
|
|
8164
|
+
const result = await hooks.runChild(child, {
|
|
8165
|
+
from: leader,
|
|
8166
|
+
text: job.brief
|
|
8167
|
+
});
|
|
8168
|
+
return reportOf(job.id, result);
|
|
8169
|
+
})),
|
|
8170
|
+
notes
|
|
8171
|
+
};
|
|
8172
|
+
} finally {
|
|
8173
|
+
await hooks.reclaim();
|
|
8174
|
+
}
|
|
7471
8175
|
};
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
const
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
};
|
|
7478
|
-
/** The `run.started` event: the workflow, its entries, nodes and inputs. */
|
|
7479
|
-
const runStartedEvent = (graph, runId, options) => ({
|
|
7480
|
-
type: "run.started",
|
|
7481
|
-
runId,
|
|
7482
|
-
workflow: graph.workflow.name,
|
|
7483
|
-
entries: graph.entries,
|
|
7484
|
-
nodes: declaredNodes(graph),
|
|
7485
|
-
edges: graph.edges.map(({ from, to, when, maxRounds }) => ({
|
|
7486
|
-
from,
|
|
7487
|
-
to,
|
|
7488
|
-
...when === void 0 ? {} : { when },
|
|
7489
|
-
...maxRounds === void 0 ? {} : { maxRounds }
|
|
7490
|
-
})),
|
|
7491
|
-
channels: graph.channels,
|
|
7492
|
-
inputs: options.inputs,
|
|
7493
|
-
...options.settings === void 0 ? {} : { settings: options.settings },
|
|
7494
|
-
...runBudget(graph, options.settings),
|
|
7495
|
-
...options.orkVersion === void 0 ? {} : { ork: options.orkVersion }
|
|
7496
|
-
});
|
|
7497
|
-
/** The unread mail of nodes that never ran; a node that ran saw its notice. */
|
|
7498
|
-
const unreadOf = (nodes, unread) => {
|
|
7499
|
-
const ran = new Set(nodes.filter((node) => node.runs > 0).map((node) => node.id));
|
|
7500
|
-
const left = [...unread ?? []].filter(([node]) => !ran.has(node)).map(([node, messages]) => ({
|
|
7501
|
-
node,
|
|
7502
|
-
messages
|
|
7503
|
-
}));
|
|
7504
|
-
return left.length === 0 ? {} : { unread: left };
|
|
7505
|
-
};
|
|
7506
|
-
/**
|
|
7507
|
-
* The run's result: failed when any reason was given, unless it stopped. A
|
|
7508
|
-
* skipped node or a handled failure leaves it `succeeded`, and is counted.
|
|
7509
|
-
*/
|
|
7510
|
-
const runResult = (run) => ({
|
|
7511
|
-
runId: run.runId,
|
|
7512
|
-
status: run.stopped ?? (run.reasons.length > 0 ? "failed" : "succeeded"),
|
|
7513
|
-
costUsd: run.costUsd,
|
|
7514
|
-
nodes: run.nodes,
|
|
7515
|
-
reasons: run.reasons,
|
|
7516
|
-
worktrees: run.worktrees,
|
|
7517
|
-
...run.outputs === void 0 ? {} : { outputs: run.outputs },
|
|
7518
|
-
skipped: run.nodes.filter((node) => node.runs === 0).length,
|
|
7519
|
-
failuresHandled: run.failuresHandled,
|
|
7520
|
-
...unreadOf(run.nodes, run.unread)
|
|
7521
|
-
});
|
|
7522
|
-
/** A workflow's `outputs`, rendered as the run ends. */
|
|
7523
|
-
const renderOutputs = (outputs, scope) => outputs === void 0 ? void 0 : Object.fromEntries(Object.entries(outputs).map(([name, text]) => {
|
|
7524
|
-
const template = parseTemplate(text);
|
|
7525
|
-
return [name, template.ok ? renderTemplate(template.parts, scope) : text];
|
|
7526
|
-
}));
|
|
7527
|
-
/** Closes the run's worktrees; a failure is one more reason, not a crash. */
|
|
7528
|
-
const closeWorkspace = (workspace, reasons) => (workspace?.close() ?? Promise.resolve([])).catch((error) => {
|
|
7529
|
-
reasons.push(`cleaning up worktrees: ${String(error)}`);
|
|
7530
|
-
return [];
|
|
7531
|
-
});
|
|
7532
|
-
// ../core/src/run/slots.ts
|
|
7533
|
-
/**
|
|
7534
|
-
* @file Session slots: at most `limits.concurrency` Claude sessions run at
|
|
7535
|
-
* once. Queued activations take a free slot when the scheduler pumps;
|
|
7536
|
-
* delegated children and leaders returning from `delegate` wait for one and
|
|
7537
|
-
* are served first, so a leader waiting on its children never holds a slot
|
|
7538
|
-
* they need.
|
|
7539
|
-
*/
|
|
7540
|
-
/** Only a Claude session takes a slot; tools and gates are not limited. */
|
|
7541
|
-
const needsSlot = (node) => isClaudeNode(node);
|
|
7542
|
-
/** A call spends through its run, so it needs budget as a session does. */
|
|
7543
|
-
const spendsBudget = (node) => needsSlot(node) || node.type === "workflow.call";
|
|
7544
|
-
const createSlots = (limit, onFree) => {
|
|
7545
|
-
let used = 0;
|
|
7546
|
-
const waiting = [];
|
|
7547
|
-
return {
|
|
7548
|
-
limit,
|
|
7549
|
-
tryTake: () => {
|
|
7550
|
-
if (used >= limit || waiting.length > 0) return false;
|
|
7551
|
-
used += 1;
|
|
7552
|
-
return true;
|
|
7553
|
-
},
|
|
7554
|
-
take: () => {
|
|
7555
|
-
if (used < limit && waiting.length === 0) {
|
|
7556
|
-
used += 1;
|
|
7557
|
-
return Promise.resolve();
|
|
8176
|
+
const delegate = (tasks) => {
|
|
8177
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8178
|
+
const jobs = tasks.map((task) => {
|
|
8179
|
+
if (!children.some((c) => c.id === task.child)) {
|
|
8180
|
+
const names = children.map((c) => c.id).join(", ");
|
|
8181
|
+
return rejected(task.child, `not one of your children (${names === "" ? "you have none" : names})`);
|
|
7558
8182
|
}
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
return
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
onFree();
|
|
7568
|
-
} else next();
|
|
7569
|
-
}
|
|
8183
|
+
if (seen.has(task.child) || hooks.isRunning(task.child)) return rejected(task.child, templates.includes(task.child) && maxWorkers > 0 ? "already running; use spawn_worker for another instance" : "already running");
|
|
8184
|
+
seen.add(task.child);
|
|
8185
|
+
return {
|
|
8186
|
+
id: task.child,
|
|
8187
|
+
brief: task.brief
|
|
8188
|
+
};
|
|
8189
|
+
});
|
|
8190
|
+
return runJobs(jobs);
|
|
7570
8191
|
};
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
const listOf = (forEach, scope) => {
|
|
7577
|
-
if ("items" in forEach) return {
|
|
7578
|
-
ok: true,
|
|
7579
|
-
list: forEach.items
|
|
8192
|
+
const nextInstance = (template) => {
|
|
8193
|
+
for (let n = 2;; n += 1) {
|
|
8194
|
+
const id = `${template}-${String(n)}`;
|
|
8195
|
+
if (!graph.nodes.has(id) && !spawned.includes(id)) return id;
|
|
8196
|
+
}
|
|
7580
8197
|
};
|
|
7581
|
-
const
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
8198
|
+
const spawn = (tasks) => {
|
|
8199
|
+
const jobs = tasks.map((task) => {
|
|
8200
|
+
if (!templates.includes(task.template)) {
|
|
8201
|
+
const names = templates.join(", ");
|
|
8202
|
+
return rejected(task.template, `not one of your child workers (${names === "" ? "you have none" : names})`);
|
|
8203
|
+
}
|
|
8204
|
+
if (spawned.length >= maxWorkers) return rejected(task.template, `no workers left to spawn (maxWorkers: ${String(maxWorkers)})`);
|
|
8205
|
+
const id = nextInstance(task.template);
|
|
8206
|
+
spawned.push(id);
|
|
8207
|
+
return {
|
|
8208
|
+
id,
|
|
8209
|
+
template: task.template,
|
|
8210
|
+
brief: task.brief
|
|
8211
|
+
};
|
|
8212
|
+
});
|
|
8213
|
+
return runJobs(jobs);
|
|
7587
8214
|
};
|
|
7588
|
-
const name = String(templatePath(forEach.from)?.at(-1) ?? forEach.from);
|
|
7589
8215
|
return {
|
|
7590
|
-
|
|
7591
|
-
|
|
8216
|
+
children,
|
|
8217
|
+
fanned: (owner?.children ?? []).filter((id) => isFanned(id)),
|
|
8218
|
+
templates,
|
|
8219
|
+
workersLeft: () => Math.max(0, maxWorkers - spawned.length),
|
|
8220
|
+
delegate,
|
|
8221
|
+
spawn
|
|
7592
8222
|
};
|
|
7593
8223
|
};
|
|
7594
|
-
|
|
7595
|
-
const sourceOf = (forEach, scope) => {
|
|
7596
|
-
if (!("from" in forEach)) return void 0;
|
|
7597
|
-
const [root, id] = templatePath(forEach.from) ?? [];
|
|
7598
|
-
return root === "nodes" ? scope.nodes[String(id)]?.output : void 0;
|
|
7599
|
-
};
|
|
7600
|
-
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8224
|
+
// ../core/src/run/scope.ts
|
|
7601
8225
|
/**
|
|
7602
|
-
*
|
|
7603
|
-
*
|
|
7604
|
-
* failed. The full output stays in the item's own `node.finished`.
|
|
8226
|
+
* @file A run's id and folder, and the scope templates and `when:` conditions
|
|
8227
|
+
* read during it.
|
|
7605
8228
|
*/
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
const
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
"
|
|
7612
|
-
|
|
7613
|
-
"merge"
|
|
7614
|
-
].map((field) => [field, output[field]]);
|
|
7615
|
-
const failed = !ok && typeof output["exitCode"] === "number" ? [["exitCode", output["exitCode"]], ["error", error]] : [];
|
|
7616
|
-
return Object.fromEntries([
|
|
7617
|
-
...fields,
|
|
7618
|
-
...own,
|
|
7619
|
-
...failed
|
|
7620
|
-
].filter(([, value]) => value !== void 0));
|
|
7621
|
-
};
|
|
7622
|
-
/** Whether an item passed but its work could not reach its owner. */
|
|
7623
|
-
const mergeFailed = ({ output }) => isRecord$1(output["merge"]) && output["merge"]["ok"] === false;
|
|
7624
|
-
const statusOf$2 = (result) => {
|
|
7625
|
-
if (mergeFailed(result)) return "fail";
|
|
7626
|
-
const { ok, output } = result;
|
|
7627
|
-
const status = output["status"];
|
|
7628
|
-
if (typeof status === "string") return status;
|
|
7629
|
-
return ok ? "pass" : "fail";
|
|
7630
|
-
};
|
|
7631
|
-
/** Why a fan-out did not succeed, naming the first item that failed. */
|
|
7632
|
-
const whyNot = (count, failures, skipped) => {
|
|
7633
|
-
const parts = [`${String(failures.length)} of ${String(count)} items failed`];
|
|
7634
|
-
const [first] = failures;
|
|
7635
|
-
if (first !== void 0) parts[0] = `${parts[0] ?? ""}; first, ${first}`;
|
|
7636
|
-
if (skipped > 0) parts.push(`${String(skipped)} not started`);
|
|
7637
|
-
return parts.join("; ");
|
|
8229
|
+
/** The run's id (given, or a new one) and its `.ork/runs/<run>` folder. */
|
|
8230
|
+
const runPlace = (options) => {
|
|
8231
|
+
const runId = options.runId ?? randomUUID().slice(0, 8);
|
|
8232
|
+
return {
|
|
8233
|
+
runId,
|
|
8234
|
+
runDir: options.runDir ?? path.join(options.cwd, ".ork", "runs", runId)
|
|
8235
|
+
};
|
|
7638
8236
|
};
|
|
7639
8237
|
/**
|
|
7640
|
-
*
|
|
7641
|
-
*
|
|
8238
|
+
* The folders every node of a run reads from: the workflow's, the project's,
|
|
8239
|
+
* Ork's.
|
|
7642
8240
|
*/
|
|
7643
|
-
const
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
8241
|
+
const foldersOf = (options) => ({
|
|
8242
|
+
workflowDir: options.workflowDir ?? options.cwd,
|
|
8243
|
+
projectRoot: options.projectRoot ?? options.cwd,
|
|
8244
|
+
...options.orkDir === void 0 ? {} : { orkDir: options.orkDir }
|
|
8245
|
+
});
|
|
8246
|
+
/** What templates and `when:` read during a run. */
|
|
8247
|
+
const runScope = (inputs, outputs, runId, item) => {
|
|
8248
|
+
const scope = {
|
|
8249
|
+
inputs,
|
|
8250
|
+
nodes: Object.fromEntries(Object.entries(outputs).map(([id, output]) => [id, { output }]))
|
|
8251
|
+
};
|
|
8252
|
+
if (runId !== void 0) scope.run = { id: runId };
|
|
8253
|
+
if (item !== void 0) {
|
|
8254
|
+
scope.item = item.value;
|
|
8255
|
+
scope.index = item.index;
|
|
7650
8256
|
}
|
|
8257
|
+
return scope;
|
|
7651
8258
|
};
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
}
|
|
7693
|
-
const narrowed = chosen.length !== list.length;
|
|
7694
|
-
list = chosen;
|
|
7695
|
-
if (narrowed) recordList();
|
|
7696
|
-
}
|
|
7697
|
-
const node = nodeOf(activation);
|
|
7698
|
-
const session = node !== void 0 && needsSlot(node);
|
|
7699
|
-
const spends = node !== void 0 && spendsBudget(node);
|
|
7700
|
-
const declared = node === void 0 ? void 0 : declaredOutputOf(node);
|
|
7701
|
-
const results = list.map(() => ({}));
|
|
7702
|
-
const statuses = list.map(() => "skipped");
|
|
7703
|
-
const failures = [];
|
|
7704
|
-
const mayStart = () => !signal.aborted && !deps.isStopped() && (forEach.onFailure !== "stop" || failures.length === 0);
|
|
7705
|
-
const runItem = async (index) => {
|
|
7706
|
-
const itemId = `${id}.${String(index)}`;
|
|
7707
|
-
const hold = holds.forNode(itemId, id);
|
|
7708
|
-
await awaitTurn(hold, session ? slots : null, signal);
|
|
7709
|
-
try {
|
|
7710
|
-
if (!mayStart() || spends && deps.budgetSpent()) return;
|
|
7711
|
-
const result = await perform({
|
|
7712
|
-
id: itemId,
|
|
7713
|
-
input,
|
|
7714
|
-
template: id,
|
|
7715
|
-
item: {
|
|
7716
|
-
of: id,
|
|
7717
|
-
index,
|
|
7718
|
-
value: list[index]
|
|
7719
|
-
},
|
|
7720
|
-
round
|
|
7721
|
-
});
|
|
7722
|
-
results[index] = entryOf(declared, result);
|
|
7723
|
-
statuses[index] = statusOf$2(result);
|
|
7724
|
-
if (!result.ok) failures.push(`${itemId}: ${result.error ?? "failed"}`);
|
|
7725
|
-
else if (mergeFailed(result)) failures.push(`${itemId}: its work could not be merged`);
|
|
7726
|
-
} finally {
|
|
7727
|
-
if (session) slots.release();
|
|
7728
|
-
}
|
|
7729
|
-
};
|
|
7730
|
-
let next = 0;
|
|
7731
|
-
const worker = async () => {
|
|
7732
|
-
while (next < list.length && mayStart()) {
|
|
7733
|
-
const index = next;
|
|
7734
|
-
next += 1;
|
|
7735
|
-
await runItem(index);
|
|
8259
|
+
// ../core/src/run/context.ts
|
|
8260
|
+
/**
|
|
8261
|
+
* @file One activation's execute context: what the scheduler hands a node's
|
|
8262
|
+
* executor. Its inputs and the outputs so far, where it works, the budget
|
|
8263
|
+
* left, the human, its hold and its own stop signal (ADR-041), its ports on
|
|
8264
|
+
* the bus, the session it resumes, its share of the machine, and, for a
|
|
8265
|
+
* leader, its ownership of its children.
|
|
8266
|
+
*/
|
|
8267
|
+
const nodeContext = (deps, activation, cwd, round, clock) => {
|
|
8268
|
+
const { options, runId } = deps;
|
|
8269
|
+
const { graph } = options;
|
|
8270
|
+
const { id, template } = activation;
|
|
8271
|
+
const context = {
|
|
8272
|
+
runId,
|
|
8273
|
+
id,
|
|
8274
|
+
workflow: graph.workflow,
|
|
8275
|
+
inputs: options.inputs,
|
|
8276
|
+
outputs: deps.outputs,
|
|
8277
|
+
input: activation.input,
|
|
8278
|
+
round,
|
|
8279
|
+
cwd,
|
|
8280
|
+
runDir: deps.runDir,
|
|
8281
|
+
...foldersOf(options),
|
|
8282
|
+
budgetLeftUsd: deps.spend.left(),
|
|
8283
|
+
budgetLeftNow: deps.spend.budgetLeft,
|
|
8284
|
+
reserveBudget: (usd) => {
|
|
8285
|
+
deps.spend.reserve(id, usd, deps.nodeOf(activation)?.type !== "claude.leader");
|
|
8286
|
+
},
|
|
8287
|
+
budgetFreed: (signal) => deps.spend.freed(id, signal),
|
|
8288
|
+
quota: deps.spend.quota,
|
|
8289
|
+
ownSpend: (sessionId, total) => deps.spend.ownOf(id, sessionId, total),
|
|
8290
|
+
human: deps.human,
|
|
8291
|
+
hold: deps.holds.forNode(id, template),
|
|
8292
|
+
...deps.ports.forNode(id, template, cwd),
|
|
8293
|
+
...options.calls === void 0 ? {} : { calls: options.calls },
|
|
8294
|
+
emit: (event) => {
|
|
8295
|
+
deps.ports.owners.seen(id, event, cwd);
|
|
8296
|
+
if (event.type === "session.finished" && event.fork !== true) {
|
|
8297
|
+
const { sessionId, usage } = event.outcome;
|
|
8298
|
+
deps.spend.report(id, sessionId, usage.costUsd);
|
|
7736
8299
|
}
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
output: {
|
|
7745
|
-
items: list,
|
|
7746
|
-
results,
|
|
7747
|
-
statuses,
|
|
7748
|
-
count: list.length
|
|
7749
|
-
},
|
|
7750
|
-
...ok ? {} : { error: whyNot(list.length, failures, skipped) },
|
|
7751
|
-
...!ok && deps.isStopped() ? { limited: deps.reasons[0] ?? "the run stopped" } : {}
|
|
7752
|
-
};
|
|
7753
|
-
return {
|
|
7754
|
-
...stops.ended(id, gathered),
|
|
7755
|
-
output: gathered.output
|
|
7756
|
-
};
|
|
8300
|
+
options.onEvent({
|
|
8301
|
+
type: "node.event",
|
|
8302
|
+
id,
|
|
8303
|
+
event
|
|
8304
|
+
});
|
|
8305
|
+
},
|
|
8306
|
+
signal: deps.stops.signalFor(id, template)
|
|
7757
8307
|
};
|
|
8308
|
+
const owner = ownerPort(deps, id, template);
|
|
8309
|
+
if (owner !== void 0) context.owner = owner;
|
|
8310
|
+
if (template !== void 0) context.template = template;
|
|
8311
|
+
const machine = clock === void 0 ? void 0 : machinePort(options, runId, id, clock);
|
|
8312
|
+
if (machine !== void 0) context.machine = machine;
|
|
8313
|
+
if (activation.brief !== void 0) context.brief = activation.brief;
|
|
8314
|
+
if (activation.item !== void 0) context.item = activation.item;
|
|
8315
|
+
const resume = options.replay?.cutOff?.(activation) ?? resumeOf(deps.nodeOf(activation), deps.sessionIds.get(id) ?? options.replay?.previousSession?.(activation));
|
|
8316
|
+
if (resume !== void 0) context.resume = resume;
|
|
8317
|
+
if (deps.nodeOf(activation)?.type === "claude.leader") context.ownership = createOwnership({
|
|
8318
|
+
graph,
|
|
8319
|
+
leader: id,
|
|
8320
|
+
runChild: deps.runChild,
|
|
8321
|
+
isRunning: (other) => deps.running.has(other),
|
|
8322
|
+
checkpoint: () => options.workspace?.checkpoint(id) ?? Promise.resolve(null),
|
|
8323
|
+
releaseClaims: () => {
|
|
8324
|
+
const held = context.claims?.held() ?? [];
|
|
8325
|
+
for (const resource of held) context.claims?.release(resource);
|
|
8326
|
+
return held;
|
|
8327
|
+
},
|
|
8328
|
+
lend: deps.slots.release,
|
|
8329
|
+
reclaim: deps.slots.take
|
|
8330
|
+
});
|
|
8331
|
+
return context;
|
|
7758
8332
|
};
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
const
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
8333
|
+
/** Where `id`'s questions go first, if its leader takes them (ADR-063). */
|
|
8334
|
+
const ownerPort = (deps, id, template) => deps.ports.owners.portFor(id, template, {
|
|
8335
|
+
budgetLeftUsd: deps.spend.left,
|
|
8336
|
+
charge: (owner, sessionId, usage) => {
|
|
8337
|
+
const own = deps.spend.count(owner, {
|
|
8338
|
+
ok: true,
|
|
8339
|
+
output: {},
|
|
8340
|
+
sessionId,
|
|
8341
|
+
usage
|
|
8342
|
+
}, false);
|
|
8343
|
+
deps.summary(owner).costUsd += own.usage?.costUsd ?? 0;
|
|
8344
|
+
},
|
|
8345
|
+
emit: (owner, event) => {
|
|
8346
|
+
deps.options.onEvent({
|
|
8347
|
+
type: "node.event",
|
|
8348
|
+
id: owner,
|
|
8349
|
+
event
|
|
7772
8350
|
});
|
|
7773
|
-
} finally {
|
|
7774
|
-
hooks.slots.release();
|
|
7775
8351
|
}
|
|
8352
|
+
});
|
|
8353
|
+
/**
|
|
8354
|
+
* The session a node that runs again continues, when it resumes (ADR-033): by
|
|
8355
|
+
* `session: resume`, the default for a leader; a worker starts fresh unless it
|
|
8356
|
+
* says `resume`.
|
|
8357
|
+
*/
|
|
8358
|
+
const resumeOf = (node, sessionId) => {
|
|
8359
|
+
if (node === void 0 || !isClaudeNode(node)) return void 0;
|
|
8360
|
+
if (node.type === "claude.subagent") return void 0;
|
|
8361
|
+
return (node.session ?? (node.type === "claude.leader" ? "resume" : "fresh")) === "resume" ? sessionId : void 0;
|
|
7776
8362
|
};
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
error
|
|
8363
|
+
// ../core/src/run/contacts.ts
|
|
8364
|
+
const SESSION_TYPES$5 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
|
|
8365
|
+
const isSession$1 = (node) => SESSION_TYPES$5.has(node?.type ?? "");
|
|
8366
|
+
const isTool = (node) => node?.type.startsWith("tool.") ?? false;
|
|
8367
|
+
const channelPeersOf = (id, channels) => channels.flatMap(([a, b]) => {
|
|
8368
|
+
if (a === id) return [b];
|
|
8369
|
+
return b === id ? [a] : [];
|
|
7785
8370
|
});
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
8371
|
+
/** Every declared node, with its owner, as a run records them. */
|
|
8372
|
+
const declaredNodes = (graph) => [...graph.nodes.values()].map(({ id, node }) => node.parent === void 0 ? {
|
|
8373
|
+
id,
|
|
8374
|
+
type: node.type
|
|
8375
|
+
} : {
|
|
8376
|
+
id,
|
|
8377
|
+
type: node.type,
|
|
8378
|
+
parent: node.parent
|
|
8379
|
+
});
|
|
8380
|
+
/** Whom a session may write to, by how it knows them. */
|
|
8381
|
+
const contactsOf = (id, nodes, channels) => {
|
|
8382
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
8383
|
+
return {
|
|
8384
|
+
owner: byId.get(id)?.parent,
|
|
8385
|
+
children: nodes.filter((node) => node.parent === id && isSession$1(node)).map((node) => node.id),
|
|
8386
|
+
channels: channelPeersOf(id, channels).filter((peer) => !isTool(byId.get(peer)))
|
|
7791
8387
|
};
|
|
7792
|
-
if (result.error !== void 0) report.error = result.error;
|
|
7793
|
-
return report;
|
|
7794
8388
|
};
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
const
|
|
7798
|
-
const
|
|
7799
|
-
|
|
7800
|
-
|
|
7801
|
-
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
}
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
if (seen.has(task.child) || hooks.isRunning(task.child)) return rejected(task.child, templates.includes(task.child) && maxWorkers > 0 ? "already running; use spawn_worker for another instance" : "already running");
|
|
7850
|
-
seen.add(task.child);
|
|
7851
|
-
return {
|
|
7852
|
-
id: task.child,
|
|
7853
|
-
brief: task.brief
|
|
7854
|
-
};
|
|
8389
|
+
/** Whom a program in the tool node `id` may write to. */
|
|
8390
|
+
const scriptContactsOf = (id, nodes, channels) => {
|
|
8391
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
8392
|
+
const owner = byId.get(id)?.parent;
|
|
8393
|
+
return [.../* @__PURE__ */ new Set([...owner !== void 0 && isSession$1(byId.get(owner)) ? [owner] : [], ...channelPeersOf(id, channels).filter((peer) => isSession$1(byId.get(peer)))])];
|
|
8394
|
+
};
|
|
8395
|
+
/** Whether a program in a tool node may write to the session `id`. */
|
|
8396
|
+
const hearsFromScripts = (id, nodes, channels) => nodes.some((node) => isTool(node) && scriptContactsOf(node.id, nodes, channels).includes(id));
|
|
8397
|
+
// ../core/src/usage/spend.ts
|
|
8398
|
+
/** What a report would add to the session's earlier ones, counting nothing. */
|
|
8399
|
+
const unreported = (reported, key, total) => {
|
|
8400
|
+
if (total <= 0) return 0;
|
|
8401
|
+
const before = reported.get(key) ?? 0;
|
|
8402
|
+
return total < before ? total : total - before;
|
|
8403
|
+
};
|
|
8404
|
+
/**
|
|
8405
|
+
* What a report adds to the session's earlier ones, keeping the total in
|
|
8406
|
+
* `reported`. A zero says nothing, so it is ignored; a lower total is a session
|
|
8407
|
+
* that started counting again, so all of it is new.
|
|
8408
|
+
*/
|
|
8409
|
+
const ownSpend = (reported, key, total) => {
|
|
8410
|
+
const own = unreported(reported, key, total);
|
|
8411
|
+
if (total > 0) reported.set(key, total);
|
|
8412
|
+
return own;
|
|
8413
|
+
};
|
|
8414
|
+
/**
|
|
8415
|
+
* What a run's events say it spent, for `ork resume`. The total is what its
|
|
8416
|
+
* nodes recorded. A session's report counts once its node finished, or when
|
|
8417
|
+
* `ork chat` made it outside any node: a session cut off before its node
|
|
8418
|
+
* finished has recorded nothing, so its next total counts the part that was cut
|
|
8419
|
+
* off. A leader's fork is never resumed, so its report counts when it comes,
|
|
8420
|
+
* whether or not its leader finished.
|
|
8421
|
+
*/
|
|
8422
|
+
const earlierSpend = (events) => {
|
|
8423
|
+
const reported = /* @__PURE__ */ new Map();
|
|
8424
|
+
const open = /* @__PURE__ */ new Map();
|
|
8425
|
+
const forks = /* @__PURE__ */ new Map();
|
|
8426
|
+
let total = 0;
|
|
8427
|
+
for (const event of events) if (event.type === "node.started") open.set(event.id, null);
|
|
8428
|
+
else if (event.type === "node.finished") {
|
|
8429
|
+
total += event.usage?.costUsd ?? 0;
|
|
8430
|
+
const last = open.get(event.id);
|
|
8431
|
+
if (last !== null && last !== void 0) ownSpend(reported, last.key, last.cost);
|
|
8432
|
+
open.delete(event.id);
|
|
8433
|
+
} else if (event.type === "node.event" && event.event.type === "session.finished") {
|
|
8434
|
+
const { sessionId, usage } = event.event.outcome;
|
|
8435
|
+
const key = `${event.id}/${sessionId}`;
|
|
8436
|
+
if (event.event.fork === true) {
|
|
8437
|
+
const own = ownSpend(reported, key, usage.costUsd);
|
|
8438
|
+
total += own;
|
|
8439
|
+
forks.set(event.id, (forks.get(event.id) ?? 0) + own);
|
|
8440
|
+
} else if (open.has(event.id)) open.set(event.id, {
|
|
8441
|
+
key,
|
|
8442
|
+
cost: usage.costUsd
|
|
7855
8443
|
});
|
|
7856
|
-
|
|
8444
|
+
else ownSpend(reported, key, usage.costUsd);
|
|
8445
|
+
}
|
|
8446
|
+
return {
|
|
8447
|
+
total,
|
|
8448
|
+
reported,
|
|
8449
|
+
forks
|
|
8450
|
+
};
|
|
8451
|
+
};
|
|
8452
|
+
// ../core/src/run/reserves.ts
|
|
8453
|
+
const createReserves = () => {
|
|
8454
|
+
const held = /* @__PURE__ */ new Map();
|
|
8455
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
8456
|
+
const wake = () => {
|
|
8457
|
+
const woken = [...waiters];
|
|
8458
|
+
waiters.clear();
|
|
8459
|
+
for (const resolve of woken) resolve();
|
|
7857
8460
|
};
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
8461
|
+
return {
|
|
8462
|
+
/** All that is set aside now. */
|
|
8463
|
+
total: () => [...held.values()].reduce((sum, reserve) => sum + reserve.usd, 0),
|
|
8464
|
+
/**
|
|
8465
|
+
* `id` sets aside `usd` (nothing, or 0, lets it go). `waitable` is false
|
|
8466
|
+
* for a leader, whose children must never wait on it.
|
|
8467
|
+
*/
|
|
8468
|
+
reserve: (id, usd, waitable) => {
|
|
8469
|
+
const before = held.get(id)?.usd ?? 0;
|
|
8470
|
+
if (usd === void 0 || usd <= 0) held.delete(id);
|
|
8471
|
+
else held.set(id, {
|
|
8472
|
+
usd,
|
|
8473
|
+
waitable
|
|
8474
|
+
});
|
|
8475
|
+
if ((usd ?? 0) < before) wake();
|
|
8476
|
+
},
|
|
8477
|
+
/** The node ended: whatever it still held is let go. */
|
|
8478
|
+
drop: (id) => {
|
|
8479
|
+
if (held.delete(id)) wake();
|
|
8480
|
+
},
|
|
8481
|
+
/**
|
|
8482
|
+
* Waits until a reserve shrinks. `false` at once when no other node holds
|
|
8483
|
+
* one worth waiting on, or when `signal` aborts.
|
|
8484
|
+
*/
|
|
8485
|
+
freed: (id, signal) => {
|
|
8486
|
+
if (![...held].some(([other, reserve]) => other !== id && reserve.waitable) || signal.aborted) return Promise.resolve(false);
|
|
8487
|
+
return new Promise((resolve) => {
|
|
8488
|
+
const done = (freed) => () => {
|
|
8489
|
+
waiters.delete(onFree);
|
|
8490
|
+
signal.removeEventListener("abort", onAbort);
|
|
8491
|
+
resolve(freed);
|
|
8492
|
+
};
|
|
8493
|
+
const onFree = done(true);
|
|
8494
|
+
const onAbort = done(false);
|
|
8495
|
+
waiters.add(onFree);
|
|
8496
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
8497
|
+
});
|
|
7862
8498
|
}
|
|
7863
8499
|
};
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
};
|
|
7878
|
-
});
|
|
7879
|
-
return runJobs(jobs);
|
|
8500
|
+
};
|
|
8501
|
+
// ../core/src/run/spending.ts
|
|
8502
|
+
/**
|
|
8503
|
+
* A result with only what this activation spent (ADR-046): a session's report
|
|
8504
|
+
* holds its total so far, while its tokens are this process's own. `reported`
|
|
8505
|
+
* holds each session's total.
|
|
8506
|
+
*/
|
|
8507
|
+
const withOwnSpend = (id, result, reported) => {
|
|
8508
|
+
const { sessionId, usage } = result;
|
|
8509
|
+
if (sessionId === void 0 || usage === void 0) return result;
|
|
8510
|
+
const own = {
|
|
8511
|
+
...usage,
|
|
8512
|
+
costUsd: ownSpend(reported, `${id}/${sessionId}`, usage.costUsd)
|
|
7880
8513
|
};
|
|
7881
8514
|
return {
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
8515
|
+
...result,
|
|
8516
|
+
usage: own,
|
|
8517
|
+
output: "usage" in result.output ? {
|
|
8518
|
+
...result.output,
|
|
8519
|
+
usage: own
|
|
8520
|
+
} : result.output
|
|
7888
8521
|
};
|
|
7889
8522
|
};
|
|
7890
|
-
|
|
8523
|
+
/** A run's spending so far; a resumed run starts from what it had spent. */
|
|
8524
|
+
const createSpend = (budgetUsd, earlier, limits) => {
|
|
8525
|
+
const reported = new Map(earlier?.reported);
|
|
8526
|
+
let spent = earlier?.total ?? 0;
|
|
8527
|
+
/** What running sessions last reported, not yet in `spent`, by `node/session`. */
|
|
8528
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
8529
|
+
const committed = () => spent + [...inFlight.values()].reduce((sum, cost) => sum + cost, 0);
|
|
8530
|
+
/** Caps running sessions were given (ADR-077): out of what is offered. */
|
|
8531
|
+
const reserves = createReserves();
|
|
8532
|
+
const budgetLeft = () => budgetUsd === void 0 ? void 0 : budgetUsd - committed() - reserves.total();
|
|
8533
|
+
return {
|
|
8534
|
+
total: () => spent,
|
|
8535
|
+
/** A node sets aside its session's cap; nothing or 0 lets it go. */
|
|
8536
|
+
reserve: reserves.reserve,
|
|
8537
|
+
/** Waits until another node's reserve frees; `false` when none can. */
|
|
8538
|
+
freed: reserves.freed,
|
|
8539
|
+
/** What a session has spent in this activation, from its total so far. */
|
|
8540
|
+
ownOf: (id, sessionId, total) => unreported(reported, `${id}/${sessionId}`, total),
|
|
8541
|
+
/** A running session said what it has spent so far. */
|
|
8542
|
+
report: (id, sessionId, total) => {
|
|
8543
|
+
const key = `${id}/${sessionId}`;
|
|
8544
|
+
inFlight.set(key, unreported(reported, key, total));
|
|
8545
|
+
},
|
|
8546
|
+
/** The node ended: `count` has what its sessions last reported. */
|
|
8547
|
+
settled: (id) => {
|
|
8548
|
+
for (const key of inFlight.keys()) if (key.startsWith(`${id}/`)) inFlight.delete(key);
|
|
8549
|
+
reserves.drop(id);
|
|
8550
|
+
},
|
|
8551
|
+
/** What a session may spend: the least of the budget and any quota. */
|
|
8552
|
+
left: () => {
|
|
8553
|
+
const known = [budgetLeft(), limits?.().leftUsd].filter((left) => left !== void 0);
|
|
8554
|
+
return known.length === 0 ? void 0 : Math.min(...known);
|
|
8555
|
+
},
|
|
8556
|
+
budgetLeft,
|
|
8557
|
+
/** What the limits across runs allow now. */
|
|
8558
|
+
quota: () => limits?.() ?? {},
|
|
8559
|
+
/**
|
|
8560
|
+
* What stops anything new from starting: a quota first, as a budget can be
|
|
8561
|
+
* raised.
|
|
8562
|
+
*/
|
|
8563
|
+
spentOut: () => {
|
|
8564
|
+
const blocked = limits?.().blocked;
|
|
8565
|
+
if (blocked !== void 0) return {
|
|
8566
|
+
status: "quota_exhausted",
|
|
8567
|
+
reason: blocked
|
|
8568
|
+
};
|
|
8569
|
+
return budgetUsd !== void 0 && committed() >= budgetUsd ? {
|
|
8570
|
+
status: "budget_exhausted",
|
|
8571
|
+
reason: `the run's budget of $${String(budgetUsd)} is spent`
|
|
8572
|
+
} : null;
|
|
8573
|
+
},
|
|
8574
|
+
/**
|
|
8575
|
+
* Counts a result, and gives it back with only its own spend. A replayed
|
|
8576
|
+
* result is as stored, and already in what the run started from.
|
|
8577
|
+
*/
|
|
8578
|
+
count: (id, result, replayed) => {
|
|
8579
|
+
if (replayed) return result;
|
|
8580
|
+
const own = withOwnSpend(id, result, reported);
|
|
8581
|
+
spent += own.usage?.costUsd ?? 0;
|
|
8582
|
+
return own;
|
|
8583
|
+
}
|
|
8584
|
+
};
|
|
8585
|
+
};
|
|
8586
|
+
/** A run's spending, from what the scheduler is given. */
|
|
8587
|
+
const spendOf = (budgetUsd, options) => createSpend(budgetUsd, options.replay?.earlier, options.limits);
|
|
8588
|
+
// ../core/src/run/gather.ts
|
|
8589
|
+
const asList = (value) => Array.isArray(value) ? value : [];
|
|
8590
|
+
/** `before` then `now`, leaving out an entry already there. */
|
|
8591
|
+
const joined = (before, now) => {
|
|
8592
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8593
|
+
const out = [];
|
|
8594
|
+
for (const entry of [...asList(before), ...asList(now)]) {
|
|
8595
|
+
const key = JSON.stringify(entry);
|
|
8596
|
+
if (seen.has(key)) continue;
|
|
8597
|
+
seen.add(key);
|
|
8598
|
+
out.push(entry);
|
|
8599
|
+
}
|
|
8600
|
+
return out;
|
|
8601
|
+
};
|
|
7891
8602
|
/**
|
|
7892
|
-
*
|
|
7893
|
-
*
|
|
8603
|
+
* `result` with each gathered field holding every round's entries so far.
|
|
8604
|
+
* `previous` is the node's output from its last round, already gathered.
|
|
7894
8605
|
*/
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
const
|
|
8606
|
+
const joinRounds = (fields, previous, result) => {
|
|
8607
|
+
if (fields.length === 0 || previous === void 0) return result;
|
|
8608
|
+
const output = { ...result.output };
|
|
8609
|
+
for (const field of fields) output[field] = joined(previous[field], result.output[field]);
|
|
7898
8610
|
return {
|
|
7899
|
-
|
|
7900
|
-
|
|
8611
|
+
...result,
|
|
8612
|
+
output
|
|
7901
8613
|
};
|
|
7902
8614
|
};
|
|
8615
|
+
/** `result` with `node`'s `output.gather` fields joined to its earlier rounds. */
|
|
8616
|
+
const gatherRounds = (node, previous, result) => joinRounds(gatheredFieldsOf(node), previous, result);
|
|
7903
8617
|
/**
|
|
7904
|
-
*
|
|
7905
|
-
*
|
|
8618
|
+
* What `activation`'s rounds reported so far, to gather onto. A spawned
|
|
8619
|
+
* instance gathers nothing earlier: its id (`impl-2`) is reused by a different
|
|
8620
|
+
* worker in the leader's next round. A delegated child that hasn't run since a
|
|
8621
|
+
* resume starts from what it reported before it, since its leader's recorded
|
|
8622
|
+
* rounds are handed back whole and never run it again.
|
|
7906
8623
|
*/
|
|
7907
|
-
const
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
8624
|
+
const previousOf = (activation, current, replay) => {
|
|
8625
|
+
const { brief, template, id } = activation;
|
|
8626
|
+
if (brief !== void 0 && template !== void 0) return void 0;
|
|
8627
|
+
if (current !== void 0 || brief === void 0) return current;
|
|
8628
|
+
return replay?.lastOutput?.(id);
|
|
8629
|
+
};
|
|
8630
|
+
/** Gathers each activation's result onto what its node reported before. */
|
|
8631
|
+
const createGatherer = (outputs, replay, nodeOf) => (activation, result) => gatherRounds(nodeOf(activation), previousOf(activation, outputs[activation.id], replay), result);
|
|
8632
|
+
// ../core/src/run/results.ts
|
|
8633
|
+
/** Control nodes pass their input through; a join's input is its output. */
|
|
8634
|
+
const passThrough = (input) => ({
|
|
8635
|
+
output: input ?? {},
|
|
8636
|
+
ok: true
|
|
7911
8637
|
});
|
|
7912
|
-
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
8638
|
+
const recordRun = (entry, result) => {
|
|
8639
|
+
entry.runs += 1;
|
|
8640
|
+
entry.ok = result.ok;
|
|
8641
|
+
entry.output = result.output;
|
|
8642
|
+
entry.costUsd += result.usage?.costUsd ?? 0;
|
|
8643
|
+
if (result.error === void 0) delete entry.error;
|
|
8644
|
+
else entry.error = result.error;
|
|
8645
|
+
if (result.ok) delete entry.end;
|
|
8646
|
+
else entry.end = endOf(result);
|
|
8647
|
+
};
|
|
8648
|
+
const startedEvent = (activation, nodeType, round, agent) => {
|
|
8649
|
+
const event = {
|
|
8650
|
+
type: "node.started",
|
|
8651
|
+
id: activation.id,
|
|
8652
|
+
nodeType,
|
|
8653
|
+
round
|
|
7917
8654
|
};
|
|
7918
|
-
if (
|
|
7919
|
-
if (
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
8655
|
+
if (activation.brief !== void 0) event.owner = activation.brief.from;
|
|
8656
|
+
if (agent !== void 0) event.agent = agent;
|
|
8657
|
+
const { item } = activation;
|
|
8658
|
+
if (item !== void 0) event.item = {
|
|
8659
|
+
of: item.of,
|
|
8660
|
+
index: item.index
|
|
8661
|
+
};
|
|
8662
|
+
return event;
|
|
7924
8663
|
};
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
* left, the human, its hold and its own stop signal (ADR-041), its ports on
|
|
7930
|
-
* the bus, the session it resumes, and, for a leader, its ownership of its
|
|
7931
|
-
* children.
|
|
7932
|
-
*/
|
|
7933
|
-
const nodeContext = (deps, activation, cwd, round) => {
|
|
7934
|
-
const { options, runId } = deps;
|
|
7935
|
-
const { graph } = options;
|
|
7936
|
-
const { id, template } = activation;
|
|
7937
|
-
const context = {
|
|
7938
|
-
runId,
|
|
7939
|
-
id,
|
|
7940
|
-
workflow: graph.workflow,
|
|
7941
|
-
inputs: options.inputs,
|
|
7942
|
-
outputs: deps.outputs,
|
|
7943
|
-
input: activation.input,
|
|
8664
|
+
const finishedEvent = (id, round, result) => {
|
|
8665
|
+
const event = {
|
|
8666
|
+
type: "node.finished",
|
|
8667
|
+
id,
|
|
7944
8668
|
round,
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
...foldersOf(options),
|
|
7948
|
-
budgetLeftUsd: deps.spend.left(),
|
|
7949
|
-
budgetLeftNow: deps.spend.budgetLeft,
|
|
7950
|
-
reserveBudget: (usd) => {
|
|
7951
|
-
deps.spend.reserve(id, usd, deps.nodeOf(activation)?.type !== "claude.leader");
|
|
7952
|
-
},
|
|
7953
|
-
budgetFreed: (signal) => deps.spend.freed(id, signal),
|
|
7954
|
-
quota: deps.spend.quota,
|
|
7955
|
-
ownSpend: (sessionId, total) => deps.spend.ownOf(id, sessionId, total),
|
|
7956
|
-
human: deps.human,
|
|
7957
|
-
hold: deps.holds.forNode(id, template),
|
|
7958
|
-
...deps.ports.forNode(id, template, cwd),
|
|
7959
|
-
...options.calls === void 0 ? {} : { calls: options.calls },
|
|
7960
|
-
emit: (event) => {
|
|
7961
|
-
deps.ports.owners.seen(id, event, cwd);
|
|
7962
|
-
if (event.type === "session.finished" && event.fork !== true) {
|
|
7963
|
-
const { sessionId, usage } = event.outcome;
|
|
7964
|
-
deps.spend.report(id, sessionId, usage.costUsd);
|
|
7965
|
-
}
|
|
7966
|
-
options.onEvent({
|
|
7967
|
-
type: "node.event",
|
|
7968
|
-
id,
|
|
7969
|
-
event
|
|
7970
|
-
});
|
|
7971
|
-
},
|
|
7972
|
-
signal: deps.stops.signalFor(id, template)
|
|
8669
|
+
ok: result.ok,
|
|
8670
|
+
output: result.output
|
|
7973
8671
|
};
|
|
7974
|
-
|
|
7975
|
-
if (
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
if (activation.item !== void 0) context.item = activation.item;
|
|
7979
|
-
const resume = options.replay?.cutOff?.(activation) ?? resumeOf(deps.nodeOf(activation), deps.sessionIds.get(id) ?? options.replay?.previousSession?.(activation));
|
|
7980
|
-
if (resume !== void 0) context.resume = resume;
|
|
7981
|
-
if (deps.nodeOf(activation)?.type === "claude.leader") context.ownership = createOwnership({
|
|
7982
|
-
graph,
|
|
7983
|
-
leader: id,
|
|
7984
|
-
runChild: deps.runChild,
|
|
7985
|
-
isRunning: (other) => deps.running.has(other),
|
|
7986
|
-
checkpoint: () => options.workspace?.checkpoint(id) ?? Promise.resolve(null),
|
|
7987
|
-
releaseClaims: () => {
|
|
7988
|
-
const held = context.claims?.held() ?? [];
|
|
7989
|
-
for (const resource of held) context.claims?.release(resource);
|
|
7990
|
-
return held;
|
|
7991
|
-
},
|
|
7992
|
-
lend: deps.slots.release,
|
|
7993
|
-
reclaim: deps.slots.take
|
|
7994
|
-
});
|
|
7995
|
-
return context;
|
|
8672
|
+
if (result.usage !== void 0) event.usage = result.usage;
|
|
8673
|
+
if (result.error !== void 0) event.error = result.error;
|
|
8674
|
+
event.end = endOf(result);
|
|
8675
|
+
return event;
|
|
7996
8676
|
};
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
const own = deps.spend.count(owner, {
|
|
8002
|
-
ok: true,
|
|
8003
|
-
output: {},
|
|
8004
|
-
sessionId,
|
|
8005
|
-
usage
|
|
8006
|
-
}, false);
|
|
8007
|
-
deps.summary(owner).costUsd += own.usage?.costUsd ?? 0;
|
|
8677
|
+
const failure = (error) => ({
|
|
8678
|
+
output: {
|
|
8679
|
+
status: "fail",
|
|
8680
|
+
summary: error
|
|
8008
8681
|
},
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8682
|
+
ok: false,
|
|
8683
|
+
error
|
|
8684
|
+
});
|
|
8685
|
+
/** The node's entry in the run summary, created on first use. */
|
|
8686
|
+
const summaryOf = (summaries, id) => {
|
|
8687
|
+
const existing = summaries.get(id);
|
|
8688
|
+
if (existing !== void 0) return existing;
|
|
8689
|
+
const created = {
|
|
8690
|
+
id,
|
|
8691
|
+
runs: 0,
|
|
8692
|
+
ok: null,
|
|
8693
|
+
output: null,
|
|
8694
|
+
costUsd: 0
|
|
8695
|
+
};
|
|
8696
|
+
summaries.set(id, created);
|
|
8697
|
+
return created;
|
|
8698
|
+
};
|
|
8699
|
+
/** A run's summary; a resumed run's leaders keep their forks' spend (ADR-063). */
|
|
8700
|
+
const createSummaries = (earlier) => {
|
|
8701
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
8702
|
+
const summary = (id) => summaryOf(summaries, id);
|
|
8703
|
+
for (const [id, costUsd] of earlier?.forks ?? []) summary(id).costUsd += costUsd;
|
|
8704
|
+
return {
|
|
8705
|
+
summaries,
|
|
8706
|
+
summary
|
|
8707
|
+
};
|
|
8708
|
+
};
|
|
8709
|
+
/** The tighter of the workflow's budget and what a caller left it, if any. */
|
|
8710
|
+
const runBudget = (graph, settings) => {
|
|
8711
|
+
const limits = [graph.workflow.limits?.budgetUsd, settings?.budgetCap].filter((limit) => limit !== void 0);
|
|
8712
|
+
return limits.length === 0 ? {} : { budgetUsd: Math.min(...limits) };
|
|
8713
|
+
};
|
|
8714
|
+
/** The `run.started` event: the workflow, its entries, nodes and inputs. */
|
|
8715
|
+
const runStartedEvent = (graph, runId, options) => ({
|
|
8716
|
+
type: "run.started",
|
|
8717
|
+
runId,
|
|
8718
|
+
workflow: graph.workflow.name,
|
|
8719
|
+
entries: graph.entries,
|
|
8720
|
+
nodes: declaredNodes(graph),
|
|
8721
|
+
edges: graph.edges.map(({ from, to, when, maxRounds }) => ({
|
|
8722
|
+
from,
|
|
8723
|
+
to,
|
|
8724
|
+
...when === void 0 ? {} : { when },
|
|
8725
|
+
...maxRounds === void 0 ? {} : { maxRounds }
|
|
8726
|
+
})),
|
|
8727
|
+
channels: graph.channels,
|
|
8728
|
+
inputs: options.inputs,
|
|
8729
|
+
...options.settings === void 0 ? {} : { settings: options.settings },
|
|
8730
|
+
...runBudget(graph, options.settings),
|
|
8731
|
+
...options.orkVersion === void 0 ? {} : { ork: options.orkVersion }
|
|
8016
8732
|
});
|
|
8733
|
+
/** The unread mail of nodes that never ran; a node that ran saw its notice. */
|
|
8734
|
+
const unreadOf = (nodes, unread) => {
|
|
8735
|
+
const ran = new Set(nodes.filter((node) => node.runs > 0).map((node) => node.id));
|
|
8736
|
+
const left = [...unread ?? []].filter(([node]) => !ran.has(node)).map(([node, messages]) => ({
|
|
8737
|
+
node,
|
|
8738
|
+
messages
|
|
8739
|
+
}));
|
|
8740
|
+
return left.length === 0 ? {} : { unread: left };
|
|
8741
|
+
};
|
|
8017
8742
|
/**
|
|
8018
|
-
* The
|
|
8019
|
-
*
|
|
8020
|
-
* says `resume`.
|
|
8743
|
+
* The run's result: failed when any reason was given, unless it stopped. A
|
|
8744
|
+
* skipped node or a handled failure leaves it `succeeded`, and is counted.
|
|
8021
8745
|
*/
|
|
8022
|
-
const
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8746
|
+
const runResult = (run) => ({
|
|
8747
|
+
runId: run.runId,
|
|
8748
|
+
status: run.stopped ?? (run.reasons.length > 0 ? "failed" : "succeeded"),
|
|
8749
|
+
costUsd: run.costUsd,
|
|
8750
|
+
nodes: run.nodes,
|
|
8751
|
+
reasons: run.reasons,
|
|
8752
|
+
worktrees: run.worktrees,
|
|
8753
|
+
...run.outputs === void 0 ? {} : { outputs: run.outputs },
|
|
8754
|
+
skipped: run.nodes.filter((node) => node.runs === 0).length,
|
|
8755
|
+
failuresHandled: run.failuresHandled,
|
|
8756
|
+
...unreadOf(run.nodes, run.unread)
|
|
8757
|
+
});
|
|
8758
|
+
/** A workflow's `outputs`, rendered as the run ends. */
|
|
8759
|
+
const renderOutputs = (outputs, scope) => outputs === void 0 ? void 0 : Object.fromEntries(Object.entries(outputs).map(([name, text]) => {
|
|
8760
|
+
const template = parseTemplate(text);
|
|
8761
|
+
return [name, template.ok ? renderTemplate(template.parts, scope) : text];
|
|
8762
|
+
}));
|
|
8763
|
+
/** Closes the run's worktrees; a failure is one more reason, not a crash. */
|
|
8764
|
+
const closeWorkspace = (workspace, reasons) => (workspace?.close() ?? Promise.resolve([])).catch((error) => {
|
|
8765
|
+
reasons.push(`cleaning up worktrees: ${String(error)}`);
|
|
8766
|
+
return [];
|
|
8767
|
+
});
|
|
8027
8768
|
// ../core/src/store/nest-items.ts
|
|
8028
8769
|
/** Whether a tree node is an item of a fanned node (`build.2`). */
|
|
8029
8770
|
const isItem = (node) => node.template !== void 0 && /\.\d+$/u.test(node.id);
|
|
@@ -8307,6 +9048,13 @@ const runTree = (events) => {
|
|
|
8307
9048
|
}
|
|
8308
9049
|
return roots;
|
|
8309
9050
|
};
|
|
9051
|
+
/** The nodes of a run waiting for a machine-wide slot now. */
|
|
9052
|
+
const slotWaitsOf = (events) => {
|
|
9053
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
9054
|
+
for (const event of events) if (event.type === "slot.waiting") waiting.add(event.id);
|
|
9055
|
+
else if (event.type === "slot.taken" || event.type === "node.finished") waiting.delete(event.id);
|
|
9056
|
+
return [...waiting];
|
|
9057
|
+
};
|
|
8310
9058
|
/** Folds a run's `ork pause` and `ork continue` commands (ADR-041). */
|
|
8311
9059
|
const pausedOf = (events) => {
|
|
8312
9060
|
let run = false;
|
|
@@ -8499,7 +9247,10 @@ const createStops = (run) => {
|
|
|
8499
9247
|
own.delete(id);
|
|
8500
9248
|
return stopped.delete(id) ? {
|
|
8501
9249
|
...result,
|
|
8502
|
-
...
|
|
9250
|
+
...failedWith(endFor("interrupted"), {
|
|
9251
|
+
status: "fail",
|
|
9252
|
+
summary: "stopped by the user"
|
|
9253
|
+
})
|
|
8503
9254
|
} : result;
|
|
8504
9255
|
}
|
|
8505
9256
|
};
|
|
@@ -8569,6 +9320,8 @@ const createClock = (options) => {
|
|
|
8569
9320
|
else resting.delete(id);
|
|
8570
9321
|
changed();
|
|
8571
9322
|
},
|
|
9323
|
+
/** Stands the node's clock (and its owners' and the run's) still for `wait`. */
|
|
9324
|
+
waitFor: asking,
|
|
8572
9325
|
/** The same channel, noting who waits on the user for how long. */
|
|
8573
9326
|
human: (channel) => ({
|
|
8574
9327
|
permit: (request) => asking(request.nodeId, () => channel.permit(request)),
|
|
@@ -8588,18 +9341,17 @@ const createClock = (options) => {
|
|
|
8588
9341
|
};
|
|
8589
9342
|
};
|
|
8590
9343
|
const timedOut = (timeout, partial) => {
|
|
8591
|
-
const
|
|
8592
|
-
return {
|
|
9344
|
+
const end = endFor("timed_out", { timeout });
|
|
9345
|
+
return endedWith({
|
|
8593
9346
|
...partial,
|
|
8594
9347
|
ok: false,
|
|
8595
|
-
error,
|
|
8596
9348
|
output: {
|
|
8597
9349
|
...partial?.output,
|
|
8598
9350
|
status: "fail",
|
|
8599
|
-
summary:
|
|
9351
|
+
summary: end.message,
|
|
8600
9352
|
timedOut: true
|
|
8601
9353
|
}
|
|
8602
|
-
};
|
|
9354
|
+
}, end);
|
|
8603
9355
|
};
|
|
8604
9356
|
/** Runs `start` under `timeout`, if it has one. */
|
|
8605
9357
|
const withTimeout = async (bound) => {
|
|
@@ -8650,9 +9402,28 @@ const watchRunTimeout = (watched) => {
|
|
|
8650
9402
|
cancelled.abort();
|
|
8651
9403
|
};
|
|
8652
9404
|
};
|
|
9405
|
+
// ../core/src/run/watch.ts
|
|
9406
|
+
/**
|
|
9407
|
+
* @file A `watch:` on a tool or MCP node (RFC-011): the node's look, repeated
|
|
9408
|
+
* until its condition holds, with no model turn. The last look is its output.
|
|
9409
|
+
* A failed look, such as a network error, is tried again up to `misses` in a
|
|
9410
|
+
* row; an answer the node didn't declare fails it at once, as waiting can't
|
|
9411
|
+
* mend that. The time between looks isn't work, so a pause keeps it and the
|
|
9412
|
+
* run's clock stands still. It records what changed, not every look.
|
|
9413
|
+
*/
|
|
9414
|
+
/**
|
|
9415
|
+
* The ends of a failed look that the next look may mend besides a transient
|
|
9416
|
+
* kind: what a tool's non-zero exit or an MCP call's error ends as.
|
|
9417
|
+
*/
|
|
9418
|
+
const LOOKS_AGAIN = /* @__PURE__ */ new Set([
|
|
9419
|
+
"command_failed",
|
|
9420
|
+
"auth",
|
|
9421
|
+
"tool_missing",
|
|
9422
|
+
"interrupted"
|
|
9423
|
+
]);
|
|
8653
9424
|
const SAID_LIMIT = 1024;
|
|
8654
9425
|
/** What a look says, for telling whether it changed and showing it. */
|
|
8655
|
-
const saidBy = (result, declared) => {
|
|
9426
|
+
const saidBy$1 = (result, declared) => {
|
|
8656
9427
|
const { json, exitCode, stdout, text } = result.output;
|
|
8657
9428
|
const value = declared !== void 0 && typeof json === "object" && json !== null ? Object.fromEntries(Object.keys(declared).map((name) => [name, Reflect.get(json, name)])) : json ?? {
|
|
8658
9429
|
exitCode,
|
|
@@ -8685,7 +9456,7 @@ const leftBehind = (signal, graceMs) => {
|
|
|
8685
9456
|
const runWatch = async (options) => {
|
|
8686
9457
|
const { id, watch, declared, look, hold, signal, graceMs, onEvent } = options;
|
|
8687
9458
|
const until = parseExpr(watch.until);
|
|
8688
|
-
if (!until.ok) return
|
|
9459
|
+
if (!until.ok) return failedWith(endFor("invalid_node", { reason: `watch.until: ${until.message}` }));
|
|
8689
9460
|
const every = options.everyMs ?? timeoutMs(watch.every) ?? 0;
|
|
8690
9461
|
const misses = watch.misses ?? 3;
|
|
8691
9462
|
const limit = new AbortController();
|
|
@@ -8706,17 +9477,16 @@ const runWatch = async (options) => {
|
|
|
8706
9477
|
};
|
|
8707
9478
|
const expiredResult = () => {
|
|
8708
9479
|
const error = `watched for ${watch.timeout}, and ${markValue(watch.until)} never held`;
|
|
8709
|
-
return {
|
|
9480
|
+
return endedWith({
|
|
8710
9481
|
...last,
|
|
8711
9482
|
ok: false,
|
|
8712
|
-
error,
|
|
8713
9483
|
output: {
|
|
8714
9484
|
...last?.output,
|
|
8715
9485
|
status: "fail",
|
|
8716
9486
|
summary: error,
|
|
8717
9487
|
timedOut: true
|
|
8718
9488
|
}
|
|
8719
|
-
};
|
|
9489
|
+
}, endFor("timed_out", { timeout: watch.timeout }, error));
|
|
8720
9490
|
};
|
|
8721
9491
|
/** Stands still while paused, and records the hold at once. */
|
|
8722
9492
|
const rest = async () => {
|
|
@@ -8761,11 +9531,14 @@ const runWatch = async (options) => {
|
|
|
8761
9531
|
for (;;) {
|
|
8762
9532
|
await hold?.wait("start", active);
|
|
8763
9533
|
const result = active.aborted ? null : await lookOnce();
|
|
8764
|
-
if (signal.aborted) return ended("stopped",
|
|
9534
|
+
if (signal.aborted) return ended("stopped", failedWith(endFor("interrupted"), {
|
|
9535
|
+
status: "fail",
|
|
9536
|
+
summary: "stopped while watching"
|
|
9537
|
+
}));
|
|
8765
9538
|
if (limit.signal.aborted || result === null) return ended("timed_out", expiredResult());
|
|
8766
9539
|
looks += 1;
|
|
8767
9540
|
last = result;
|
|
8768
|
-
const said = saidBy(result, declared);
|
|
9541
|
+
const said = saidBy$1(result, declared);
|
|
8769
9542
|
if (said !== lastSaid) {
|
|
8770
9543
|
lastSaid = said;
|
|
8771
9544
|
onEvent({
|
|
@@ -8781,7 +9554,7 @@ const runWatch = async (options) => {
|
|
|
8781
9554
|
missed = 0;
|
|
8782
9555
|
if (holds(result.output)) return ended("met", result);
|
|
8783
9556
|
} else {
|
|
8784
|
-
if (!(result.transient === true || result.output["timedOut"] === true)) return ended("failed", result);
|
|
9557
|
+
if (!(result.transient === true || LOOKS_AGAIN.has(result.end?.kind) || result.output["timedOut"] === true)) return ended("failed", result);
|
|
8785
9558
|
missed += 1;
|
|
8786
9559
|
if (missed >= misses) return ended("failed", {
|
|
8787
9560
|
...result,
|
|
@@ -8798,19 +9571,36 @@ const runWatch = async (options) => {
|
|
|
8798
9571
|
const repeatsCall = (event) => event.type === "tool.called" || event.type === "tool.result";
|
|
8799
9572
|
// ../core/src/run/execute.ts
|
|
8800
9573
|
/** Runs an activation; control nodes pass their input through. */
|
|
8801
|
-
const
|
|
9574
|
+
const saidBy = (error) => error instanceof Error ? error.message : String(error);
|
|
9575
|
+
/**
|
|
9576
|
+
* A thrown error's end: a failed git or missing program by its text, else
|
|
9577
|
+
* `fallback`.
|
|
9578
|
+
*/
|
|
9579
|
+
const thrown = (error, fallback) => {
|
|
9580
|
+
const text = saidBy(error);
|
|
9581
|
+
const read = endOf({
|
|
9582
|
+
ok: false,
|
|
9583
|
+
output: {},
|
|
9584
|
+
error: text
|
|
9585
|
+
});
|
|
9586
|
+
return failedWith(read.kind === "unknown" ? endFor(fallback, { reason: text }, text) : read);
|
|
9587
|
+
};
|
|
9588
|
+
/** Runs an activation, its end's detail scrubbed of the run's paths. */
|
|
9589
|
+
const createExecute = (deps) => async (activation, round) => scrubbedEnd(await executeOne(deps, activation, round), [deps.options.cwd, deps.options.projectRoot ?? ""]);
|
|
9590
|
+
const executeOne = async (deps, activation, round) => {
|
|
8802
9591
|
const { options, contextDeps, clock, graceMs, nodeOf } = deps;
|
|
8803
9592
|
const node = nodeOf(activation);
|
|
8804
|
-
if (node === void 0) return
|
|
9593
|
+
if (node === void 0) return failedWith(endFor("unknown", {}, `unknown node ${activation.id}`));
|
|
8805
9594
|
if (node.type.startsWith("control.")) return passThrough(activation.input);
|
|
8806
9595
|
const executor = options.executors[node.type];
|
|
8807
|
-
if (executor === void 0) return
|
|
9596
|
+
if (executor === void 0) return failedWith(endFor("unknown", {}, `no executor for ${node.type}`));
|
|
8808
9597
|
try {
|
|
8809
9598
|
const forEach = activation.item === void 0 ? forEachOf(node) : void 0;
|
|
8810
9599
|
if (forEach !== void 0) return await deps.fanOut(activation, forEach, round);
|
|
8811
9600
|
const { workspace } = options;
|
|
8812
|
-
const place = await workspace?.prepare(activation.id, activation.template) ?? { cwd: options.cwd };
|
|
8813
|
-
|
|
9601
|
+
const place = await (workspace?.prepare(activation.id, activation.template) ?? Promise.resolve({ cwd: options.cwd })).catch((error) => thrown(error, "invalid_node"));
|
|
9602
|
+
if ("ok" in place) return place;
|
|
9603
|
+
const context = nodeContext(contextDeps, activation, place.cwd, round, clock);
|
|
8814
9604
|
const once = (signal, emit) => withTimeout({
|
|
8815
9605
|
timeout: timeoutOf(node, options.graph.workflow),
|
|
8816
9606
|
clock: clock.forNode(activation.id),
|
|
@@ -8842,9 +9632,10 @@ const createExecute = (deps) => async (activation, round) => {
|
|
|
8842
9632
|
},
|
|
8843
9633
|
everyMs: options.watchEveryMs
|
|
8844
9634
|
});
|
|
8845
|
-
|
|
9635
|
+
const ended = result.ok || !context.signal.aborted ? result : endedWith(result, stoppedEnd(context.signal));
|
|
9636
|
+
return await workspace?.finish(activation.id, ended) ?? ended;
|
|
8846
9637
|
} catch (error) {
|
|
8847
|
-
return
|
|
9638
|
+
return thrown(error, "unknown");
|
|
8848
9639
|
}
|
|
8849
9640
|
};
|
|
8850
9641
|
// ../core/src/run/bus-calls.ts
|
|
@@ -9358,7 +10149,7 @@ const failed = (id, why) => {
|
|
|
9358
10149
|
return reason === "" ? `${id} failed` : `${id} failed: ${reason}`;
|
|
9359
10150
|
};
|
|
9360
10151
|
/** A program that ran says how it exited; anything else has its own reason. */
|
|
9361
|
-
const toolLine = (id, result) => {
|
|
10152
|
+
const toolLine$1 = (id, result) => {
|
|
9362
10153
|
const { output, ok, error } = result;
|
|
9363
10154
|
const code = output["exitCode"];
|
|
9364
10155
|
if (!(typeof code === "number" && code >= 0 && output["timedOut"] !== true && (ok || code !== 0))) return ok ? `${id} passed` : failed(id, error ?? "");
|
|
@@ -9368,7 +10159,7 @@ const toolLine = (id, result) => {
|
|
|
9368
10159
|
/** Ork's own line about a result, when a rule gives no body. */
|
|
9369
10160
|
const defaultRuleBody = (id, node, result) => {
|
|
9370
10161
|
const { output, ok } = result;
|
|
9371
|
-
if (node !== void 0 && isToolNode(node)) return toolLine(id, result);
|
|
10162
|
+
if (node !== void 0 && isToolNode(node)) return toolLine$1(id, result);
|
|
9372
10163
|
const summary = output["summary"];
|
|
9373
10164
|
const said = typeof summary === "string" ? summary : "";
|
|
9374
10165
|
if (!ok) return failed(id, result.error ?? said);
|
|
@@ -9636,7 +10427,7 @@ const startRun = (options) => {
|
|
|
9636
10427
|
expire: (reason) => {
|
|
9637
10428
|
stop("timed_out", reason);
|
|
9638
10429
|
options.calls?.stopAll();
|
|
9639
|
-
abort.abort();
|
|
10430
|
+
abort.abort(runTimedOut(limits.timeout));
|
|
9640
10431
|
settle();
|
|
9641
10432
|
},
|
|
9642
10433
|
leave: () => {
|
|
@@ -10816,12 +11607,13 @@ const parseReport = (value) => {
|
|
|
10816
11607
|
// ../core/src/run/brief.ts
|
|
10817
11608
|
/**
|
|
10818
11609
|
* @file What a Claude node is asked: its brief, rendered against the run's
|
|
10819
|
-
* inputs and outputs, and
|
|
10820
|
-
*
|
|
10821
|
-
*
|
|
10822
|
-
* its
|
|
10823
|
-
*
|
|
10824
|
-
* worktree or edges
|
|
11610
|
+
* inputs and outputs, the model and effort it runs on (a fanned node's from
|
|
11611
|
+
* its item), and its `claude.subagent` children as Claude Code subagents of
|
|
11612
|
+
* its session (the SDK's `agents`, the CLI's `--agents`). A subagent's role
|
|
11613
|
+
* (its agent's instructions), `instructions` and `prompt` are its system
|
|
11614
|
+
* prompt, and its `description` says when the session hands it work with the
|
|
11615
|
+
* Agent tool; it runs inside that session, with no process, worktree or edges
|
|
11616
|
+
* of its own.
|
|
10825
11617
|
*/
|
|
10826
11618
|
/** The tool a session hands a subagent work with. */
|
|
10827
11619
|
const AGENT_TOOL = "Agent";
|
|
@@ -10850,6 +11642,23 @@ const renderBrief = (text, inputs, outputs, before, item) => {
|
|
|
10850
11642
|
warnings
|
|
10851
11643
|
};
|
|
10852
11644
|
};
|
|
11645
|
+
/**
|
|
11646
|
+
* The model and effort a session asks for: the node's own, rendered with its
|
|
11647
|
+
* item on a fanned node (`{{item.model}}`), else the workflow's defaults. An
|
|
11648
|
+
* item's value that is no model name or effort level fails it.
|
|
11649
|
+
*/
|
|
11650
|
+
const modelOf = (node, defaults, run) => {
|
|
11651
|
+
const scope = runScope(run.inputs, run.outputs ?? {}, void 0, run.item);
|
|
11652
|
+
const model = itemSetting("model", node.model, scope);
|
|
11653
|
+
if (!model.ok) return model;
|
|
11654
|
+
const effort = itemEffort(node.effort, scope);
|
|
11655
|
+
if (!effort.ok) return effort;
|
|
11656
|
+
return {
|
|
11657
|
+
ok: true,
|
|
11658
|
+
model: model.value ?? defaults.model,
|
|
11659
|
+
effort: effort.value ?? defaults.effort
|
|
11660
|
+
};
|
|
11661
|
+
};
|
|
10853
11662
|
const rendered = (text, inputs, outputs) => {
|
|
10854
11663
|
const brief = renderBrief(text, inputs, outputs);
|
|
10855
11664
|
return brief.ok ? brief.text : (text ?? "").trim();
|
|
@@ -10929,7 +11738,7 @@ const mailboxInstructions = (mailbox, owner) => {
|
|
|
10929
11738
|
const verifyInstructions = (node) => {
|
|
10930
11739
|
if (!("verify" in node) || node.verify === void 0) return [];
|
|
10931
11740
|
const { command } = node.verify;
|
|
10932
|
-
return [`- A report that is not \`fail\` or \`changes_requested\` is accepted only if ${command === void 0 || command.includes("{{") ? "a check the workflow defines" : `\`${command}\``} passes when you report. If it fails, you are told why and asked to fix it, so run it yourself first if you can
|
|
11741
|
+
return [`- A report that is not \`fail\` or \`changes_requested\` is accepted only if ${command === void 0 || command.includes("{{") ? "a check the workflow defines" : `\`${command}\``} passes when you report. If it fails, you are told why and asked to fix it, ${(command !== void 0 && node.tools?.deny?.includes(heavyRule(command))) === true ? "so run targeted checks (such as the tests you changed) first: Ork runs the full check, and this session may not." : "so run it yourself first if you can."}`];
|
|
10933
11742
|
};
|
|
10934
11743
|
/** Ork's own instructions for a node, kept short: they cost tokens every turn. */
|
|
10935
11744
|
const harnessInstructions = (workflow, id, node, options = {}) => {
|
|
@@ -11026,6 +11835,11 @@ const planNodeSession = (workflow, id, options) => {
|
|
|
11026
11835
|
ok: false,
|
|
11027
11836
|
error: `\`${id}\` uses worktree isolation, which a run on its own does not set up; run the whole workflow, or pass \`--in-place\` to run it in this checkout`
|
|
11028
11837
|
};
|
|
11838
|
+
const models = modelOf(node, defaults, options);
|
|
11839
|
+
if (!models.ok) return {
|
|
11840
|
+
ok: false,
|
|
11841
|
+
error: models.error
|
|
11842
|
+
};
|
|
11029
11843
|
const warnings = [];
|
|
11030
11844
|
const brief = renderBrief(node.prompt, options.inputs, options.outputs, options.before, options.item);
|
|
11031
11845
|
if (!brief.ok) return {
|
|
@@ -11073,10 +11887,8 @@ const planNodeSession = (workflow, id, options) => {
|
|
|
11073
11887
|
spec.agents = agents;
|
|
11074
11888
|
spec.tools.only?.push(AGENT_TOOL);
|
|
11075
11889
|
}
|
|
11076
|
-
|
|
11077
|
-
if (
|
|
11078
|
-
const effort = node.effort ?? defaults.effort;
|
|
11079
|
-
if (effort !== void 0) spec.effort = effort;
|
|
11890
|
+
if (models.model !== void 0) spec.model = models.model;
|
|
11891
|
+
if (models.effort !== void 0) spec.effort = models.effort;
|
|
11080
11892
|
const maxTurns = node.maxTurns ?? defaults.maxTurns;
|
|
11081
11893
|
if (maxTurns !== void 0) spec.maxTurns = maxTurns;
|
|
11082
11894
|
const budgets = [node.budgetUsd ?? defaults.budgetUsd, options.budgetLeftUsd].filter((b) => b !== void 0);
|
|
@@ -11279,6 +12091,112 @@ const explainWorkflowText = (text, file, nodeId, where) => {
|
|
|
11279
12091
|
sources: sourcesOf(result.explanation, place, where.home ?? homedir())
|
|
11280
12092
|
} : result;
|
|
11281
12093
|
};
|
|
12094
|
+
// ../core/src/usage/quota.ts
|
|
12095
|
+
const WINDOWS = [
|
|
12096
|
+
"day",
|
|
12097
|
+
"week",
|
|
12098
|
+
"month"
|
|
12099
|
+
];
|
|
12100
|
+
const FIELDS = {
|
|
12101
|
+
day: "dayUsd",
|
|
12102
|
+
week: "weekUsd",
|
|
12103
|
+
month: "monthUsd"
|
|
12104
|
+
};
|
|
12105
|
+
/** Where a window began and when the next begins, in local time. */
|
|
12106
|
+
const windowOf = (window, now) => {
|
|
12107
|
+
const [year, month, day] = [
|
|
12108
|
+
now.getFullYear(),
|
|
12109
|
+
now.getMonth(),
|
|
12110
|
+
now.getDate()
|
|
12111
|
+
];
|
|
12112
|
+
if (window === "month") return {
|
|
12113
|
+
since: new Date(year, month, 1),
|
|
12114
|
+
until: new Date(year, month + 1)
|
|
12115
|
+
};
|
|
12116
|
+
if (window === "day") return {
|
|
12117
|
+
since: new Date(year, month, day),
|
|
12118
|
+
until: new Date(year, month, day + 1)
|
|
12119
|
+
};
|
|
12120
|
+
const monday = day - (now.getDay() + 6) % 7;
|
|
12121
|
+
return {
|
|
12122
|
+
since: new Date(year, month, monday),
|
|
12123
|
+
until: new Date(year, month, monday + 7)
|
|
12124
|
+
};
|
|
12125
|
+
};
|
|
12126
|
+
/** The caps that apply: the user's on everything, and the project's on itself. */
|
|
12127
|
+
const capsOf = (user, project, root) => WINDOWS.flatMap((window) => {
|
|
12128
|
+
const field = FIELDS[window];
|
|
12129
|
+
const everywhere = user?.[field];
|
|
12130
|
+
const here = project?.[field];
|
|
12131
|
+
const caps = [];
|
|
12132
|
+
if (everywhere !== void 0) caps.push({
|
|
12133
|
+
window,
|
|
12134
|
+
capUsd: everywhere
|
|
12135
|
+
});
|
|
12136
|
+
if (here !== void 0 && root !== void 0) caps.push({
|
|
12137
|
+
window,
|
|
12138
|
+
capUsd: here,
|
|
12139
|
+
project: root
|
|
12140
|
+
});
|
|
12141
|
+
return caps;
|
|
12142
|
+
});
|
|
12143
|
+
/** Each cap with what has been spent against it. */
|
|
12144
|
+
const standingOf = (store, caps, now) => caps.map((cap) => {
|
|
12145
|
+
const { since, until } = windowOf(cap.window, now);
|
|
12146
|
+
const spentUsd = store.spentSince(since.toISOString(), cap.project);
|
|
12147
|
+
return {
|
|
12148
|
+
...cap,
|
|
12149
|
+
spentUsd,
|
|
12150
|
+
leftUsd: cap.capUsd - spentUsd,
|
|
12151
|
+
resetsAt: until.toISOString()
|
|
12152
|
+
};
|
|
12153
|
+
});
|
|
12154
|
+
/** Dollars: cents, or more digits for an amount under a dollar. */
|
|
12155
|
+
const usd = (amount) => `$${amount.toFixed(amount > 0 && amount < 1 ? 4 : 2)}`;
|
|
12156
|
+
const at = (iso) => new Date(iso).toLocaleString(void 0, {
|
|
12157
|
+
dateStyle: "medium",
|
|
12158
|
+
timeStyle: "short"
|
|
12159
|
+
});
|
|
12160
|
+
const capReason = (spent) => `${spent.project === void 0 ? "your" : "this project's"} ${spent.window} quota of ${usd(spent.capUsd)} is spent (${usd(spent.spentUsd)}); it lifts on ${at(spent.resetsAt)}, or raise it in ${spent.project === void 0 ? "~/.ork/settings.yaml" : ".ork/project.yaml"}`;
|
|
12161
|
+
/** What a run or a call refused at its start says first, for its caller to read. */
|
|
12162
|
+
const NOT_STARTED = "Not started: ";
|
|
12163
|
+
/** Why a plan limit stops a run. */
|
|
12164
|
+
const planReason = (limit) => `your Claude plan's ${limit.window.replaceAll("_", " ")} limit was reached${limit.resetsAt === void 0 ? "" : `; it resets on ${at(limit.resetsAt)}, or use ork usage --clear-plan-limits if it has lifted`}`;
|
|
12165
|
+
/**
|
|
12166
|
+
* The windows that hold the whole account. One for a single model, such as
|
|
12167
|
+
* `seven_day_opus`, stops only the session it refused: other models may be
|
|
12168
|
+
* free.
|
|
12169
|
+
*/
|
|
12170
|
+
const ACCOUNT_WINDOWS = /* @__PURE__ */ new Set([
|
|
12171
|
+
"five_hour",
|
|
12172
|
+
"seven_day",
|
|
12173
|
+
"overage",
|
|
12174
|
+
"plan"
|
|
12175
|
+
]);
|
|
12176
|
+
/** The plan limits in force, the one that lifts last first. */
|
|
12177
|
+
const refusing = (limits, now) => limits.filter((limit) => limit.status === "rejected" && ACCOUNT_WINDOWS.has(limit.window) && limit.resetsAt !== void 0 && Date.parse(limit.resetsAt) > now.getTime()).toSorted((a, b) => String(b.resetsAt).localeCompare(String(a.resetsAt)));
|
|
12178
|
+
/** How long a report with no reset time stays worth showing. */
|
|
12179
|
+
const SHOWN_MS = 864e5;
|
|
12180
|
+
/** What the caps and the plan allow a run now. */
|
|
12181
|
+
const limitsNow = (store, caps, now) => {
|
|
12182
|
+
const standing = standingOf(store, caps, now);
|
|
12183
|
+
const [plan] = refusing(store.planLimits(), now);
|
|
12184
|
+
const spent = standing.find((cap) => cap.leftUsd <= 0);
|
|
12185
|
+
const blocked = plan === void 0 ? spent === void 0 ? void 0 : capReason(spent) : planReason(plan);
|
|
12186
|
+
return {
|
|
12187
|
+
...standing.length === 0 ? {} : { leftUsd: Math.min(...standing.map((cap) => cap.leftUsd)) },
|
|
12188
|
+
...blocked === void 0 ? {} : { blocked }
|
|
12189
|
+
};
|
|
12190
|
+
};
|
|
12191
|
+
/** What `ork usage` and the panel show. */
|
|
12192
|
+
const limitsReport = (store, { caps, problems }, now) => ({
|
|
12193
|
+
problems,
|
|
12194
|
+
caps: standingOf(store, caps, now),
|
|
12195
|
+
plan: store.planLimits().filter((limit) => {
|
|
12196
|
+
if (limit.status === "allowed") return false;
|
|
12197
|
+
return (limit.resetsAt === void 0 ? Date.parse(limit.at) + SHOWN_MS : Date.parse(limit.resetsAt)) > now.getTime();
|
|
12198
|
+
})
|
|
12199
|
+
});
|
|
11282
12200
|
// ../core/src/remote/follow.ts
|
|
11283
12201
|
/**
|
|
11284
12202
|
* @file Following a run's events from another process (`ork logs -f`, the local
|
|
@@ -11292,18 +12210,16 @@ const followEvents = async (store, runId, onEvent, options) => {
|
|
|
11292
12210
|
seq: options.after ?? 0,
|
|
11293
12211
|
finished: false
|
|
11294
12212
|
};
|
|
11295
|
-
const done = new AbortController();
|
|
11296
12213
|
const emit = (stored) => {
|
|
11297
12214
|
if (stored.seq <= seen.seq) return;
|
|
11298
12215
|
seen.seq = stored.seq;
|
|
11299
12216
|
onEvent(stored);
|
|
11300
|
-
|
|
11301
|
-
|
|
11302
|
-
|
|
11303
|
-
}
|
|
12217
|
+
const { type } = stored.event;
|
|
12218
|
+
if (type === "run.finished") seen.finished = true;
|
|
12219
|
+
else if (type === "run.started" || type === "run.resumed") seen.finished = false;
|
|
11304
12220
|
};
|
|
11305
12221
|
const aborted = () => options.signal?.aborted === true;
|
|
11306
|
-
/** Takes what the store has; true once
|
|
12222
|
+
/** Takes what the store has; true once the run's latest go has finished. */
|
|
11307
12223
|
const drain = () => {
|
|
11308
12224
|
for (const stored of store.events(runId, seen.seq)) emit(stored);
|
|
11309
12225
|
return seen.finished;
|
|
@@ -11312,7 +12228,11 @@ const followEvents = async (store, runId, onEvent, options) => {
|
|
|
11312
12228
|
if (drain()) return "finished";
|
|
11313
12229
|
if (!options.follow) return "stored";
|
|
11314
12230
|
if (aborted()) return "aborted";
|
|
11315
|
-
const
|
|
12231
|
+
const done = new AbortController();
|
|
12232
|
+
const via = await followSocket(socketPath(runId, options.home), seen.seq, (stored) => {
|
|
12233
|
+
emit(stored);
|
|
12234
|
+
if (seen.finished) done.abort();
|
|
12235
|
+
}, options.signal === void 0 ? done.signal : AbortSignal.any([options.signal, done.signal]));
|
|
11316
12236
|
if (seen.finished) return "finished";
|
|
11317
12237
|
if (aborted()) return "aborted";
|
|
11318
12238
|
if (via === "ended") continue;
|
|
@@ -11362,31 +12282,58 @@ const noWait = (id) => ({
|
|
|
11362
12282
|
run: id
|
|
11363
12283
|
}
|
|
11364
12284
|
});
|
|
11365
|
-
/**
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
12285
|
+
/**
|
|
12286
|
+
* A call that could not start its run: refused by a limit across runs
|
|
12287
|
+
* (RFC-007), or something the calling node asks for that isn't there.
|
|
12288
|
+
*/
|
|
12289
|
+
const startFailed = (error) => failedWith(error.includes("Not started: ") ? endFor("plan_limit", {}, error) : endFor("invalid_node", { reason: error }), {
|
|
12290
|
+
status: "fail",
|
|
12291
|
+
summary: error
|
|
12292
|
+
});
|
|
12293
|
+
/** How a run that ended `status` ended, when its nodes don't say. */
|
|
12294
|
+
const STATUS_KINDS = {
|
|
12295
|
+
succeeded: "done",
|
|
12296
|
+
failed: "unknown",
|
|
12297
|
+
budget_exhausted: "budget_limit",
|
|
12298
|
+
quota_exhausted: "plan_limit",
|
|
12299
|
+
timed_out: "timed_out",
|
|
12300
|
+
interrupted: "interrupted"
|
|
12301
|
+
};
|
|
12302
|
+
/**
|
|
12303
|
+
* How a called run ended, carried up to the node that called it: a limit the
|
|
12304
|
+
* run stopped at, or else how its first failed node ended.
|
|
12305
|
+
*/
|
|
12306
|
+
const runEnd = (result, detail) => {
|
|
12307
|
+
const kind = STATUS_KINDS[result.status];
|
|
12308
|
+
const node = result.nodes.find((entry) => entry.ok === false);
|
|
12309
|
+
return kind === "unknown" && node !== void 0 ? endOf({
|
|
12310
|
+
ok: false,
|
|
12311
|
+
output: node.output ?? {},
|
|
12312
|
+
...node.error === void 0 ? {} : { error: node.error },
|
|
12313
|
+
...node.end === void 0 ? {} : { end: node.end }
|
|
12314
|
+
}) : endFor(kind, {}, detail);
|
|
11377
12315
|
};
|
|
12316
|
+
/** A node's report as a call's result; a failing one says why. */
|
|
12317
|
+
const reportResult = (id, output, ok, usage, end = endFor(ok ? "done" : "reported_failure")) => endedWith({
|
|
12318
|
+
ok,
|
|
12319
|
+
output: {
|
|
12320
|
+
...output,
|
|
12321
|
+
run: id
|
|
12322
|
+
},
|
|
12323
|
+
usage
|
|
12324
|
+
}, end);
|
|
11378
12325
|
/**
|
|
11379
12326
|
* What the node last reported, for a caller coming back to the run: a resumed
|
|
11380
12327
|
* run hands its kept results back with no new events, so none would come.
|
|
11381
12328
|
*/
|
|
11382
12329
|
const storedReport = (watch, id, node) => {
|
|
11383
12330
|
const found = watch.store.events(id).flatMap(({ event }) => event.type === "node.finished" && event.id === node && event.ok ? [event.output] : []).at(-1);
|
|
11384
|
-
return found === void 0 ? void 0 : reportResult(id,
|
|
12331
|
+
return found === void 0 ? void 0 : reportResult(id, found, true, usageOf(watch.newCost(id)));
|
|
11385
12332
|
};
|
|
11386
12333
|
const resultOf = (id, result, cost) => {
|
|
11387
12334
|
const ok = result.status === "succeeded";
|
|
11388
12335
|
const summary = ok ? `Run ${id} succeeded.` : `Run ${id} ended ${result.status}: ${result.reasons.join("; ")}`;
|
|
11389
|
-
return {
|
|
12336
|
+
return endedWith({
|
|
11390
12337
|
ok,
|
|
11391
12338
|
output: {
|
|
11392
12339
|
status: ok ? "pass" : "fail",
|
|
@@ -11395,9 +12342,8 @@ const resultOf = (id, result, cost) => {
|
|
|
11395
12342
|
runStatus: result.status,
|
|
11396
12343
|
outputs: result.outputs ?? {}
|
|
11397
12344
|
},
|
|
11398
|
-
usage: usageOf(cost)
|
|
11399
|
-
|
|
11400
|
-
};
|
|
12345
|
+
usage: usageOf(cost)
|
|
12346
|
+
}, ok ? endFor("done") : runEnd(result, summary));
|
|
11401
12347
|
};
|
|
11402
12348
|
/** The first thing `pick` finds in the run's events after `after`, if any. */
|
|
11403
12349
|
const followRun = async ({ store, home }, id, after, signal, pick) => {
|
|
@@ -11420,9 +12366,12 @@ const waitForRun = async (watch, id, after, signal) => {
|
|
|
11420
12366
|
const cost = watch.newCost(id);
|
|
11421
12367
|
if (result !== void 0) return resultOf(id, result, cost);
|
|
11422
12368
|
const [run] = watch.store.lookup(id);
|
|
11423
|
-
const
|
|
12369
|
+
const said = `Run ${id} ended without a result (${run === void 0 ? "gone" : liveStatus(run)}).`;
|
|
11424
12370
|
return {
|
|
11425
|
-
...
|
|
12371
|
+
...failedWith(endFor("unknown", {}, said), {
|
|
12372
|
+
status: "fail",
|
|
12373
|
+
summary: said
|
|
12374
|
+
}),
|
|
11426
12375
|
usage: usageOf(cost)
|
|
11427
12376
|
};
|
|
11428
12377
|
};
|
|
@@ -11431,20 +12380,34 @@ const waitForNode = async (watch, id, node, after, signal) => {
|
|
|
11431
12380
|
const seen = await followRun(watch, id, after, signal, (event) => {
|
|
11432
12381
|
if (event.type === "node.finished" && event.id === node) return {
|
|
11433
12382
|
ok: event.ok,
|
|
11434
|
-
output: event.output
|
|
12383
|
+
output: event.output,
|
|
12384
|
+
end: endOf(event)
|
|
11435
12385
|
};
|
|
11436
12386
|
return event.type === "run.finished" ? { ended: event.result } : void 0;
|
|
11437
12387
|
});
|
|
11438
12388
|
const usage = usageOf(watch.newCost(id));
|
|
11439
|
-
if (seen === void 0)
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
12389
|
+
if (seen === void 0) {
|
|
12390
|
+
const said = `Run ${id} ended before ${node} reported.`;
|
|
12391
|
+
return {
|
|
12392
|
+
...failedWith(endFor("unknown", {}, said), {
|
|
12393
|
+
status: "fail",
|
|
12394
|
+
summary: said
|
|
12395
|
+
}),
|
|
12396
|
+
usage
|
|
12397
|
+
};
|
|
12398
|
+
}
|
|
12399
|
+
if ("ended" in seen) {
|
|
12400
|
+
const said = `${node} of run ${id} did not report: the run ended ${seen.ended.status}.`;
|
|
12401
|
+
const end = seen.ended.status === "succeeded" ? endFor("no_report", {}, said) : runEnd(seen.ended, said);
|
|
12402
|
+
return {
|
|
12403
|
+
...failedWith(end, {
|
|
12404
|
+
status: "fail",
|
|
12405
|
+
summary: said
|
|
12406
|
+
}),
|
|
12407
|
+
usage
|
|
12408
|
+
};
|
|
12409
|
+
}
|
|
12410
|
+
return reportResult(id, seen.output, seen.ok, usage, seen.end);
|
|
11448
12411
|
};
|
|
11449
12412
|
// ../core/src/remote/detach.ts
|
|
11450
12413
|
/**
|
|
@@ -11976,18 +12939,18 @@ const usageRowsOf = (run, events) => {
|
|
|
11976
12939
|
}
|
|
11977
12940
|
return rows;
|
|
11978
12941
|
};
|
|
11979
|
-
const SOURCE$
|
|
12942
|
+
const SOURCE$2 = `SELECT seq, at, data FROM events
|
|
11980
12943
|
WHERE run_id = ? AND (
|
|
11981
12944
|
type IN ('run.started', 'node.started', 'node.finished')
|
|
11982
12945
|
OR (type = 'node.event'
|
|
11983
12946
|
AND json_extract(data, '$.event.type') IN ('session.started', 'session.finished'))
|
|
11984
12947
|
) ORDER BY seq`;
|
|
11985
|
-
const INSERT = `INSERT OR IGNORE INTO usage (
|
|
12948
|
+
const INSERT$1 = `INSERT OR IGNORE INTO usage (
|
|
11986
12949
|
run_id, seq, at, project, workflow, node, template, model, driver, kind,
|
|
11987
12950
|
round, failed, cost_usd, input_tokens, output_tokens, cache_read_tokens,
|
|
11988
12951
|
cache_write_tokens, turns, duration_ms
|
|
11989
12952
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
11990
|
-
const rowsFromEvents = (db, run) => usageRowsOf(run, db.prepare(SOURCE$
|
|
12953
|
+
const rowsFromEvents = (db, run) => usageRowsOf(run, db.prepare(SOURCE$2).all(run.id).map((r) => ({
|
|
11991
12954
|
seq: Number(r["seq"]),
|
|
11992
12955
|
at: text(r, "at"),
|
|
11993
12956
|
event: eventOf(text(r, "data"))
|
|
@@ -11997,7 +12960,7 @@ const writeRow = (insert, row) => {
|
|
|
11997
12960
|
};
|
|
11998
12961
|
/** Fills the ledger from the runs recorded before it existed. */
|
|
11999
12962
|
const backfillUsage = (db) => {
|
|
12000
|
-
const insert = db.prepare(INSERT);
|
|
12963
|
+
const insert = db.prepare(INSERT$1);
|
|
12001
12964
|
for (const run of db.prepare("SELECT id, project, workflow FROM runs").all()) {
|
|
12002
12965
|
const rows = rowsFromEvents(db, {
|
|
12003
12966
|
id: text(run, "id"),
|
|
@@ -12059,7 +13022,7 @@ const alive = (row) => {
|
|
|
12059
13022
|
};
|
|
12060
13023
|
/** The ledger's queries; `recordUsage` is the store's own, called on append. */
|
|
12061
13024
|
const usageQueries = (db) => {
|
|
12062
|
-
const insert = db.prepare(INSERT);
|
|
13025
|
+
const insert = db.prepare(INSERT$1);
|
|
12063
13026
|
const spentAll = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) AS spent FROM usage WHERE at >= ?");
|
|
12064
13027
|
const spentHere = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) AS spent FROM usage WHERE at >= ? AND project = ?");
|
|
12065
13028
|
const runById = db.prepare("SELECT id, project, workflow FROM runs WHERE id = ?");
|
|
@@ -12384,21 +13347,26 @@ const resumeNode = async (talk, run, request, node, signal) => {
|
|
|
12384
13347
|
const refused = await talk.resumeSession(run, node, body, signal);
|
|
12385
13348
|
if (refused !== null) return failure(refused);
|
|
12386
13349
|
const finished = store.events(run.id, from).flatMap(({ event }) => event.type === "node.event" && event.event.type === "session.finished" && event.id === node ? [event.event.outcome] : []).at(-1);
|
|
12387
|
-
if (finished?.report === null || finished === void 0)
|
|
13350
|
+
if (finished?.report === null || finished === void 0) {
|
|
13351
|
+
const said = `${node} ended without a report when resumed.`;
|
|
13352
|
+
return failedWith(endFor("no_report", {}, said), {
|
|
13353
|
+
status: "fail",
|
|
13354
|
+
summary: said
|
|
13355
|
+
});
|
|
13356
|
+
}
|
|
12388
13357
|
const ok = finished.report.status !== "fail";
|
|
12389
13358
|
const own = usageRowsOf(run, store.events(run.id)).filter((row) => row.seq > from && row.node === node).reduce((sum, row) => sum + row.costUsd, 0);
|
|
12390
|
-
return {
|
|
13359
|
+
return endedWith({
|
|
12391
13360
|
ok,
|
|
12392
13361
|
output: {
|
|
12393
13362
|
...finished.report,
|
|
12394
13363
|
run: run.id
|
|
12395
13364
|
},
|
|
12396
|
-
...ok ? {} : { error: finished.report.summary },
|
|
12397
13365
|
usage: {
|
|
12398
13366
|
...finished.usage,
|
|
12399
13367
|
costUsd: own + talk.newCost(run.id)
|
|
12400
13368
|
}
|
|
12401
|
-
};
|
|
13369
|
+
}, endFor(ok ? "done" : "reported_failure"));
|
|
12402
13370
|
};
|
|
12403
13371
|
/** A later activation: as a message, or by resuming a session that has ended. */
|
|
12404
13372
|
const continueConversation = async (talk, id, request, signal) => {
|
|
@@ -12417,7 +13385,13 @@ const continueConversation = async (talk, id, request, signal) => {
|
|
|
12417
13385
|
if (isLive()) await followRun(talk, id, lastSeq(store, id), signal, finishedRun);
|
|
12418
13386
|
const [run] = store.lookup(id);
|
|
12419
13387
|
if (run === void 0) return failure(`Run ${id} is gone.`);
|
|
12420
|
-
if (signal.aborted)
|
|
13388
|
+
if (signal.aborted) {
|
|
13389
|
+
const said = `Stopped while run ${id} was finishing.`;
|
|
13390
|
+
return failedWith(endFor("interrupted", {}, said), {
|
|
13391
|
+
status: "fail",
|
|
13392
|
+
summary: said
|
|
13393
|
+
});
|
|
13394
|
+
}
|
|
12421
13395
|
return resumeNode(talk, run, request, node, signal);
|
|
12422
13396
|
};
|
|
12423
13397
|
// ../core/src/remote/calls.ts
|
|
@@ -12513,7 +13487,7 @@ const createCalls = (options) => {
|
|
|
12513
13487
|
};
|
|
12514
13488
|
const converse = async (request, signal) => {
|
|
12515
13489
|
const unadopted = await adoptConversation(request, signal);
|
|
12516
|
-
if (unadopted !== null) return
|
|
13490
|
+
if (unadopted !== null) return startFailed(unadopted);
|
|
12517
13491
|
const known = conversations.get(request.callId);
|
|
12518
13492
|
if (known !== void 0) {
|
|
12519
13493
|
const done = stopOnAbort(known, signal);
|
|
@@ -12525,7 +13499,7 @@ const createCalls = (options) => {
|
|
|
12525
13499
|
}
|
|
12526
13500
|
const begun = await begin(request);
|
|
12527
13501
|
adopt.delete(request.callId);
|
|
12528
|
-
if (!begun.ok) return
|
|
13502
|
+
if (!begun.ok) return startFailed(begun.error);
|
|
12529
13503
|
const { id, after, fresh } = begun;
|
|
12530
13504
|
conversations.set(request.callId, id);
|
|
12531
13505
|
const done = stopOnAbort(id, signal);
|
|
@@ -12534,7 +13508,7 @@ const createCalls = (options) => {
|
|
|
12534
13508
|
const refused = await linkChannels(talk, id, request, signal);
|
|
12535
13509
|
if (refused !== null) {
|
|
12536
13510
|
stop(id);
|
|
12537
|
-
return
|
|
13511
|
+
return startFailed(refused);
|
|
12538
13512
|
}
|
|
12539
13513
|
}
|
|
12540
13514
|
if (fresh) tellInput(talk, id, request);
|
|
@@ -12550,7 +13524,7 @@ const createCalls = (options) => {
|
|
|
12550
13524
|
};
|
|
12551
13525
|
const callOnce = async (request, signal) => {
|
|
12552
13526
|
const begun = await begin(request);
|
|
12553
|
-
if (!begun.ok) return
|
|
13527
|
+
if (!begun.ok) return startFailed(begun.error);
|
|
12554
13528
|
const { id, after } = begun;
|
|
12555
13529
|
const done = stopOnAbort(id, signal);
|
|
12556
13530
|
try {
|
|
@@ -13510,6 +14484,82 @@ const projectQueries = (db) => {
|
|
|
13510
14484
|
}
|
|
13511
14485
|
};
|
|
13512
14486
|
};
|
|
14487
|
+
// ../core/src/store/tool-cpu.ts
|
|
14488
|
+
const SOURCE$1 = `SELECT seq, at, data FROM events
|
|
14489
|
+
WHERE run_id = ? AND (
|
|
14490
|
+
type = 'run.started'
|
|
14491
|
+
OR (type = 'node.event' AND json_extract(data, '$.event.type') = 'tool.result'
|
|
14492
|
+
AND json_extract(data, '$.event.cpu') IS NOT NULL)
|
|
14493
|
+
) ORDER BY seq`;
|
|
14494
|
+
const INSERT = `INSERT OR IGNORE INTO tool_cpu (
|
|
14495
|
+
run_id, seq, at, project, workflow, node, template, user_ms, system_ms
|
|
14496
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
14497
|
+
/** A run's tool commands' rows, from its events. */
|
|
14498
|
+
const rowsOf = (db, run) => {
|
|
14499
|
+
const declared = /* @__PURE__ */ new Set();
|
|
14500
|
+
const rows = [];
|
|
14501
|
+
for (const r of db.prepare(SOURCE$1).all(run.id)) {
|
|
14502
|
+
const event = eventOf(text(r, "data"));
|
|
14503
|
+
if (event.type === "run.started") {
|
|
14504
|
+
for (const node of event.nodes) declared.add(node.id);
|
|
14505
|
+
continue;
|
|
14506
|
+
}
|
|
14507
|
+
if (event.type !== "node.event" || event.event.type !== "tool.result") continue;
|
|
14508
|
+
const { cpu } = event.event;
|
|
14509
|
+
if (cpu === void 0) continue;
|
|
14510
|
+
const template = declared.has(event.id) ? void 0 : templateOf(event.id, declared);
|
|
14511
|
+
rows.push({
|
|
14512
|
+
runId: run.id,
|
|
14513
|
+
seq: Number(r["seq"]),
|
|
14514
|
+
at: text(r, "at"),
|
|
14515
|
+
project: run.project,
|
|
14516
|
+
workflow: run.workflow,
|
|
14517
|
+
node: event.id,
|
|
14518
|
+
...template === void 0 ? {} : { template },
|
|
14519
|
+
userMs: cpu.userMs,
|
|
14520
|
+
systemMs: cpu.systemMs
|
|
14521
|
+
});
|
|
14522
|
+
}
|
|
14523
|
+
return rows;
|
|
14524
|
+
};
|
|
14525
|
+
const write = (db, rows) => {
|
|
14526
|
+
const insert = db.prepare(INSERT);
|
|
14527
|
+
for (const row of rows) insert.run(row.runId, row.seq, row.at, row.project, row.workflow, row.node, row.template ?? null, row.userMs, row.systemMs);
|
|
14528
|
+
};
|
|
14529
|
+
const runsOf = (db, sql, ...values) => db.prepare(sql).all(...values).map((run) => ({
|
|
14530
|
+
id: text(run, "id"),
|
|
14531
|
+
project: text(run, "project"),
|
|
14532
|
+
workflow: text(run, "workflow")
|
|
14533
|
+
}));
|
|
14534
|
+
/** Fills the table from the runs recorded before it existed. */
|
|
14535
|
+
const backfillToolCpu = (db) => {
|
|
14536
|
+
for (const run of runsOf(db, "SELECT id, project, workflow FROM runs")) write(db, rowsOf(db, run));
|
|
14537
|
+
};
|
|
14538
|
+
const toolCpuQueries = (db) => ({
|
|
14539
|
+
/** Writes a run's rows; the store's own, on each timed `tool.result`. */
|
|
14540
|
+
recordToolCpu: (runId) => {
|
|
14541
|
+
const [run] = runsOf(db, "SELECT id, project, workflow FROM runs WHERE id = ?", runId);
|
|
14542
|
+
if (run !== void 0) write(db, rowsOf(db, run));
|
|
14543
|
+
},
|
|
14544
|
+
toolCpu: (filter = {}) => {
|
|
14545
|
+
const { sql, values } = whereOf(filter);
|
|
14546
|
+
return db.prepare(`SELECT * FROM tool_cpu ${sql} ORDER BY at, run_id, seq`).all(...values).map((r) => {
|
|
14547
|
+
const row = {
|
|
14548
|
+
runId: text(r, "run_id"),
|
|
14549
|
+
seq: Number(r["seq"]),
|
|
14550
|
+
at: text(r, "at"),
|
|
14551
|
+
project: text(r, "project"),
|
|
14552
|
+
workflow: text(r, "workflow"),
|
|
14553
|
+
node: text(r, "node"),
|
|
14554
|
+
userMs: Number(r["user_ms"]),
|
|
14555
|
+
systemMs: Number(r["system_ms"])
|
|
14556
|
+
};
|
|
14557
|
+
const template = text(r, "template");
|
|
14558
|
+
if (template !== "") row.template = template;
|
|
14559
|
+
return row;
|
|
14560
|
+
});
|
|
14561
|
+
}
|
|
14562
|
+
});
|
|
13513
14563
|
// ../core/src/store/triggers.ts
|
|
13514
14564
|
const recordOf$1 = (row) => {
|
|
13515
14565
|
const acceptedHash = text(row, "accepted_hash");
|
|
@@ -13587,8 +14637,8 @@ const triggerQueries = (db) => {
|
|
|
13587
14637
|
};
|
|
13588
14638
|
// ../core/src/store/queries.ts
|
|
13589
14639
|
/**
|
|
13590
|
-
* Every query set on `db`; `insertFeedback` and `
|
|
13591
|
-
* own.
|
|
14640
|
+
* Every query set on `db`; `insertFeedback`, `recordUsage` and `recordToolCpu`
|
|
14641
|
+
* are the store's own.
|
|
13592
14642
|
*/
|
|
13593
14643
|
const storeQueries = (db) => ({
|
|
13594
14644
|
...decisionQueries(db),
|
|
@@ -13596,6 +14646,7 @@ const storeQueries = (db) => ({
|
|
|
13596
14646
|
...feedbackQueries(db),
|
|
13597
14647
|
...projectQueries(db),
|
|
13598
14648
|
...usageQueries(db),
|
|
14649
|
+
...toolCpuQueries(db),
|
|
13599
14650
|
...planLimitQueries(db),
|
|
13600
14651
|
...triggerQueries(db),
|
|
13601
14652
|
...draftQueries(db)
|
|
@@ -13799,7 +14850,23 @@ const MIGRATIONS = [
|
|
|
13799
14850
|
ALTER TABLE runs ADD COLUMN failures_handled INTEGER;`,
|
|
13800
14851
|
`ALTER TABLE decisions ADD COLUMN note TEXT;`,
|
|
13801
14852
|
`ALTER TABLE decisions ADD COLUMN owner TEXT;`,
|
|
13802
|
-
`ALTER TABLE decisions ADD COLUMN document TEXT
|
|
14853
|
+
`ALTER TABLE decisions ADD COLUMN document TEXT;`,
|
|
14854
|
+
(db) => {
|
|
14855
|
+
db.exec(`CREATE TABLE tool_cpu (
|
|
14856
|
+
run_id TEXT NOT NULL,
|
|
14857
|
+
seq INTEGER NOT NULL,
|
|
14858
|
+
at TEXT NOT NULL,
|
|
14859
|
+
project TEXT NOT NULL,
|
|
14860
|
+
workflow TEXT NOT NULL,
|
|
14861
|
+
node TEXT NOT NULL,
|
|
14862
|
+
template TEXT,
|
|
14863
|
+
user_ms INTEGER NOT NULL,
|
|
14864
|
+
system_ms INTEGER NOT NULL,
|
|
14865
|
+
PRIMARY KEY (run_id, seq)
|
|
14866
|
+
) WITHOUT ROWID;
|
|
14867
|
+
CREATE INDEX tool_cpu_by_time ON tool_cpu (at);`);
|
|
14868
|
+
backfillToolCpu(db);
|
|
14869
|
+
}
|
|
13803
14870
|
];
|
|
13804
14871
|
const versionOf = (db) => Number(db.prepare("PRAGMA user_version").get()?.["user_version"] ?? 0);
|
|
13805
14872
|
/**
|
|
@@ -13925,8 +14992,9 @@ const openStore = (file = storePath(), options = {}) => {
|
|
|
13925
14992
|
const allRuns = db.prepare("SELECT * FROM runs ORDER BY started_at DESC, rowid DESC LIMIT ?");
|
|
13926
14993
|
const projectRuns = db.prepare("SELECT * FROM runs WHERE project = ? ORDER BY started_at DESC, rowid DESC LIMIT ?");
|
|
13927
14994
|
const eventsAfter = db.prepare("SELECT seq, at, data FROM events WHERE run_id = ? AND seq > ? ORDER BY seq");
|
|
14995
|
+
const controlEvents = db.prepare("SELECT seq, at, data FROM events WHERE run_id = ? AND type IN ('run.command', 'run.started', 'run.finished') ORDER BY seq");
|
|
13928
14996
|
const runNodes = db.prepare("SELECT node FROM events WHERE run_id = ? AND node IS NOT NULL GROUP BY node ORDER BY MIN(seq)");
|
|
13929
|
-
const { insertFeedback, recordUsage, recordPlanLimit, ...queries } = storeQueries(db);
|
|
14997
|
+
const { insertFeedback, recordUsage, recordToolCpu, recordPlanLimit, ...queries } = storeQueries(db);
|
|
13930
14998
|
return {
|
|
13931
14999
|
...queries,
|
|
13932
15000
|
begin: (run) => {
|
|
@@ -13953,6 +15021,7 @@ const openStore = (file = storePath(), options = {}) => {
|
|
|
13953
15021
|
insertEvent.run(runId, seq, at, event.type, nodeOf(event), JSON.stringify(event, maskReplacer));
|
|
13954
15022
|
if (event.type === "node.finished" && event.usage !== void 0) addCost.run(event.usage.costUsd, runId);
|
|
13955
15023
|
if (event.type === "node.event" && event.event.type === "session.finished") recordUsage(runId);
|
|
15024
|
+
if (event.type === "node.event" && event.event.type === "tool.result" && event.event.cpu !== void 0) recordToolCpu(runId);
|
|
13956
15025
|
if (event.type === "node.event" && event.event.type === "plan.limit") recordPlanLimit(event.event, at);
|
|
13957
15026
|
if (event.type === "feedback.recorded") insertFeedback(runId, seq, at, event.node, event.feedback);
|
|
13958
15027
|
if (event.type === "run.finished") {
|
|
@@ -13981,12 +15050,95 @@ const openStore = (file = storePath(), options = {}) => {
|
|
|
13981
15050
|
at: text(r, "at"),
|
|
13982
15051
|
event: eventOf(text(r, "data"))
|
|
13983
15052
|
})),
|
|
15053
|
+
controlEvents: (runId) => controlEvents.all(runId).map((r) => ({
|
|
15054
|
+
runId,
|
|
15055
|
+
seq: Number(r["seq"]),
|
|
15056
|
+
at: text(r, "at"),
|
|
15057
|
+
event: eventOf(text(r, "data"))
|
|
15058
|
+
})),
|
|
13984
15059
|
nodes: (runId) => runNodes.all(runId).map((r) => text(r, "node")),
|
|
13985
15060
|
close: () => {
|
|
13986
15061
|
db.close();
|
|
13987
15062
|
}
|
|
13988
15063
|
};
|
|
13989
15064
|
};
|
|
15065
|
+
// ../core/src/store/activity.ts
|
|
15066
|
+
/**
|
|
15067
|
+
* @file What each node of a run is doing, and when it started and finished,
|
|
15068
|
+
* folded from the stored events so nothing new has to be recorded. The tree
|
|
15069
|
+
* (view.ts) folds the events alone; this needs when they were written, and
|
|
15070
|
+
* keeps a screen's one-line answer to "what is it doing" in one place.
|
|
15071
|
+
*/
|
|
15072
|
+
const LONGEST_LINE = 120;
|
|
15073
|
+
/**
|
|
15074
|
+
* The fields of a tool's input that say what it's working on, best first. Only
|
|
15075
|
+
* these are shown, so what else a tool was given stays in the logs.
|
|
15076
|
+
*/
|
|
15077
|
+
const TARGETS = [
|
|
15078
|
+
"file_path",
|
|
15079
|
+
"notebook_path",
|
|
15080
|
+
"path",
|
|
15081
|
+
"url",
|
|
15082
|
+
"pattern",
|
|
15083
|
+
"query",
|
|
15084
|
+
"command"
|
|
15085
|
+
];
|
|
15086
|
+
const oneLine = (text) => {
|
|
15087
|
+
const line = text.replaceAll(/\s+/gu, " ").trim();
|
|
15088
|
+
return line.length > LONGEST_LINE ? `${line.slice(0, 119)}…` : line;
|
|
15089
|
+
};
|
|
15090
|
+
const toolLine = ({ name, input }) => {
|
|
15091
|
+
const target = TARGETS.map((field) => input[field]).find((value) => typeof value === "string" && value.trim() !== "");
|
|
15092
|
+
if (typeof target !== "string") return name;
|
|
15093
|
+
return oneLine(target.startsWith(`${name} `) ? target : `${name} ${target}`);
|
|
15094
|
+
};
|
|
15095
|
+
/** What a finished node said, or why it failed. */
|
|
15096
|
+
const finishedNow = (ok, error, summary) => {
|
|
15097
|
+
if (!ok) return error === void 0 ? void 0 : oneLine(error);
|
|
15098
|
+
return typeof summary === "string" && summary.trim() !== "" ? oneLine(summary) : void 0;
|
|
15099
|
+
};
|
|
15100
|
+
/** Each node's activity, by id; a node that never started has none. */
|
|
15101
|
+
const runActivity = (stored) => {
|
|
15102
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
15103
|
+
const of = (id) => {
|
|
15104
|
+
const found = nodes.get(id);
|
|
15105
|
+
if (found !== void 0) return found;
|
|
15106
|
+
const fresh = {};
|
|
15107
|
+
nodes.set(id, fresh);
|
|
15108
|
+
return fresh;
|
|
15109
|
+
};
|
|
15110
|
+
for (const { at, event } of stored) if (event.type === "node.started") {
|
|
15111
|
+
const node = of(event.id);
|
|
15112
|
+
node.startedAt ??= at;
|
|
15113
|
+
delete node.finishedAt;
|
|
15114
|
+
delete node.now;
|
|
15115
|
+
} else if (event.type === "node.event") {
|
|
15116
|
+
if (event.event.type === "tool.called") of(event.id).now = toolLine(event.event);
|
|
15117
|
+
} else if (event.type === "slot.waiting") of(event.id).now = oneLine(slotWaitLine(event.holders));
|
|
15118
|
+
else if (event.type === "slot.taken") delete of(event.id).now;
|
|
15119
|
+
else if (event.type === "decision.opened") of(event.node).now = oneLine(`Asked: ${event.question}`);
|
|
15120
|
+
else if (event.type === "decision.answered") of(event.node).now = oneLine(`Answered: ${event.answer}`);
|
|
15121
|
+
else if (event.type === "decision.withdrawn") delete of(event.node).now;
|
|
15122
|
+
else if (event.type === "node.finished") {
|
|
15123
|
+
const node = of(event.id);
|
|
15124
|
+
node.finishedAt = at;
|
|
15125
|
+
const now = finishedNow(event.ok, event.error, event.output["summary"]);
|
|
15126
|
+
if (now === void 0) delete node.now;
|
|
15127
|
+
else node.now = now;
|
|
15128
|
+
}
|
|
15129
|
+
return nodes;
|
|
15130
|
+
};
|
|
15131
|
+
/** The ownership tree, with what each node is doing and when it ran. */
|
|
15132
|
+
const activeTree = (stored) => {
|
|
15133
|
+
const activity = runActivity(stored);
|
|
15134
|
+
const dress = (node) => {
|
|
15135
|
+
Object.assign(node, activity.get(node.id));
|
|
15136
|
+
for (const child of node.children) dress(child);
|
|
15137
|
+
};
|
|
15138
|
+
const roots = runTree(stored.map(({ event }) => event));
|
|
15139
|
+
for (const root of roots) dress(root);
|
|
15140
|
+
return roots;
|
|
15141
|
+
};
|
|
13990
15142
|
// ../core/src/triggers/check.ts
|
|
13991
15143
|
/**
|
|
13992
15144
|
* @file Checking `triggers.yaml` against the project's workflows (RFC-011):
|
|
@@ -14653,112 +15805,6 @@ const planUpgrade = (options) => {
|
|
|
14653
15805
|
const RUNS_LOOKED_AT = 500;
|
|
14654
15806
|
/** The ids of runs still running anywhere on this machine, newest first. */
|
|
14655
15807
|
const runningRunIds = (store) => store.runs({ limit: RUNS_LOOKED_AT }).filter((run) => liveStatus(run) === "running").map((run) => run.id);
|
|
14656
|
-
// ../core/src/usage/quota.ts
|
|
14657
|
-
const WINDOWS = [
|
|
14658
|
-
"day",
|
|
14659
|
-
"week",
|
|
14660
|
-
"month"
|
|
14661
|
-
];
|
|
14662
|
-
const FIELDS = {
|
|
14663
|
-
day: "dayUsd",
|
|
14664
|
-
week: "weekUsd",
|
|
14665
|
-
month: "monthUsd"
|
|
14666
|
-
};
|
|
14667
|
-
/** Where a window began and when the next begins, in local time. */
|
|
14668
|
-
const windowOf = (window, now) => {
|
|
14669
|
-
const [year, month, day] = [
|
|
14670
|
-
now.getFullYear(),
|
|
14671
|
-
now.getMonth(),
|
|
14672
|
-
now.getDate()
|
|
14673
|
-
];
|
|
14674
|
-
if (window === "month") return {
|
|
14675
|
-
since: new Date(year, month, 1),
|
|
14676
|
-
until: new Date(year, month + 1)
|
|
14677
|
-
};
|
|
14678
|
-
if (window === "day") return {
|
|
14679
|
-
since: new Date(year, month, day),
|
|
14680
|
-
until: new Date(year, month, day + 1)
|
|
14681
|
-
};
|
|
14682
|
-
const monday = day - (now.getDay() + 6) % 7;
|
|
14683
|
-
return {
|
|
14684
|
-
since: new Date(year, month, monday),
|
|
14685
|
-
until: new Date(year, month, monday + 7)
|
|
14686
|
-
};
|
|
14687
|
-
};
|
|
14688
|
-
/** The caps that apply: the user's on everything, and the project's on itself. */
|
|
14689
|
-
const capsOf = (user, project, root) => WINDOWS.flatMap((window) => {
|
|
14690
|
-
const field = FIELDS[window];
|
|
14691
|
-
const everywhere = user?.[field];
|
|
14692
|
-
const here = project?.[field];
|
|
14693
|
-
const caps = [];
|
|
14694
|
-
if (everywhere !== void 0) caps.push({
|
|
14695
|
-
window,
|
|
14696
|
-
capUsd: everywhere
|
|
14697
|
-
});
|
|
14698
|
-
if (here !== void 0 && root !== void 0) caps.push({
|
|
14699
|
-
window,
|
|
14700
|
-
capUsd: here,
|
|
14701
|
-
project: root
|
|
14702
|
-
});
|
|
14703
|
-
return caps;
|
|
14704
|
-
});
|
|
14705
|
-
/** Each cap with what has been spent against it. */
|
|
14706
|
-
const standingOf = (store, caps, now) => caps.map((cap) => {
|
|
14707
|
-
const { since, until } = windowOf(cap.window, now);
|
|
14708
|
-
const spentUsd = store.spentSince(since.toISOString(), cap.project);
|
|
14709
|
-
return {
|
|
14710
|
-
...cap,
|
|
14711
|
-
spentUsd,
|
|
14712
|
-
leftUsd: cap.capUsd - spentUsd,
|
|
14713
|
-
resetsAt: until.toISOString()
|
|
14714
|
-
};
|
|
14715
|
-
});
|
|
14716
|
-
/** Dollars: cents, or more digits for an amount under a dollar. */
|
|
14717
|
-
const usd = (amount) => `$${amount.toFixed(amount > 0 && amount < 1 ? 4 : 2)}`;
|
|
14718
|
-
const at = (iso) => new Date(iso).toLocaleString(void 0, {
|
|
14719
|
-
dateStyle: "medium",
|
|
14720
|
-
timeStyle: "short"
|
|
14721
|
-
});
|
|
14722
|
-
const capReason = (spent) => `${spent.project === void 0 ? "your" : "this project's"} ${spent.window} quota of ${usd(spent.capUsd)} is spent (${usd(spent.spentUsd)}); it lifts on ${at(spent.resetsAt)}, or raise it in ${spent.project === void 0 ? "~/.ork/settings.yaml" : ".ork/project.yaml"}`;
|
|
14723
|
-
/** What a run or a call refused at its start says first, for its caller to read. */
|
|
14724
|
-
const NOT_STARTED = "Not started: ";
|
|
14725
|
-
/** Why a plan limit stops a run. */
|
|
14726
|
-
const planReason = (limit) => `your Claude plan's ${limit.window.replaceAll("_", " ")} limit was reached${limit.resetsAt === void 0 ? "" : `; it resets on ${at(limit.resetsAt)}, or use ork usage --clear-plan-limits if it has lifted`}`;
|
|
14727
|
-
/**
|
|
14728
|
-
* The windows that hold the whole account. One for a single model, such as
|
|
14729
|
-
* `seven_day_opus`, stops only the session it refused: other models may be
|
|
14730
|
-
* free.
|
|
14731
|
-
*/
|
|
14732
|
-
const ACCOUNT_WINDOWS = /* @__PURE__ */ new Set([
|
|
14733
|
-
"five_hour",
|
|
14734
|
-
"seven_day",
|
|
14735
|
-
"overage",
|
|
14736
|
-
"plan"
|
|
14737
|
-
]);
|
|
14738
|
-
/** The plan limits in force, the one that lifts last first. */
|
|
14739
|
-
const refusing = (limits, now) => limits.filter((limit) => limit.status === "rejected" && ACCOUNT_WINDOWS.has(limit.window) && limit.resetsAt !== void 0 && Date.parse(limit.resetsAt) > now.getTime()).toSorted((a, b) => String(b.resetsAt).localeCompare(String(a.resetsAt)));
|
|
14740
|
-
/** How long a report with no reset time stays worth showing. */
|
|
14741
|
-
const SHOWN_MS = 864e5;
|
|
14742
|
-
/** What the caps and the plan allow a run now. */
|
|
14743
|
-
const limitsNow = (store, caps, now) => {
|
|
14744
|
-
const standing = standingOf(store, caps, now);
|
|
14745
|
-
const [plan] = refusing(store.planLimits(), now);
|
|
14746
|
-
const spent = standing.find((cap) => cap.leftUsd <= 0);
|
|
14747
|
-
const blocked = plan === void 0 ? spent === void 0 ? void 0 : capReason(spent) : planReason(plan);
|
|
14748
|
-
return {
|
|
14749
|
-
...standing.length === 0 ? {} : { leftUsd: Math.min(...standing.map((cap) => cap.leftUsd)) },
|
|
14750
|
-
...blocked === void 0 ? {} : { blocked }
|
|
14751
|
-
};
|
|
14752
|
-
};
|
|
14753
|
-
/** What `ork usage` and the panel show. */
|
|
14754
|
-
const limitsReport = (store, { caps, problems }, now) => ({
|
|
14755
|
-
problems,
|
|
14756
|
-
caps: standingOf(store, caps, now),
|
|
14757
|
-
plan: store.planLimits().filter((limit) => {
|
|
14758
|
-
if (limit.status === "allowed") return false;
|
|
14759
|
-
return (limit.resetsAt === void 0 ? Date.parse(limit.at) + SHOWN_MS : Date.parse(limit.resetsAt)) > now.getTime();
|
|
14760
|
-
})
|
|
14761
|
-
});
|
|
14762
15808
|
// ../core/src/usage/caps.ts
|
|
14763
15809
|
/**
|
|
14764
15810
|
* @file Which limits across runs apply where (RFC-007): the user's settings and
|
|
@@ -15113,6 +16159,11 @@ const commitWork = async (worktree, never, message) => {
|
|
|
15113
16159
|
};
|
|
15114
16160
|
}
|
|
15115
16161
|
};
|
|
16162
|
+
/** A node's result once its work could not be committed: a failed `git commit`. */
|
|
16163
|
+
const uncommitted = (result, error) => endedWith({
|
|
16164
|
+
...result,
|
|
16165
|
+
ok: false
|
|
16166
|
+
}, endFor("command_failed", { program: "git commit" }, error));
|
|
15116
16167
|
// ../core/src/worktree/close.ts
|
|
15117
16168
|
const reasonOf = (options, id) => {
|
|
15118
16169
|
const why = options.unmerged.get(id);
|
|
@@ -15294,12 +16345,7 @@ const createWorkspace = (options) => {
|
|
|
15294
16345
|
if (work.committed) event(id, "committed", worktree);
|
|
15295
16346
|
if (work.error === void 0) return next;
|
|
15296
16347
|
unmerged.set(holder, `uncommitted: ${work.error}`);
|
|
15297
|
-
|
|
15298
|
-
return {
|
|
15299
|
-
...next,
|
|
15300
|
-
ok: false,
|
|
15301
|
-
error
|
|
15302
|
-
};
|
|
16348
|
+
return uncommitted(next, work.error);
|
|
15303
16349
|
};
|
|
15304
16350
|
const finish = async (id, result) => {
|
|
15305
16351
|
const from = continued.get(id);
|
|
@@ -15355,4 +16401,4 @@ const createWorkspace = (options) => {
|
|
|
15355
16401
|
close
|
|
15356
16402
|
};
|
|
15357
16403
|
};
|
|
15358
|
-
export {
|
|
16404
|
+
export { DETACHED_ENV as $, skillsIn as $n, endOf as $t, lines as A, linkTools as An, AGENT_DRIVERS as Ar, nestItems as At, linkNodes as B, placeFiles as Bn, lowerChild as Bt, openStore as C, wakeRun as Cn, TOOL_PREFIX as Cr, runStatus as Ct, removeWorktree as D, planAgentMigration as Dn, readyDriver as Dr, processAlive as Dt, inspectProject as E, maskSecrets as En, driverChecks as Er, templateOf as Et, driverQueues as F, catalogAgent as Fn, knownExperiments as Fr, failedWith as Ft, requestContinue as G, workflowDirOf as Gn, PROJECT_FILE as Gt, tellNode as H, projectsHome as Hn, envReadsOf as Ht, createCalls as I, loadAgents as In, stoppedEnd as It, tellLiveRun as J, LESSON_CAP_TOKENS as Jn, machineRulesOf as Jt, requestPause as K, inside as Kn, gitRulesOf as Kt, addressOf as L, hasInRepoFiles as Ln, calledWorkflows as Lt, parseInputPairs as M, toolsInUse as Mn, firstLine as Mr, runScope as Mt, resolveInputs as N, createAgent as Nn, EXPERIMENTS as Nr, declaredProblem as Nt, git as O, applySteps as On, AGENT_TOOL_LAYOUTS as Or, stillTheSameProcess as Ot, driverRefusals as P, deleteAgent as Pn, experimentsOn as Pr, endedWith as Pt, listKey as Q, writePlainFile as Qn, createSignals as Qt, endLink as R, inRepoPlace as Rn, slotClock as Rt, isNodeEvent as S, socketRoom as Sn, INTEGRATIONS as Sr, pausedOf as St, BRANCH_PREFIX as T, maskReplacer as Tn, PREFERRED_DRIVER as Tr, slotWaitsOf as Tt, unlinkNodes as U, runDirOf as Un, nodeSays as Ut, peerOf as V, placeFor as Vn, startAt as Vt, flattenTree as W, shownPath as Wn, declaredOutputOf as Wt, detachResume as X, guardedBase as Xn, mcpNotText as Xt, adoptedCalls as Y, estimateTokens as Yn, MCP_FILE as Yt, resumable as Z, plainPathInside as Zn, serversIn as Zt, readProjectTriggers as _, parseTemplate as _n, isToolNode as _r, startRun as _t, releaseNotes as a, FEEDBACK_SEVERITIES as an, programsRunBy as ar, planReason as at, watchingPid as b, serveRun as bn, retiredDriverHint as br, heldTimeout as bt, detectInstaller as c, planPost as cn, serializeWorkflow as cr, explainSession as ct, parseVersion as d, publishPost as dn, WorkflowSchema as dr, sessionDriver as dt, outcomeEnd as en, WorkflowEditSchema as er, detachRun as et, lateTriggers as f, createFeedback as fn, CheckReportSchema as fr, parseReport as ft, declaredTriggers as g, evaluate as gn, isClaudeNode as gr, walkSummary as gt, triggerStanding as h, targetOf as hn, NodeSchema as hr, walkChecks as ht, planUpgrade as i, FEEDBACK_KINDS as in, beforeOf as ir, limitsReport as it, clearGitLocalEnv as j, toolLinks as jn, DRIVER_IDS as jr, scriptContactsOf as jt, gitOk as k, planMoveInto as kn, ORK_LAYOUT as kr, isItem as kt, compareVersions as l, runGh as ln, markValue as lr, planNodeSession as lt, createWatcher as m, normalizeTitle as mn, settingsOf as mr, checkWorkflowText as mt, resolveBase as n, END_KINDS as nn, newWorkflowText as nr, NOT_STARTED as nt, runningRunIds as o, FEEDBACK_TARGETS as on, renderJsonSchema as or, usd as ot, triggersReport as p, fingerprint as pn, expandEdge as pr, reportSchemaFor as pt, requestStop as q, projectRootOf as qn, loadProject as qt, capsHere as r, endFor as rn, readDocument as rr, limitsNow as rt, runningScript as s, DISCUSSIONS as sn, parseWorkflow as sr, explainWorkflowText as st, createWorkspace as t, END_DETAIL_LIMIT as tn, applyEdits as tr, followEvents as tt, isNewer as u, sendPost as un, EdgeInputSchema as ur, namingNoDriver as ut, workflowTextHash as v, renderTemplate as vn, DriverSchema as vr, MESSAGE_LIMIT as vt, storePath as w, MASK as wn, integrationFor as wr, runTree as wt, activeTree as x, socketPath as xn, timeoutMs as xr, liveStatus as xt, takeWatchLock as y, readCondition as yn, NodeIdSchema as yr, userMessageText as yt, endLinksOf as z, newProjectId as zn, slotWaitLine as zt };
|