@foldrun/core 2.4.0 → 2.6.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/dist/src/agent-activity.d.ts +5 -0
- package/dist/src/agent-activity.js +31 -0
- package/dist/src/agent-import.js +2 -2
- package/dist/src/agent-tools.js +2 -2
- package/dist/src/agents-md.d.ts +5 -1
- package/dist/src/agents-md.js +42 -18
- package/dist/src/alerts.d.ts +60 -0
- package/dist/src/alerts.js +161 -0
- package/dist/src/browser-pod.js +4 -0
- package/dist/src/capture.d.ts +23 -0
- package/dist/src/capture.js +80 -0
- package/dist/src/compare.d.ts +57 -0
- package/dist/src/compare.js +116 -0
- package/dist/src/completions.js +26 -3
- package/dist/src/deploy.d.ts +36 -1
- package/dist/src/deploy.js +237 -13
- package/dist/src/egress.d.ts +16 -1
- package/dist/src/evals.d.ts +27 -2
- package/dist/src/evals.js +62 -19
- package/dist/src/flow-lint.d.ts +14 -0
- package/dist/src/flow-lint.js +35 -0
- package/dist/src/flow-patterns.js +29 -14
- package/dist/src/frontmatter-fields.d.ts +27 -0
- package/dist/src/frontmatter-fields.js +60 -3
- package/dist/src/frontmatter-guard.d.ts +19 -0
- package/dist/src/frontmatter-guard.js +67 -0
- package/dist/src/frontmatter-schema.d.ts +2 -2
- package/dist/src/frontmatter-schema.js +33 -2
- package/dist/src/frontmatter-text.d.ts +31 -0
- package/dist/src/frontmatter-text.js +102 -0
- package/dist/src/history.d.ts +4 -0
- package/dist/src/history.js +15 -0
- package/dist/src/hooks.js +1 -1
- package/dist/src/insights.d.ts +30 -0
- package/dist/src/insights.js +152 -0
- package/dist/src/issues.d.ts +140 -0
- package/dist/src/issues.js +302 -0
- package/dist/src/judge-verdict.d.ts +19 -0
- package/dist/src/judge-verdict.js +54 -0
- package/dist/src/library.js +1 -1
- package/dist/src/limits.d.ts +8 -0
- package/dist/src/limits.js +36 -5
- package/dist/src/linkable.js +1 -1
- package/dist/src/matter.d.ts +42 -0
- package/dist/src/matter.js +132 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +40 -0
- package/dist/src/network.d.ts +12 -1
- package/dist/src/network.js +35 -8
- package/dist/src/notify.d.ts +16 -0
- package/dist/src/notify.js +17 -7
- package/dist/src/okf.js +1 -1
- package/dist/src/online-checks.d.ts +76 -0
- package/dist/src/online-checks.js +432 -0
- package/dist/src/operator.d.ts +277 -0
- package/dist/src/operator.js +176 -2
- package/dist/src/otel-config.d.ts +18 -0
- package/dist/src/otel-config.js +69 -0
- package/dist/src/otel.d.ts +38 -0
- package/dist/src/otel.js +85 -0
- package/dist/src/outcome.d.ts +40 -0
- package/dist/src/outcome.js +77 -0
- package/dist/src/package.js +9 -2
- package/dist/src/platform.d.ts +1 -1
- package/dist/src/prompt-layers.d.ts +30 -0
- package/dist/src/prompt-layers.js +77 -0
- package/dist/src/provider-health.js +2 -2
- package/dist/src/providers.d.ts +5 -0
- package/dist/src/providers.js +2 -2
- package/dist/src/run-container.d.ts +25 -2
- package/dist/src/run-container.js +61 -6
- package/dist/src/runner.d.ts +53 -2
- package/dist/src/runner.js +471 -102
- package/dist/src/runs-filter.d.ts +48 -0
- package/dist/src/runs-filter.js +73 -0
- package/dist/src/scan.d.ts +30 -1
- package/dist/src/scan.js +145 -14
- package/dist/src/starter.js +27 -2
- package/dist/src/step-exec.d.ts +39 -3
- package/dist/src/step-exec.js +118 -15
- package/dist/src/store.d.ts +144 -3
- package/dist/src/store.js +150 -7
- package/dist/src/tokens.d.ts +38 -0
- package/dist/src/tokens.js +57 -0
- package/dist/src/tool-names.js +1 -1
- package/dist/src/tool-programs.js +1 -1
- package/dist/src/trace-tree.d.ts +40 -0
- package/dist/src/trace-tree.js +156 -0
- package/dist/src/translator.d.ts +8 -0
- package/dist/src/translator.js +7 -2
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/workspace-check.d.ts +18 -0
- package/dist/src/workspace-check.js +34 -0
- package/package.json +2 -2
- package/src/agent-activity.ts +32 -0
- package/src/agent-import.ts +2 -2
- package/src/agent-tools.ts +2 -2
- package/src/agents-md.ts +39 -14
- package/src/alerts.ts +190 -0
- package/src/browser-pod.ts +3 -0
- package/src/capture.ts +88 -0
- package/src/compare.ts +146 -0
- package/src/completions.ts +27 -3
- package/src/deploy.ts +238 -12
- package/src/egress.ts +12 -2
- package/src/evals.ts +64 -19
- package/src/flow-lint.ts +36 -0
- package/src/flow-patterns.ts +26 -13
- package/src/frontmatter-fields.ts +83 -3
- package/src/frontmatter-guard.ts +70 -0
- package/src/frontmatter-schema.ts +33 -4
- package/src/frontmatter-text.ts +109 -0
- package/src/history.ts +13 -0
- package/src/hooks.ts +1 -1
- package/src/insights.ts +174 -0
- package/src/issues.ts +396 -0
- package/src/judge-verdict.ts +52 -0
- package/src/library.ts +1 -1
- package/src/limits.ts +38 -5
- package/src/linkable.ts +1 -1
- package/src/matter.ts +145 -0
- package/src/model-credential.ts +45 -0
- package/src/network.ts +35 -7
- package/src/notify.ts +22 -7
- package/src/okf.ts +1 -1
- package/src/online-checks.ts +450 -0
- package/src/operator.ts +213 -3
- package/src/otel-config.ts +67 -0
- package/src/otel.ts +100 -0
- package/src/outcome.ts +90 -0
- package/src/package.ts +8 -2
- package/src/platform.ts +1 -1
- package/src/prompt-layers.ts +90 -0
- package/src/provider-health.ts +2 -2
- package/src/providers.ts +7 -2
- package/src/run-container.ts +77 -8
- package/src/runner.ts +477 -79
- package/src/runs-filter.ts +88 -0
- package/src/scan.ts +153 -14
- package/src/starter.ts +27 -2
- package/src/step-exec.ts +168 -17
- package/src/store.ts +268 -9
- package/src/tokens.ts +68 -0
- package/src/tool-names.ts +1 -1
- package/src/tool-programs.ts +1 -1
- package/src/trace-tree.ts +187 -0
- package/src/translator.ts +13 -2
- package/src/trigger-log.ts +1 -0
- package/src/workspace-check.ts +41 -0
package/src/store.ts
CHANGED
|
@@ -31,7 +31,8 @@ import { platform } from "./platform.ts";
|
|
|
31
31
|
import { toLF, asRaw } from "./eol.ts";
|
|
32
32
|
import type { TestEffect } from "./test-mode.ts";
|
|
33
33
|
import type { OperatorEvent } from "./operator.ts";
|
|
34
|
-
import matter from "
|
|
34
|
+
import matter, { frontmatterErrorText, configMatter } from "./matter.ts";
|
|
35
|
+
import { readAgentsMdKeys } from "./agents-md.ts";
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* The frontmatter of one markdown file, as the runtime reads it. Exported
|
|
@@ -43,7 +44,7 @@ import matter from "gray-matter";
|
|
|
43
44
|
* the runtime, and there is nothing to fail to import.
|
|
44
45
|
*/
|
|
45
46
|
export function readFrontmatter(file: string): Record<string, unknown> {
|
|
46
|
-
return
|
|
47
|
+
return configMatter(fs.readFileSync(file, "utf8")).data as Record<string, unknown>;
|
|
47
48
|
}
|
|
48
49
|
import { ownToolNames, legacyUseNames, retiredToolNames } from "./tool-names.ts";
|
|
49
50
|
import { refNames } from "./refs.ts";
|
|
@@ -952,6 +953,12 @@ export interface ApiSpec {
|
|
|
952
953
|
export interface AgentInfo {
|
|
953
954
|
name: string;
|
|
954
955
|
description: string;
|
|
956
|
+
/** `owner:` — the person accountable for this agent: its own, else the
|
|
957
|
+
* workspace AGENTS.md's, else the account's. Null when none of the three
|
|
958
|
+
* names one. */
|
|
959
|
+
owner: string | null;
|
|
960
|
+
/** Which of the three `owner` came from. */
|
|
961
|
+
ownerFrom: OwnerSource | null;
|
|
955
962
|
model: string;
|
|
956
963
|
/** The other half of model selection. listAgents has always returned it;
|
|
957
964
|
* the interface did not say so, so every consumer had to re-read the file
|
|
@@ -1020,6 +1027,10 @@ export interface AgentInfo {
|
|
|
1020
1027
|
/** Tools this agent declares inline rather than in `tools:` — `apis:`,
|
|
1021
1028
|
* `scripts:` and `mcpServers:` names — which a limit may also name. */
|
|
1022
1029
|
inlineTools: string[];
|
|
1030
|
+
/** agent.md's frontmatter does not parse (bad YAML): the agent is listed
|
|
1031
|
+
* with defaults and this says why, rather than the listing throwing —
|
|
1032
|
+
* one broken file once took down every account's worker (20 Oct 2026). */
|
|
1033
|
+
frontmatterError?: string;
|
|
1023
1034
|
}
|
|
1024
1035
|
|
|
1025
1036
|
// Recognised HTTP verbs a tool may declare. Deliberately NOT named
|
|
@@ -1355,6 +1366,9 @@ export interface FlowInfo {
|
|
|
1355
1366
|
* overlap) — a default that changed underfoot would surprise every flow
|
|
1356
1367
|
* that legitimately runs in parallel. */
|
|
1357
1368
|
overlap: "skip" | "queue" | null;
|
|
1369
|
+
/** `issues: auto-close`: a run that goes fine closes this flow's open
|
|
1370
|
+
* issues (FOL-72 step 7). Unset is `stay-open`: a person resolves them. */
|
|
1371
|
+
issues?: "auto-close" | "stay-open";
|
|
1358
1372
|
/** Where this flow's runs stand in the queue against everyone else's:
|
|
1359
1373
|
* "high" is claimed before the round-robin, "low" after it. Adds to the
|
|
1360
1374
|
* account's own priority; the account's cap still bounds how many run
|
|
@@ -1467,6 +1481,13 @@ export interface FlowInfo {
|
|
|
1467
1481
|
* something rather than sitting in every file unread. */
|
|
1468
1482
|
description: string | null;
|
|
1469
1483
|
|
|
1484
|
+
/** `owner:` — the person accountable for this flow: its own, else the
|
|
1485
|
+
* workspace AGENTS.md's, else the account's. parseFlow sees only the
|
|
1486
|
+
* file, so it fills the flow's own; listFlows and readFlow add the rest. */
|
|
1487
|
+
owner: string | null;
|
|
1488
|
+
/** Which of the three `owner` came from. */
|
|
1489
|
+
ownerFrom: OwnerSource | null;
|
|
1490
|
+
|
|
1470
1491
|
/** Flow-level frontmatter values the parser could not read — a bad
|
|
1471
1492
|
* `overlap:`/`priority:`/`catchup:`/`on:`/`signature:`, or a step line
|
|
1472
1493
|
* whose `[[target]]` is malformed. Errors, the way a bad step option is,
|
|
@@ -1574,7 +1595,17 @@ export function listAgents(tenant: string, workspace: string): AgentInfo[] {
|
|
|
1574
1595
|
.filter((n) => fs.existsSync(path.join(dir, n, "agent.md")))
|
|
1575
1596
|
.sort()
|
|
1576
1597
|
.map((name) => {
|
|
1577
|
-
|
|
1598
|
+
// One agent whose YAML does not parse is that agent's problem, not
|
|
1599
|
+
// the account's: listed with defaults and the reason, never thrown
|
|
1600
|
+
// out of a listing every page, the scheduler and the worker's boot
|
|
1601
|
+
// read.
|
|
1602
|
+
let data: Record<string, any> = {};
|
|
1603
|
+
let frontmatterError: string | undefined;
|
|
1604
|
+
try {
|
|
1605
|
+
({ data } = configMatter(fs.readFileSync(path.join(dir, name, "agent.md"), "utf8")));
|
|
1606
|
+
} catch (err) {
|
|
1607
|
+
frontmatterError = frontmatterErrorText(err);
|
|
1608
|
+
}
|
|
1578
1609
|
const tools = refNames(data.tools);
|
|
1579
1610
|
const actions = webConfig(data, { ...accountFront, ...workspaceFront }).actions;
|
|
1580
1611
|
const readsWeb = tools.includes("web") && (!actions || actions.some((a) => a === "fetch" || a === "browse"));
|
|
@@ -1592,6 +1623,7 @@ export function listAgents(tenant: string, workspace: string): AgentInfo[] {
|
|
|
1592
1623
|
network,
|
|
1593
1624
|
name,
|
|
1594
1625
|
description: data.description ?? "",
|
|
1626
|
+
...resolveOwner(data.owner, workspaceFront.owner, accountFront.owner),
|
|
1595
1627
|
model: data.model ?? "default",
|
|
1596
1628
|
effort: data.effort ?? null,
|
|
1597
1629
|
tools,
|
|
@@ -1615,10 +1647,22 @@ export function listAgents(tenant: string, workspace: string): AgentInfo[] {
|
|
|
1615
1647
|
...(() => { const l = readLimits(data.limits); return { limits: l.limits, limitProblems: l.problems }; })(),
|
|
1616
1648
|
networkProblems: networkProblems(data.network),
|
|
1617
1649
|
inlineTools: inlineToolNames(data),
|
|
1650
|
+
...(frontmatterError ? { frontmatterError } : {}),
|
|
1618
1651
|
};
|
|
1619
1652
|
});
|
|
1620
1653
|
}
|
|
1621
1654
|
|
|
1655
|
+
// Kept here for callers that import it from the store; it lives in matter.ts
|
|
1656
|
+
// so the scanner can use it without the store.
|
|
1657
|
+
export { frontmatterErrorText };
|
|
1658
|
+
|
|
1659
|
+
/** The body of a file whose frontmatter would not parse: what follows the
|
|
1660
|
+
* closing fence, so a flow's steps can still be read and shown. */
|
|
1661
|
+
function bodyAfterFrontmatter(raw: string): string {
|
|
1662
|
+
const m = raw.match(/^---\n[\s\S]*?\n---[ \t]*(?:\n|$)/);
|
|
1663
|
+
return m ? raw.slice(m[0].length) : raw;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1622
1666
|
// A step targets an agent — `[[writer]]` — or another flow — `[[flow:digest]]`,
|
|
1623
1667
|
// which is how one flow triggers another.
|
|
1624
1668
|
// `(\S.*|)` rather than `(.*)`: the instruction either starts with a
|
|
@@ -1784,11 +1828,24 @@ export function parseFlow(file: string, raw: string): FlowInfo {
|
|
|
1784
1828
|
// every line kept its "\r", which STEP_RE's `.` will not match. Read it as
|
|
1785
1829
|
// LF; the line count, and so every step's `line`, is the same.
|
|
1786
1830
|
raw = raw.replace(/\r\n/g, "\n");
|
|
1787
|
-
|
|
1831
|
+
// Frontmatter that does not parse is a flow-level problem (an error in
|
|
1832
|
+
// check), not a throw: listFlows feeds every workspace listing, the
|
|
1833
|
+
// scheduler and the worker's boot. With no frontmatter read the flow is
|
|
1834
|
+
// manual, so a broken schedule never fires.
|
|
1835
|
+
let data: Record<string, any> = {};
|
|
1836
|
+
let content: string;
|
|
1837
|
+
let frontError: string | null = null;
|
|
1838
|
+
try {
|
|
1839
|
+
({ data, content } = configMatter(raw));
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
frontError = frontmatterErrorText(err);
|
|
1842
|
+
content = bodyAfterFrontmatter(raw);
|
|
1843
|
+
}
|
|
1788
1844
|
const steps: FlowStep[] = [];
|
|
1789
1845
|
// Flow-level problems: a malformed step target (below) and unreadable
|
|
1790
1846
|
// frontmatter enums (before the return). Reported as errors by flow-lint.
|
|
1791
1847
|
const frontProblems: string[] = [];
|
|
1848
|
+
if (frontError) frontProblems.push(frontError);
|
|
1792
1849
|
// Frontmatter is stripped by matter(), so add it back to keep line numbers
|
|
1793
1850
|
// pointing at the real file rather than the body.
|
|
1794
1851
|
const offset = raw.slice(0, raw.length - content.length).split("\n").length - 1;
|
|
@@ -1990,6 +2047,7 @@ export function parseFlow(file: string, raw: string): FlowInfo {
|
|
|
1990
2047
|
}
|
|
1991
2048
|
};
|
|
1992
2049
|
badEnum("overlap", data.overlap, ["skip", "queue"]);
|
|
2050
|
+
badEnum("issues", data.issues, ["stay-open", "auto-close"]);
|
|
1993
2051
|
badEnum("priority", data.priority, ["high", "normal", "low"]);
|
|
1994
2052
|
badEnum("catchup", data.catchup, ["none", "last"]);
|
|
1995
2053
|
badEnum("on", data.on, ["completed", "failed", "blocked", "any"]);
|
|
@@ -2005,6 +2063,7 @@ export function parseFlow(file: string, raw: string): FlowInfo {
|
|
|
2005
2063
|
model: data.model ?? null,
|
|
2006
2064
|
effort: data.effort ?? null,
|
|
2007
2065
|
overlap: data.overlap === "skip" || data.overlap === "queue" ? data.overlap : null,
|
|
2066
|
+
...(data.issues === "auto-close" || data.issues === "stay-open" ? { issues: data.issues } : {}),
|
|
2008
2067
|
live: data.live === true,
|
|
2009
2068
|
priority: data.priority === "high" || data.priority === "low" || data.priority === "normal" ? data.priority : null,
|
|
2010
2069
|
budget: runBudget(data.budget),
|
|
@@ -2041,6 +2100,7 @@ export function parseFlow(file: string, raw: string): FlowInfo {
|
|
|
2041
2100
|
approvers: approverList(data.approvers),
|
|
2042
2101
|
approveWithin: durationOf(data.approve_within),
|
|
2043
2102
|
description: typeof data.description === "string" ? data.description.trim() || null : null,
|
|
2103
|
+
...resolveOwner(data.owner, undefined, undefined, "flow"),
|
|
2044
2104
|
frontProblems,
|
|
2045
2105
|
steps,
|
|
2046
2106
|
};
|
|
@@ -2380,11 +2440,45 @@ function groupBlocks(blocks: FlowBlock[]): FlowBlock[][] {
|
|
|
2380
2440
|
export function listFlows(tenant: string, workspace: string): FlowInfo[] {
|
|
2381
2441
|
const dir = path.join(workspaceDir(tenant, workspace), "flows");
|
|
2382
2442
|
if (!fs.existsSync(dir)) return [];
|
|
2443
|
+
const owned = flowOwner(tenant, workspace);
|
|
2383
2444
|
return fs
|
|
2384
2445
|
.readdirSync(dir)
|
|
2385
2446
|
.filter((f) => f.endsWith(".md"))
|
|
2386
2447
|
.sort()
|
|
2387
|
-
.map((f) => parseFlow(f, fs.readFileSync(path.join(dir, f), "utf8")));
|
|
2448
|
+
.map((f) => owned(parseFlow(f, fs.readFileSync(path.join(dir, f), "utf8"))));
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
/** A flow with its owner filled from the workspace and account when its own
|
|
2452
|
+
* file names none. */
|
|
2453
|
+
function flowOwner(tenant: string, workspace: string): (flow: FlowInfo) => FlowInfo {
|
|
2454
|
+
let above: { workspace: unknown; account: unknown } | null = null;
|
|
2455
|
+
return (flow) => {
|
|
2456
|
+
if (flow.owner) return flow;
|
|
2457
|
+
above ??= {
|
|
2458
|
+
workspace: readAgentsMdKeys(workspaceDir(tenant, workspace)).owner,
|
|
2459
|
+
account: readAgentsMdKeys(accountDir(tenant)).owner,
|
|
2460
|
+
};
|
|
2461
|
+
return { ...flow, ...resolveOwner(undefined, above.workspace, above.account) };
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
export type OwnerSource = "agent" | "flow" | "workspace" | "account";
|
|
2466
|
+
|
|
2467
|
+
/** `owner:` down the cascade: the file's own, the workspace AGENTS.md's, the
|
|
2468
|
+
* account's. A blank or non-string value is no value. `own` is labelled
|
|
2469
|
+
* "agent" or "flow" by the caller's kind; here it is whichever is given. */
|
|
2470
|
+
export function resolveOwner(
|
|
2471
|
+
own: unknown,
|
|
2472
|
+
workspace?: unknown,
|
|
2473
|
+
account?: unknown,
|
|
2474
|
+
ownKind: "agent" | "flow" = "agent",
|
|
2475
|
+
): { owner: string | null; ownerFrom: OwnerSource | null } {
|
|
2476
|
+
const text = (v: unknown) => (typeof v === "string" && v.trim() ? v.trim() : null);
|
|
2477
|
+
for (const [v, from] of [[own, ownKind], [workspace, "workspace"], [account, "account"]] as const) {
|
|
2478
|
+
const t = text(v);
|
|
2479
|
+
if (t) return { owner: t, ownerFrom: from };
|
|
2480
|
+
}
|
|
2481
|
+
return { owner: null, ownerFrom: null };
|
|
2388
2482
|
}
|
|
2389
2483
|
|
|
2390
2484
|
/** One flow of a workspace by name, or null. The same read listFlows makes,
|
|
@@ -2395,7 +2489,7 @@ export function readFlow(tenant: string, workspace: string, flowName: string): F
|
|
|
2395
2489
|
if (!fs.existsSync(dir)) return null;
|
|
2396
2490
|
for (const f of fs.readdirSync(dir).filter((f) => f.endsWith(".md"))) {
|
|
2397
2491
|
const flow = parseFlow(f, fs.readFileSync(path.join(dir, f), "utf8"));
|
|
2398
|
-
if (flow.name === flowName) return flow;
|
|
2492
|
+
if (flow.name === flowName) return flowOwner(tenant, workspace)(flow);
|
|
2399
2493
|
}
|
|
2400
2494
|
return null;
|
|
2401
2495
|
}
|
|
@@ -3068,6 +3162,60 @@ export interface RunEvent {
|
|
|
3068
3162
|
/** Set on the line a hook wrote — a deny, a rewrite, context, a failure
|
|
3069
3163
|
* (hooks.ts). The run page and the Hooks page read these. */
|
|
3070
3164
|
hook?: { hook: string; event: string; did: string; tool?: string; reason?: string; ms: number };
|
|
3165
|
+
/** Set on a tool event whose content was recorded (FOL-54 step 4): the
|
|
3166
|
+
* call's input, on the call; its output, on the completion. The content is
|
|
3167
|
+
* under runs/<run>/calls/<call>.json, secrets masked — readCapturedCall.
|
|
3168
|
+
* Absent when the workspace says `capture: metadata`, and before 19 Oct
|
|
3169
|
+
* 2026. */
|
|
3170
|
+
captured?: boolean;
|
|
3171
|
+
/** On a tool call: the id of the model call that asked for it (a
|
|
3172
|
+
* ModelCall's `id`), so the trace can nest it. Since 19 Oct 2026. */
|
|
3173
|
+
inCall?: string;
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
/** One model call in a step (FOL-54 step 4): what the model was sent back
|
|
3177
|
+
* for and what came of it. A step's turns, one by one, where the step
|
|
3178
|
+
* record had only their sum. */
|
|
3179
|
+
export interface ModelCall {
|
|
3180
|
+
/** The provider's message id. */
|
|
3181
|
+
id: string;
|
|
3182
|
+
/** 1, 2, 3 … in the order they came back. */
|
|
3183
|
+
n: number;
|
|
3184
|
+
/** When the response arrived. */
|
|
3185
|
+
at: string;
|
|
3186
|
+
/** From the request going out (the step's start, or the last tool result
|
|
3187
|
+
* coming back) to the response arriving. */
|
|
3188
|
+
ms: number;
|
|
3189
|
+
model?: string;
|
|
3190
|
+
/** Why the model stopped: end_turn, tool_use, max_tokens, … */
|
|
3191
|
+
stop?: string;
|
|
3192
|
+
/** All input (fresh + cache), as the step's own `tokens` counts it. */
|
|
3193
|
+
tokens: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
3194
|
+
/** How many tools it called in this turn. */
|
|
3195
|
+
tools: number;
|
|
3196
|
+
/** Set when a sub-agent (`subagents:`) made the call. */
|
|
3197
|
+
subagent?: string;
|
|
3198
|
+
/** Which attempt of the step made it, from the second on. */
|
|
3199
|
+
attempt?: number;
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
/** The most of one tool input or output a run records (FOL-54 step 4). */
|
|
3203
|
+
export const CAPTURE_CAP = 64 * 1024;
|
|
3204
|
+
|
|
3205
|
+
/** What a recorded tool call took and gave, secrets masked, each part capped. */
|
|
3206
|
+
export interface CapturedCall {
|
|
3207
|
+
id: string;
|
|
3208
|
+
tool: string;
|
|
3209
|
+
input?: string;
|
|
3210
|
+
output?: string;
|
|
3211
|
+
/** The whole input's / output's length in characters, before masking and
|
|
3212
|
+
* the cap; `truncated` says whether the text was cut. */
|
|
3213
|
+
inputChars?: number;
|
|
3214
|
+
outputChars?: number;
|
|
3215
|
+
/** Either part cut; and which. */
|
|
3216
|
+
truncated?: boolean;
|
|
3217
|
+
inputCut?: boolean;
|
|
3218
|
+
outputCut?: boolean;
|
|
3071
3219
|
}
|
|
3072
3220
|
|
|
3073
3221
|
/** A question an agent asked a person while its step ran (tools: [ask]). */
|
|
@@ -3083,12 +3231,18 @@ export interface StepQuestion {
|
|
|
3083
3231
|
unanswered?: boolean;
|
|
3084
3232
|
}
|
|
3085
3233
|
|
|
3086
|
-
/** A message a person sent into a running step, and when the agent got it
|
|
3234
|
+
/** A message a person sent into a running step, and when the agent got it —
|
|
3235
|
+
* or, with `from`, one a colleague in the same parallel group sent it
|
|
3236
|
+
* (tools: [peers]); with `to`, one this step sent its colleagues. */
|
|
3087
3237
|
export interface StepMessage {
|
|
3088
3238
|
text: string;
|
|
3089
3239
|
by?: string | null;
|
|
3090
3240
|
at: string;
|
|
3091
3241
|
deliveredAt: string;
|
|
3242
|
+
/** The sibling agent that sent it (`by` is `peer:<agent>` then). */
|
|
3243
|
+
from?: string;
|
|
3244
|
+
/** The sibling agents this step sent it to. */
|
|
3245
|
+
to?: string[];
|
|
3092
3246
|
}
|
|
3093
3247
|
|
|
3094
3248
|
/** One attempt of a step — see StepRecord.tries. */
|
|
@@ -3137,8 +3291,17 @@ export function sumAttempts(tries: StepAttempt[]): Pick<StepAttempt, "costUsd" |
|
|
|
3137
3291
|
};
|
|
3138
3292
|
}
|
|
3139
3293
|
|
|
3140
|
-
export { type TokenUsage, tokensOf, sumTokens, cacheHitRate, measuredHitRate, runTokens } from "./tokens.ts";
|
|
3141
|
-
import { sumTokens, type TokenUsage } from "./tokens.ts";
|
|
3294
|
+
export { type TokenUsage, type PromptLayerRecord, tokensOf, sumTokens, cacheHitRate, measuredHitRate, runTokens, promptLayersChanged, promptCacheNote } from "./tokens.ts";
|
|
3295
|
+
import { sumTokens, type TokenUsage, type PromptLayerRecord } from "./tokens.ts";
|
|
3296
|
+
|
|
3297
|
+
/** The marker rescueStep puts in a rescue's instruction; how a rescue record
|
|
3298
|
+
* written before `rescueOf` existed is still recognised. */
|
|
3299
|
+
export const RESCUE_MARKER = "<rescue>\nYou are taking this step over:";
|
|
3300
|
+
|
|
3301
|
+
/** Is this record an on-fail rescue rather than a step of the flow? */
|
|
3302
|
+
export function isRescueRecord(step: { rescueOf?: string; instruction?: string | null }): boolean {
|
|
3303
|
+
return !!step.rescueOf || (step.instruction ?? "").includes(RESCUE_MARKER);
|
|
3304
|
+
}
|
|
3142
3305
|
|
|
3143
3306
|
export interface StepRecord {
|
|
3144
3307
|
agent: string;
|
|
@@ -3211,6 +3374,9 @@ export interface StepRecord {
|
|
|
3211
3374
|
* records from before this existed; readers fall back to the events. */
|
|
3212
3375
|
startedAt?: string;
|
|
3213
3376
|
finishedAt?: string;
|
|
3377
|
+
/** Every model call the step made, in order (FOL-54 step 4). Absent before
|
|
3378
|
+
* 19 Oct 2026. */
|
|
3379
|
+
calls?: ModelCall[];
|
|
3214
3380
|
/** Why a step was skipped, shown in the trace. */
|
|
3215
3381
|
skipReason?: string;
|
|
3216
3382
|
/** Evaluator loop, carried from the flow — see FlowStep. `loopRemaining`
|
|
@@ -3290,6 +3456,13 @@ export interface StepRecord {
|
|
|
3290
3456
|
* actually executed in. Carried steps provide context and are never
|
|
3291
3457
|
* billed again — the original run's ledger line already paid for them. */
|
|
3292
3458
|
carriedFrom?: string;
|
|
3459
|
+
/** Set on an on-fail rescue record (runner.ts rescueStep): the agent of the
|
|
3460
|
+
* failed step it took over. A rescue is not a step of the flow: a resumed
|
|
3461
|
+
* drive must not launch it as one of its group, and a re-run must not
|
|
3462
|
+
* copy it as work (21 Oct 2026: one rerun carried 77 fixers as pending
|
|
3463
|
+
* steps). Records written before this field are recognised by their
|
|
3464
|
+
* instruction's `<rescue>` block instead — see isRescueRecord. */
|
|
3465
|
+
rescueOf?: string;
|
|
3293
3466
|
/**
|
|
3294
3467
|
* Sandbox seconds this step rented, and how many of them were cold start.
|
|
3295
3468
|
* Set only on the isolated path — an in-process step rents nothing, so
|
|
@@ -3324,6 +3497,11 @@ export interface StepRecord {
|
|
|
3324
3497
|
* what says where a step's input tokens went. Absent before 18 Oct 2026. */
|
|
3325
3498
|
turns?: number | null;
|
|
3326
3499
|
systemPromptChars?: number;
|
|
3500
|
+
/** The system prompt's layers — name, length and hash of each, stable
|
|
3501
|
+
* ones first, the ones that change between runs marked. Two steps of
|
|
3502
|
+
* one agent compared layer by layer say why the model's cache missed
|
|
3503
|
+
* (prompt-layers.ts). Absent before 20 Oct 2026. */
|
|
3504
|
+
promptLayers?: PromptLayerRecord[];
|
|
3327
3505
|
/** Why the model's last turn ended (end_turn, max_tokens, tool_use…), as
|
|
3328
3506
|
* the SDK's closing result says. */
|
|
3329
3507
|
stopReason?: string | null;
|
|
@@ -3392,6 +3570,17 @@ export interface RunRecord {
|
|
|
3392
3570
|
* the agents decided was worth remembering, listed so a person can review
|
|
3393
3571
|
* the lesson without diffing the folder. */
|
|
3394
3572
|
memoryWrites?: string[];
|
|
3573
|
+
/** The workspace's deployed commit when the run was created — which
|
|
3574
|
+
* version of the files it ran (FOL-54 step 4). Absent for a workspace
|
|
3575
|
+
* never deployed, and before 19 Oct 2026. */
|
|
3576
|
+
revision?: string | null;
|
|
3577
|
+
/** When a worker first picked the run up: startedAt is when it was
|
|
3578
|
+
* created, so the gap is its time in the queue. */
|
|
3579
|
+
drivenAt?: string | null;
|
|
3580
|
+
/** A re-run's source (FOL-72 step 6): which run, the 1-based step it
|
|
3581
|
+
* started from, and the instruction it was given there when a person
|
|
3582
|
+
* edited it for this re-run alone. */
|
|
3583
|
+
rerunOf?: { run: string; step: number; instruction?: string };
|
|
3395
3584
|
/** Who asked for this run, when a person did: their email address. A
|
|
3396
3585
|
* schedule, a webhook and a chained flow have no one, and leave it unset.
|
|
3397
3586
|
* Kept so a gate can refuse the one approval nobody should be able to
|
|
@@ -3415,6 +3604,50 @@ export interface RunRecord {
|
|
|
3415
3604
|
steps: StepRecord[];
|
|
3416
3605
|
}
|
|
3417
3606
|
|
|
3607
|
+
// ---------------------------------------------------------------- captured calls
|
|
3608
|
+
//
|
|
3609
|
+
// What each tool call took and gave (FOL-54 step 4), beside the run under
|
|
3610
|
+
// runs/<run>/calls/<call>.json — out of the run record, which every list
|
|
3611
|
+
// and every page reads whole, and gone with the run (deleteRun removes
|
|
3612
|
+
// runs/<run>/). The runner writes them, secrets already masked.
|
|
3613
|
+
|
|
3614
|
+
const SAFE_ID = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
3615
|
+
|
|
3616
|
+
function capturedCallFile(workspaceRoot: string, runId: string, callId: string): string | null {
|
|
3617
|
+
if (!SAFE_ID.test(runId) || !SAFE_ID.test(callId) || callId.startsWith(".") || runId.startsWith(".")) return null;
|
|
3618
|
+
return path.join(workspaceRoot, "runs", runId, "calls", `${callId}.json`);
|
|
3619
|
+
}
|
|
3620
|
+
|
|
3621
|
+
/** Merge one part (the input, on the call; the output, on its completion)
|
|
3622
|
+
* into the call's file. */
|
|
3623
|
+
export function writeCapturedCall(workspaceRoot: string, runId: string, part: CapturedCall): void {
|
|
3624
|
+
const file = capturedCallFile(workspaceRoot, runId, part.id);
|
|
3625
|
+
if (!file) return;
|
|
3626
|
+
let prev: Partial<CapturedCall> = {};
|
|
3627
|
+
try {
|
|
3628
|
+
prev = JSON.parse(fs.readFileSync(file, "utf8")) as CapturedCall;
|
|
3629
|
+
} catch {
|
|
3630
|
+
/* the first part */
|
|
3631
|
+
}
|
|
3632
|
+
const next: CapturedCall = { ...prev, ...part, truncated: Boolean(prev.truncated || part.truncated) || undefined };
|
|
3633
|
+
if (!next.truncated) delete next.truncated;
|
|
3634
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
3635
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
3636
|
+
fs.writeFileSync(tmp, JSON.stringify(next));
|
|
3637
|
+
fs.renameSync(tmp, file);
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
/** One recorded call of a run, or null (not recorded, or not a call). */
|
|
3641
|
+
export function readCapturedCall(tenant: string, workspace: string, runId: string, callId: string): CapturedCall | null {
|
|
3642
|
+
const file = capturedCallFile(workspaceDir(tenant, workspace), runId, callId);
|
|
3643
|
+
if (!file) return null;
|
|
3644
|
+
try {
|
|
3645
|
+
return JSON.parse(fs.readFileSync(file, "utf8")) as CapturedCall;
|
|
3646
|
+
} catch {
|
|
3647
|
+
return null;
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3418
3651
|
export function runFilePath(tenant: string, workspace: string, runId: string) {
|
|
3419
3652
|
assertSafeName(runId, "run id");
|
|
3420
3653
|
return path.join(workspaceDir(tenant, workspace), "runs", `${runId}.json`);
|
|
@@ -3619,6 +3852,11 @@ export interface RunSummaryStep {
|
|
|
3619
3852
|
instruction?: string;
|
|
3620
3853
|
/** When a parked step last spoke — the moment the banner sorts by. */
|
|
3621
3854
|
waitingSince?: string;
|
|
3855
|
+
/** For search (FOL-72 step 9): the tools it called, how many of its calls
|
|
3856
|
+
* errored, and its model. Absent on rows written before 19 Oct 2026. */
|
|
3857
|
+
tools?: string[];
|
|
3858
|
+
toolErrors?: number;
|
|
3859
|
+
model?: string;
|
|
3622
3860
|
startedAt?: string;
|
|
3623
3861
|
finishedAt?: string;
|
|
3624
3862
|
}
|
|
@@ -3637,6 +3875,8 @@ export interface RunSummary {
|
|
|
3637
3875
|
tags?: string[];
|
|
3638
3876
|
startedBy?: string | null;
|
|
3639
3877
|
parkedAt?: string | null;
|
|
3878
|
+
/** The revision it ran (RunRecord.revision), for issues' "since". */
|
|
3879
|
+
revision?: string | null;
|
|
3640
3880
|
/** A step is parked for a person — what the approvals banner counts. */
|
|
3641
3881
|
awaitingApproval: boolean;
|
|
3642
3882
|
steps: RunSummaryStep[];
|
|
@@ -3680,6 +3920,7 @@ export function summarizeRun(run: RunRecord): RunSummary {
|
|
|
3680
3920
|
...(run.tags?.length ? { tags: run.tags } : {}),
|
|
3681
3921
|
...(run.startedBy ? { startedBy: run.startedBy } : {}),
|
|
3682
3922
|
...(run.parkedAt ? { parkedAt: run.parkedAt } : {}),
|
|
3923
|
+
...(run.revision ? { revision: run.revision } : {}),
|
|
3683
3924
|
awaitingApproval: run.steps.some((s) => s.status === "awaiting-approval" && s.waitFor !== "event"),
|
|
3684
3925
|
steps: run.steps.map((s) => {
|
|
3685
3926
|
const error = s.status === "failed" ? s.events.filter((e) => e.type === "error").at(-1)?.text : undefined;
|
|
@@ -3698,11 +3939,29 @@ export function summarizeRun(run: RunRecord): RunSummary {
|
|
|
3698
3939
|
...(s.status === "awaiting-approval" ? { instruction: s.instruction, waitingSince: s.events.at(-1)?.t ?? run.startedAt } : {}),
|
|
3699
3940
|
...(s.startedAt ? { startedAt: s.startedAt } : {}),
|
|
3700
3941
|
...(s.finishedAt ? { finishedAt: s.finishedAt } : {}),
|
|
3942
|
+
...searchFacts(s),
|
|
3701
3943
|
};
|
|
3702
3944
|
}),
|
|
3703
3945
|
};
|
|
3704
3946
|
}
|
|
3705
3947
|
|
|
3948
|
+
/** What a step's row carries for search: the tools it called (once each),
|
|
3949
|
+
* how many tool calls errored, and its model. */
|
|
3950
|
+
function searchFacts(s: StepRecord): { tools?: string[]; toolErrors?: number; model?: string } {
|
|
3951
|
+
const tools = new Set<string>();
|
|
3952
|
+
let errors = 0;
|
|
3953
|
+
for (const e of s.events) {
|
|
3954
|
+
if (e.type !== "tool") continue;
|
|
3955
|
+
if (e.ms == null) tools.add(e.text);
|
|
3956
|
+
else if (e.err) errors++;
|
|
3957
|
+
}
|
|
3958
|
+
return {
|
|
3959
|
+
...(tools.size ? { tools: [...tools].slice(0, 40) } : {}),
|
|
3960
|
+
...(errors ? { toolErrors: errors } : {}),
|
|
3961
|
+
...(s.model ? { model: s.model } : {}),
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3706
3965
|
/** A RunRecord with the shape a list reads and nothing else — see listAllRuns. */
|
|
3707
3966
|
export function runFromSummary(s: RunSummary): RunRecord {
|
|
3708
3967
|
const { awaitingApproval: _awaiting, ...run } = s;
|
package/src/tokens.ts
CHANGED
|
@@ -73,3 +73,71 @@ export function runTokens(run: { steps: readonly { tokens?: TokenUsage | null }[
|
|
|
73
73
|
const toks = run.steps.map((s) => s.tokens).filter((t): t is TokenUsage => !!t);
|
|
74
74
|
return toks.length ? sumTokens(toks) : null;
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* One layer of a step's system prompt as the record keeps it — no text,
|
|
79
|
+
* only what tells two steps' prompts apart. The layers are assembled in
|
|
80
|
+
* prompt-layers.ts; this is the shape the run page and the CLI read.
|
|
81
|
+
*/
|
|
82
|
+
export interface PromptLayerRecord {
|
|
83
|
+
name: string;
|
|
84
|
+
chars: number;
|
|
85
|
+
/** First 12 hex characters of the layer's SHA-256. */
|
|
86
|
+
hash: string;
|
|
87
|
+
/** False for a layer that changes between runs (memory, state, clock),
|
|
88
|
+
* placed after the cache boundary. Omitted when true. */
|
|
89
|
+
stable?: false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Which layers differ between two steps' prompts, by name: changed, added
|
|
94
|
+
* or removed. Empty when the prompts are the same, or either step has no
|
|
95
|
+
* layers on record.
|
|
96
|
+
*/
|
|
97
|
+
export function promptLayersChanged(prev: readonly PromptLayerRecord[] | null | undefined, next: readonly PromptLayerRecord[] | null | undefined): string[] {
|
|
98
|
+
if (!prev || !next) return [];
|
|
99
|
+
const before = new Map(prev.map((l) => [l.name, l.hash]));
|
|
100
|
+
const after = new Map(next.map((l) => [l.name, l.hash]));
|
|
101
|
+
const out: string[] = [];
|
|
102
|
+
for (const [name, hash] of after) if (before.get(name) !== hash) out.push(name);
|
|
103
|
+
for (const name of before.keys()) if (!after.has(name)) out.push(name);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A step that wrote this share of its input to the cache, or more, gets a
|
|
108
|
+
* note saying why. Below it the cache did its job: a step's turns each
|
|
109
|
+
* write their new tail, which is a few percent of a long step's input. */
|
|
110
|
+
export const CACHE_WRITE_NOTE_SHARE = 0.2;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Why a step's prompt cache missed, in one line, or null when it did not:
|
|
114
|
+
* the share of input written to the cache, and what differed from the same
|
|
115
|
+
* agent's previous step in the run — "memory, clock changed since step 2".
|
|
116
|
+
* The agent's first step in a run writes its prefix whatever happens, and
|
|
117
|
+
* says so; the same prompt written again means the entry had expired
|
|
118
|
+
* (five minutes on an API key) or siblings started before it was readable.
|
|
119
|
+
* Steps without layers on record (before 20 Oct 2026) get no note.
|
|
120
|
+
*/
|
|
121
|
+
export function promptCacheNote(
|
|
122
|
+
steps: readonly { agent: string; tokens?: TokenUsage | null; promptLayers?: readonly PromptLayerRecord[] | null }[],
|
|
123
|
+
index: number,
|
|
124
|
+
): string | null {
|
|
125
|
+
const step = steps[index];
|
|
126
|
+
if (!step?.promptLayers) return null;
|
|
127
|
+
const t = step.tokens;
|
|
128
|
+
if (!t || t.cacheWrite === undefined || !(t.input > 0)) return null;
|
|
129
|
+
const share = t.cacheWrite / t.input;
|
|
130
|
+
if (share < CACHE_WRITE_NOTE_SHARE) return null;
|
|
131
|
+
const head = `${Math.round(share * 100)}% of input written to the cache`;
|
|
132
|
+
let prev = -1;
|
|
133
|
+
for (let j = index - 1; j >= 0; j--) {
|
|
134
|
+
if (steps[j]!.agent === step.agent && steps[j]!.promptLayers) {
|
|
135
|
+
prev = j;
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (prev < 0) return `${head} — the agent's first step in this run`;
|
|
140
|
+
const changed = promptLayersChanged(steps[prev]!.promptLayers, step.promptLayers);
|
|
141
|
+
if (!changed.length) return `${head} — the same prompt as step ${prev + 1}; the entry had expired or was still being written`;
|
|
142
|
+
return `${head} — ${changed.join(", ")} changed since step ${prev + 1}`;
|
|
143
|
+
}
|
package/src/tool-names.ts
CHANGED
|
@@ -52,7 +52,7 @@ export const BUILTIN_TOOLS = new Set([
|
|
|
52
52
|
/** The platform's own groups, served in-process rather than by the SDK.
|
|
53
53
|
* `install` is granted to every agent anyway (install-tool.ts); naming it is
|
|
54
54
|
* accepted so a file that says so is not flagged. */
|
|
55
|
-
export const PLATFORM_GROUPS = new Set(["search", "history", "desks", "ask", "install"]);
|
|
55
|
+
export const PLATFORM_GROUPS = new Set(["search", "history", "desks", "ask", "peers", "install"]);
|
|
56
56
|
|
|
57
57
|
/** Would this name resolve to something the runtime provides? Built-ins win a
|
|
58
58
|
* clash, so a tool file of the same name is shadowed rather than granted. */
|
package/src/tool-programs.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import fs from "node:fs";
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import { builtinModules } from "node:module";
|
|
19
|
-
import matter from "
|
|
19
|
+
import matter from "./matter.ts";
|
|
20
20
|
import { parseRuntime } from "./runtime.ts";
|
|
21
21
|
|
|
22
22
|
import { workspaceDir, workspaceTools, type ToolDef } from "./store.ts";
|