@foldrun/core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -10
- package/dist/src/approvals.d.ts +104 -1
- package/dist/src/approvals.js +192 -2
- package/dist/src/budget.d.ts +32 -0
- package/dist/src/budget.js +177 -0
- package/dist/src/catalog.js +2 -1
- package/dist/src/confine.js +6 -1
- package/dist/src/deploy.js +4 -2
- package/dist/src/egress.d.ts +19 -2
- package/dist/src/egress.js +2 -1
- package/dist/src/evals.d.ts +4 -0
- package/dist/src/evals.js +20 -11
- package/dist/src/flow-lint.d.ts +4 -0
- package/dist/src/flow-lint.js +108 -0
- package/dist/src/host-env.d.ts +8 -0
- package/dist/src/host-env.js +48 -0
- package/dist/src/json-schema.d.ts +12 -0
- package/dist/src/json-schema.js +202 -0
- package/dist/src/library.js +13 -1
- package/dist/src/notify.d.ts +79 -1
- package/dist/src/notify.js +243 -9
- package/dist/src/paths.d.ts +5 -0
- package/dist/src/paths.js +32 -0
- package/dist/src/platform.d.ts +112 -2
- package/dist/src/platform.js +20 -2
- package/dist/src/provider-health.d.ts +55 -0
- package/dist/src/provider-health.js +232 -0
- package/dist/src/run-container.d.ts +80 -4
- package/dist/src/run-container.js +176 -37
- package/dist/src/runner.d.ts +41 -5
- package/dist/src/runner.js +513 -81
- package/dist/src/script-tools.d.ts +12 -0
- package/dist/src/script-tools.js +10 -2
- package/dist/src/secret-health.d.ts +59 -0
- package/dist/src/secret-health.js +155 -0
- package/dist/src/secrets.d.ts +19 -1
- package/dist/src/secrets.js +44 -12
- package/dist/src/starter.js +4 -0
- package/dist/src/step-exec.d.ts +62 -6
- package/dist/src/step-exec.js +199 -68
- package/dist/src/store.d.ts +300 -3
- package/dist/src/store.js +574 -26
- package/dist/src/tar.js +2 -1
- package/dist/src/test-mode.d.ts +111 -0
- package/dist/src/test-mode.js +285 -0
- package/dist/src/tool-test.js +48 -21
- package/dist/src/translator.js +2 -1
- package/dist/src/trigger-log.d.ts +43 -0
- package/dist/src/trigger-log.js +122 -0
- package/dist/src/triggers.d.ts +20 -0
- package/dist/src/triggers.js +91 -10
- package/dist/src/webhook.d.ts +15 -3
- package/dist/src/webhook.js +11 -11
- package/package.json +6 -5
- package/src/approvals.ts +253 -2
- package/src/budget.ts +178 -0
- package/src/catalog.ts +2 -1
- package/src/confine.ts +6 -1
- package/src/deploy.ts +4 -1
- package/src/egress.ts +20 -4
- package/src/evals.ts +25 -9
- package/src/flow-lint.ts +118 -0
- package/src/host-env.ts +51 -0
- package/src/json-schema.ts +184 -0
- package/src/library.ts +13 -1
- package/src/notify.ts +272 -11
- package/src/paths.ts +29 -0
- package/src/platform.ts +119 -3
- package/src/provider-health.ts +274 -0
- package/src/run-container.ts +215 -35
- package/src/runner.ts +525 -73
- package/src/script-tools.ts +18 -2
- package/src/secret-health.ts +185 -0
- package/src/secrets.ts +44 -10
- package/src/starter.ts +4 -0
- package/src/step-exec.ts +176 -21
- package/src/storage.ts +8 -2
- package/src/store.ts +785 -25
- package/src/tar.ts +2 -1
- package/src/test-mode.ts +311 -0
- package/src/tool-test.ts +49 -20
- package/src/translator.ts +2 -1
- package/src/trigger-log.ts +159 -0
- package/src/triggers.ts +92 -10
- package/src/webhook.ts +17 -11
|
@@ -23,6 +23,7 @@ import path from "node:path";
|
|
|
23
23
|
import crypto from "node:crypto";
|
|
24
24
|
import { spawn, spawnSync } from "node:child_process";
|
|
25
25
|
import { isPlatformPath } from "./store.js";
|
|
26
|
+
import { divertedSummary, isDivertedPath } from "./test-mode.js";
|
|
26
27
|
import { isFileValue, fileContent } from "./secrets.js";
|
|
27
28
|
import { safeTenantSegment } from "./runtime.js";
|
|
28
29
|
import { dataRoot } from "./paths.js";
|
|
@@ -95,7 +96,69 @@ export function allowedBack(rel) {
|
|
|
95
96
|
* the step did not touch it, so it is not ours to write back. Concurrent
|
|
96
97
|
* edits survive, and a step that genuinely changed a file still wins.
|
|
97
98
|
*/
|
|
98
|
-
|
|
99
|
+
/**
|
|
100
|
+
* What a workspace copy looked like when it went in: the sha256 of every
|
|
101
|
+
* file the step could hand back. Small enough to ride into the sandbox
|
|
102
|
+
* beside the input, so a driver that inherits the step can tell what the
|
|
103
|
+
* step changed without the staging copy the first driver held in /tmp.
|
|
104
|
+
*/
|
|
105
|
+
export function hashTree(dir) {
|
|
106
|
+
const out = {};
|
|
107
|
+
const walk = (d) => {
|
|
108
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
109
|
+
const abs = path.join(d, entry.name);
|
|
110
|
+
const rel = path.relative(dir, abs).replaceAll("\\", "/");
|
|
111
|
+
if (entry.isSymbolicLink())
|
|
112
|
+
continue;
|
|
113
|
+
if (entry.isDirectory()) {
|
|
114
|
+
if (allowedBack(rel + "/"))
|
|
115
|
+
walk(abs);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (!allowedBack(rel))
|
|
119
|
+
continue;
|
|
120
|
+
out[rel] = crypto.createHash("sha256").update(fs.readFileSync(abs)).digest("hex");
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
if (fs.existsSync(dir))
|
|
124
|
+
walk(dir);
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
const sha = (b) => crypto.createHash("sha256").update(b).digest("hex");
|
|
128
|
+
/**
|
|
129
|
+
* Both sides changed a file the step was handed. When each is the handed-in
|
|
130
|
+
* content with lines added on the end — a ledger, history.md, a .jsonl — the
|
|
131
|
+
* answer is the base, then the host's additions, then the step's. Only the
|
|
132
|
+
* base's hash may be known, so the base is found as the longest shared
|
|
133
|
+
* leading run of whole lines whose hash matches. Anything else is null: a
|
|
134
|
+
* real conflict, which the caller must not settle by dropping one side.
|
|
135
|
+
*/
|
|
136
|
+
export function mergeAppends(was, host, next) {
|
|
137
|
+
// Both sides created the same new file: no shared base to append to, and
|
|
138
|
+
// joining two JSON documents is corruption, not a merge.
|
|
139
|
+
if (was === null)
|
|
140
|
+
return null;
|
|
141
|
+
const h = host.toString("utf8");
|
|
142
|
+
const n = next.toString("utf8");
|
|
143
|
+
let common = 0;
|
|
144
|
+
const max = Math.min(h.length, n.length);
|
|
145
|
+
while (common < max && h.charCodeAt(common) === n.charCodeAt(common))
|
|
146
|
+
common++;
|
|
147
|
+
for (let end = common; end >= 0; end--) {
|
|
148
|
+
if (end > 0 && h[end - 1] !== "\n")
|
|
149
|
+
continue;
|
|
150
|
+
const prefix = h.slice(0, end);
|
|
151
|
+
const matches = typeof was === "string" ? sha(Buffer.from(prefix, "utf8")) === was : Buffer.from(prefix, "utf8").equals(was);
|
|
152
|
+
if (!matches)
|
|
153
|
+
continue;
|
|
154
|
+
const hostAdded = h.slice(end);
|
|
155
|
+
const stepAdded = n.slice(end);
|
|
156
|
+
const joined = hostAdded && !hostAdded.endsWith("\n") ? hostAdded + "\n" : hostAdded;
|
|
157
|
+
return Buffer.from(prefix + joined + stepAdded, "utf8");
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
export function applyContainerChanges(hostWs, containerWs, baseline, note, divert) {
|
|
99
162
|
const applied = [];
|
|
100
163
|
const walk = (dir) => {
|
|
101
164
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -116,17 +179,61 @@ export function applyContainerChanges(hostWs, containerWs, baseline) {
|
|
|
116
179
|
}
|
|
117
180
|
if (!allowedBack(rel))
|
|
118
181
|
continue;
|
|
119
|
-
|
|
182
|
+
let next = fs.readFileSync(abs);
|
|
183
|
+
// What the step was handed: its bytes (a staging dir) or its hash.
|
|
184
|
+
// null when the file did not exist then; undefined when no baseline.
|
|
185
|
+
let was;
|
|
186
|
+
if (typeof baseline === "string") {
|
|
187
|
+
const p = path.join(baseline, rel);
|
|
188
|
+
was = fs.existsSync(p) ? fs.readFileSync(p) : null;
|
|
189
|
+
}
|
|
190
|
+
else if (baseline) {
|
|
191
|
+
was = baseline[rel.replaceAll("\\", "/")] ?? null;
|
|
192
|
+
}
|
|
193
|
+
const same = (b) => was === null || was === undefined ? false : typeof was === "string" ? sha(b) === was : b.equals(was);
|
|
120
194
|
// Untouched by this step: whatever the host says now is more current
|
|
121
195
|
// than what we handed in, including a change made while it ran.
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
196
|
+
if (same(next))
|
|
197
|
+
continue;
|
|
198
|
+
const target = path.join(hostWs, rel);
|
|
199
|
+
const hostNow = fs.existsSync(target) ? fs.readFileSync(target) : null;
|
|
200
|
+
if (hostNow && hostNow.equals(next))
|
|
201
|
+
continue;
|
|
202
|
+
// The host moved too since the step was handed its copy — another run
|
|
203
|
+
// or a person wrote it. Writing the step's copy over it erased the
|
|
204
|
+
// other run's ledger rows on 2026-09-14. Merge appends; keep both
|
|
205
|
+
// sides of anything else.
|
|
206
|
+
const hostMoved = was !== undefined && hostNow !== null && !same(hostNow);
|
|
207
|
+
if (hostMoved) {
|
|
208
|
+
const merged = mergeAppends(was ?? null, hostNow, next);
|
|
209
|
+
if (merged) {
|
|
210
|
+
next = merged;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const ext = path.extname(rel);
|
|
214
|
+
const stem = rel.slice(0, rel.length - ext.length);
|
|
215
|
+
const conflictRel = `${stem}.conflict-${Date.now().toString(36)}${ext}`;
|
|
216
|
+
const conflictAbs = path.join(hostWs, conflictRel);
|
|
217
|
+
fs.mkdirSync(path.dirname(conflictAbs), { recursive: true });
|
|
218
|
+
fs.writeFileSync(conflictAbs, next);
|
|
219
|
+
applied.push(conflictRel);
|
|
220
|
+
note?.(`write-back conflict on ${rel.replaceAll("\\", "/")}: it changed on the workspace while this step ran, ` +
|
|
221
|
+
`and not only by appending — kept the workspace's version; this step's version is ${conflictRel.replaceAll("\\", "/")}`);
|
|
125
222
|
continue;
|
|
223
|
+
}
|
|
126
224
|
}
|
|
127
|
-
|
|
128
|
-
|
|
225
|
+
// A test run's state/ and storage/ writes land under the run, not on
|
|
226
|
+
// the workspace: the step ran against the real files and its output
|
|
227
|
+
// is kept where a person can read it, but the next real run starts
|
|
228
|
+
// from state a test never touched. Decided after the merge so the
|
|
229
|
+
// diverted copy is what WOULD have been written, conflicts included.
|
|
230
|
+
if (divert && isDivertedPath(rel)) {
|
|
231
|
+
const aside = path.join(divert.to, rel);
|
|
232
|
+
fs.mkdirSync(path.dirname(aside), { recursive: true });
|
|
233
|
+
fs.writeFileSync(aside, next);
|
|
234
|
+
divert.note(rel.replaceAll("\\", "/"), divertedSummary(rel.replaceAll("\\", "/"), hostNow, next));
|
|
129
235
|
continue;
|
|
236
|
+
}
|
|
130
237
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
131
238
|
fs.writeFileSync(target, next);
|
|
132
239
|
applied.push(rel);
|
|
@@ -253,9 +360,9 @@ try {
|
|
|
253
360
|
|
|
254
361
|
// API and script tools, rebuilt in here from their specs. Secrets were
|
|
255
362
|
// substituted into API headers before the input crossed the boundary, so
|
|
256
|
-
// buildApiTools gets an already-resolved environment.
|
|
257
|
-
//
|
|
258
|
-
// the
|
|
363
|
+
// buildApiTools gets an already-resolved environment. verify: gets the
|
|
364
|
+
// container's own environment, whole: it is the boundary, assembled on
|
|
365
|
+
// the host from an allowlist, and the declared secrets are in it.
|
|
259
366
|
const api = buildApiTools("", input.apis, undefined, { env, missing: [] });
|
|
260
367
|
const script = buildScriptTools(
|
|
261
368
|
agentDir,
|
|
@@ -300,10 +407,13 @@ try {
|
|
|
300
407
|
env,
|
|
301
408
|
timeoutSec: input.timeoutSec,
|
|
302
409
|
budgetUsd: input.budgetUsd,
|
|
410
|
+
budgetNote: input.budgetNote,
|
|
303
411
|
price: input.price,
|
|
304
412
|
verify: input.verify,
|
|
305
|
-
verifyEnv:
|
|
413
|
+
verifyEnv: env,
|
|
306
414
|
output: input.output,
|
|
415
|
+
schema: input.schema,
|
|
416
|
+
maxTurns: input.maxTurns,
|
|
307
417
|
// The container is the boundary; the SDK's own bash sandbox here would
|
|
308
418
|
// only block declared network use (SSH, curl) for no added safety.
|
|
309
419
|
sandboxBash: false,
|
|
@@ -409,8 +519,17 @@ export function runnerImageTag() {
|
|
|
409
519
|
* has not been built before. `npm pack` of the installed core, so the image
|
|
410
520
|
* runs the same bytes the server does.
|
|
411
521
|
*/
|
|
412
|
-
|
|
413
|
-
|
|
522
|
+
/**
|
|
523
|
+
* `platform`: build for another architecture — "linux/arm64" on the amd64
|
|
524
|
+
* box, through QEMU — and name the result with the arch appended so it never
|
|
525
|
+
* shadows the native image in the daemon. Only the publisher asks for this;
|
|
526
|
+
* a step always runs the native image. Needs binfmt registered on the host
|
|
527
|
+
* (tonistiigi/binfmt); the publisher does that itself, every time, because
|
|
528
|
+
* the registration does not survive a reboot.
|
|
529
|
+
*/
|
|
530
|
+
export function ensureRunnerImage(opts = {}) {
|
|
531
|
+
const arch = opts.platform ? `-${opts.platform.split("/").pop()}` : "";
|
|
532
|
+
const tag = process.env.FOLDRUN_RUNNER_IMAGE ?? runnerImageTag() + arch;
|
|
414
533
|
const log = [];
|
|
415
534
|
if (process.env.FOLDRUN_RUNNER_IMAGE)
|
|
416
535
|
return { tag, log };
|
|
@@ -430,7 +549,8 @@ export function ensureRunnerImage() {
|
|
|
430
549
|
fs.writeFileSync(path.join(build, "entry.sh"), ENTRY);
|
|
431
550
|
fs.writeFileSync(path.join(build, "Dockerfile"), DOCKERFILE);
|
|
432
551
|
log.push(`building runner image ${tag} (first run only)`);
|
|
433
|
-
const
|
|
552
|
+
const args = ["build", ...(opts.platform ? ["--platform", opts.platform] : []), "-t", tag, build];
|
|
553
|
+
const out = spawnSync(cli(), args, { encoding: "utf8" });
|
|
434
554
|
if (out.status !== 0) {
|
|
435
555
|
throw new Error(`runner image build failed:\n${(out.stderr || out.stdout).slice(-2000)}`);
|
|
436
556
|
}
|
|
@@ -505,6 +625,44 @@ function ensureRunNetwork() {
|
|
|
505
625
|
}
|
|
506
626
|
return RUN_NETWORK;
|
|
507
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* What crosses into the container as its environment, and how.
|
|
630
|
+
*
|
|
631
|
+
* A docker --env-file is one `KEY=value` per line and cannot carry a
|
|
632
|
+
* newline. @file secrets are multi-line by nature (a PEM key, a cert), so
|
|
633
|
+
* they are staged as files NOW, into the copied-in workspace, and the env
|
|
634
|
+
* carries the container path — the one place a path is stable across the
|
|
635
|
+
* boundary. A plain secret whose value happens to hold a newline gets the
|
|
636
|
+
* same treatment, and the trace says so: it used to be dropped from the
|
|
637
|
+
* env file without a word, and the step failed later, elsewhere, on a
|
|
638
|
+
* variable that read as unset. Everything else goes through the env file.
|
|
639
|
+
*/
|
|
640
|
+
export function stageContainerEnv(env, wsIn, agentRel, emit) {
|
|
641
|
+
const containerEnv = {};
|
|
642
|
+
const stage = (k, content) => {
|
|
643
|
+
const rel = path.join(agentRel, ".secret-files", k.toLowerCase());
|
|
644
|
+
const abs = path.join(wsIn, rel);
|
|
645
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true, mode: 0o700 });
|
|
646
|
+
fs.writeFileSync(abs, content, { mode: 0o600 });
|
|
647
|
+
return `/workspace/${rel.replaceAll("\\", "/")}`;
|
|
648
|
+
};
|
|
649
|
+
for (const [k, v] of Object.entries(env)) {
|
|
650
|
+
if (typeof v !== "string")
|
|
651
|
+
continue;
|
|
652
|
+
if (isFileValue(v)) {
|
|
653
|
+
containerEnv[k] = stage(k, fileContent(v));
|
|
654
|
+
}
|
|
655
|
+
else if (/[\r\n]/.test(v)) {
|
|
656
|
+
containerEnv[k] = stage(k, v);
|
|
657
|
+
emit("info", `${k} holds a line break and cannot cross as an environment variable — ` +
|
|
658
|
+
`passed as a file instead; $${k} is its path in the sandbox`);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
containerEnv[k] = v;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return containerEnv;
|
|
665
|
+
}
|
|
508
666
|
/** The limits a size class reserves. Large is the install's configured
|
|
509
667
|
* limits unchanged, so existing installs bill and behave identically;
|
|
510
668
|
* small defaults to a quarter-ish slice and is env-tunable. */
|
|
@@ -595,6 +753,8 @@ export async function runStepInContainer(args) {
|
|
|
595
753
|
filter: (src) => !isPlatformPath(path.relative(args.workspaceRoot, src)),
|
|
596
754
|
});
|
|
597
755
|
const libIn = path.join(staging, "library");
|
|
756
|
+
if (args.galleryRoot && fs.existsSync(args.galleryRoot))
|
|
757
|
+
fs.cpSync(args.galleryRoot, libIn, { recursive: true });
|
|
598
758
|
if (fs.existsSync(args.libraryRoot))
|
|
599
759
|
fs.cpSync(args.libraryRoot, libIn, { recursive: true });
|
|
600
760
|
else
|
|
@@ -602,28 +762,7 @@ export async function runStepInContainer(args) {
|
|
|
602
762
|
const jobIn = path.join(staging, "job");
|
|
603
763
|
fs.mkdirSync(jobIn);
|
|
604
764
|
fs.writeFileSync(path.join(jobIn, "input.json"), JSON.stringify(args.input, null, 2));
|
|
605
|
-
|
|
606
|
-
// --env-file cannot carry a newline. So they are staged as files *now*,
|
|
607
|
-
// into the copied-in workspace, and the env carries the container path —
|
|
608
|
-
// the one place a path is stable across the boundary. Everything else
|
|
609
|
-
// goes through the env file. (Without this the key silently vanished:
|
|
610
|
-
// the old filter dropped any value with a newline, and ssh got an empty
|
|
611
|
-
// -i path.)
|
|
612
|
-
const containerEnv = {};
|
|
613
|
-
for (const [k, v] of Object.entries(args.env)) {
|
|
614
|
-
if (typeof v !== "string")
|
|
615
|
-
continue;
|
|
616
|
-
if (isFileValue(v)) {
|
|
617
|
-
const rel = path.join(args.input.agentRel, ".secret-files", k.toLowerCase());
|
|
618
|
-
const abs = path.join(wsIn, rel);
|
|
619
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true, mode: 0o700 });
|
|
620
|
-
fs.writeFileSync(abs, fileContent(v), { mode: 0o600 });
|
|
621
|
-
containerEnv[k] = `/workspace/${rel.replaceAll("\\", "/")}`;
|
|
622
|
-
}
|
|
623
|
-
else if (!v.includes("\n")) {
|
|
624
|
-
containerEnv[k] = v;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
765
|
+
const containerEnv = stageContainerEnv(args.env, wsIn, args.input.agentRel, args.emit);
|
|
627
766
|
const envFile = path.join(staging, "env");
|
|
628
767
|
fs.writeFileSync(envFile, Object.entries(containerEnv).map(([k, v]) => `${k}=${v}`).join("\n"), { mode: 0o600 });
|
|
629
768
|
const flags = [
|
|
@@ -737,7 +876,7 @@ export async function runStepInContainer(args) {
|
|
|
737
876
|
fs.mkdirSync(wsOut);
|
|
738
877
|
const back = spawnSync(cli(), ["cp", `${containerId}:/workspace/.`, wsOut], { encoding: "utf8" });
|
|
739
878
|
if (back.status === 0) {
|
|
740
|
-
applyContainerChanges(args.workspaceRoot, wsOut, wsIn);
|
|
879
|
+
applyContainerChanges(args.workspaceRoot, wsOut, wsIn, (m) => args.emit("error", m), args.divert);
|
|
741
880
|
}
|
|
742
881
|
else {
|
|
743
882
|
args.emit("error", `copy-out failed — the step's file changes were lost:\n${back.stderr.slice(0, 500)}`);
|
package/dist/src/runner.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FlowStep, type RunRecord } from "./store.ts";
|
|
1
|
+
import { type FlowStep, type RunRecord, type StepRecord } from "./store.ts";
|
|
2
2
|
/** How much of the earlier groups' output a step is handed, in characters.
|
|
3
3
|
* Newest groups are kept when the total is over it. */
|
|
4
4
|
export declare const CONTEXT_CHARS = 30000;
|
|
@@ -12,6 +12,17 @@ export declare const DESK_RUNS_CAP = 100;
|
|
|
12
12
|
* step is most likely to be about.
|
|
13
13
|
*/
|
|
14
14
|
export declare function joinEarlierGroups(groupResults: (string | null)[], gi: number, cap?: number): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* `parallel: N` — a pool of N slots the instances of one fan-out share.
|
|
17
|
+
* Fan-out always ran every instance at once, and still does unless the
|
|
18
|
+
* step says otherwise: twenty browser steps against one site, or one
|
|
19
|
+
* vendor's rate limit, is what the cap is for. Nothing is rented while an
|
|
20
|
+
* instance waits for a slot.
|
|
21
|
+
*/
|
|
22
|
+
export declare function slotPool(size: number): {
|
|
23
|
+
acquire: () => Promise<void>;
|
|
24
|
+
release: () => void;
|
|
25
|
+
};
|
|
15
26
|
export interface DiscoveredSkill {
|
|
16
27
|
name: string;
|
|
17
28
|
description: string;
|
|
@@ -102,6 +113,20 @@ export declare function applicableSkills<T extends {
|
|
|
102
113
|
*/
|
|
103
114
|
export declare function saysUntilMarker(result: string | null, until: string): boolean;
|
|
104
115
|
export declare function resolveDocLinks(text: string, workspaceRoot: string): string;
|
|
116
|
+
/**
|
|
117
|
+
* Write one attempt onto the step's history and re-total the step.
|
|
118
|
+
*
|
|
119
|
+
* runStep reports the attempt it just ran — its cost, tokens and sandbox
|
|
120
|
+
* seconds — by assigning the step's fields. Those become this attempt's row;
|
|
121
|
+
* the step's fields then become the sum of every row, which is what the
|
|
122
|
+
* run's spend, the flow's budget: and the platform's bill read. A retried
|
|
123
|
+
* step used to carry only its last attempt's cost, so a `retry: 2` step
|
|
124
|
+
* could spend three shares while reporting one.
|
|
125
|
+
*/
|
|
126
|
+
export declare function recordAttempt(step: StepRecord, n: number, status: "completed" | "failed", startedAt: string, eventsBefore: number,
|
|
127
|
+
/** The attempt re-attached to a sandbox a previous driver left: same
|
|
128
|
+
* attempt, so its row is replaced rather than repeated. */
|
|
129
|
+
reattach?: boolean): void;
|
|
105
130
|
/**
|
|
106
131
|
* Copy a directory tree by reading and writing the bytes.
|
|
107
132
|
*
|
|
@@ -132,8 +157,19 @@ export declare const OVERLOAD_RETRIES = 3;
|
|
|
132
157
|
*/
|
|
133
158
|
export declare function isTransientOverload(text: string): boolean;
|
|
134
159
|
export declare function backoffMs(attempt: number, text?: string, random?: () => number): number;
|
|
135
|
-
|
|
136
|
-
|
|
160
|
+
/**
|
|
161
|
+
* `schema: <path>` — the file, read and parsed, or null. Agent-relative like
|
|
162
|
+
* every other path in a flow (`../../schemas/lead.json`), confined to the
|
|
163
|
+
* workspace: a schema is the author's document, never something a step
|
|
164
|
+
* can point outside.
|
|
165
|
+
*/
|
|
166
|
+
export declare function readSchemaFile(workspaceRoot: string, rel: string): Record<string, unknown> | boolean | null;
|
|
167
|
+
export declare function createFlowRun(tenant: string, workspace: string, steps: FlowStep[], flowName: string, status: "queued" | "running", tags?: string[], startedBy?: string | null, opts?: {
|
|
168
|
+
test?: boolean;
|
|
169
|
+
}): RunRecord;
|
|
170
|
+
export declare function startFlowRun(tenant: string, workspace: string, steps: FlowStep[], flowName: string, modelOverride?: string | null, tags?: string[], effortOverride?: string | null, startedBy?: string | null, opts?: {
|
|
171
|
+
test?: boolean;
|
|
172
|
+
}): RunRecord;
|
|
137
173
|
/**
|
|
138
174
|
* Drive a run to completion: groups in order, approval gates, retries.
|
|
139
175
|
*
|
|
@@ -206,7 +242,7 @@ export interface Reconciliation {
|
|
|
206
242
|
* outlives any process. A run whose person already answered is not waiting,
|
|
207
243
|
* and is picked back up — see below.
|
|
208
244
|
*/
|
|
209
|
-
export declare function reconcileRuns(tenant: string, now?: number): Reconciliation[];
|
|
245
|
+
export declare function reconcileRuns(tenant: string, now?: number, live?: ReadonlySet<string>): Reconciliation[];
|
|
210
246
|
/**
|
|
211
247
|
* The same, for every account this installation holds.
|
|
212
248
|
*
|
|
@@ -214,7 +250,7 @@ export declare function reconcileRuns(tenant: string, now?: number): Reconciliat
|
|
|
214
250
|
* one hard-coded account left every other account's interrupted runs marked
|
|
215
251
|
* `running` forever, with no process left to finish them.
|
|
216
252
|
*/
|
|
217
|
-
export declare function reconcileAllRuns(now?: number): Reconciliation[];
|
|
253
|
+
export declare function reconcileAllRuns(now?: number, live?: ReadonlySet<string>): Reconciliation[];
|
|
218
254
|
/**
|
|
219
255
|
* Wait for a run to stop being in progress.
|
|
220
256
|
*
|