@foldrun/core 2.3.0 → 2.5.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.
Files changed (135) hide show
  1. package/dist/src/agent-activity.d.ts +5 -0
  2. package/dist/src/agent-activity.js +31 -0
  3. package/dist/src/agent-import.js +1 -1
  4. package/dist/src/agent-tools.js +1 -1
  5. package/dist/src/agents-md.js +1 -1
  6. package/dist/src/alerts.d.ts +60 -0
  7. package/dist/src/alerts.js +161 -0
  8. package/dist/src/browser-pod.js +4 -0
  9. package/dist/src/capture.d.ts +23 -0
  10. package/dist/src/capture.js +80 -0
  11. package/dist/src/compare.d.ts +57 -0
  12. package/dist/src/compare.js +116 -0
  13. package/dist/src/completions.js +1 -0
  14. package/dist/src/deploy.d.ts +36 -1
  15. package/dist/src/deploy.js +170 -11
  16. package/dist/src/egress.d.ts +16 -1
  17. package/dist/src/evals.d.ts +5 -1
  18. package/dist/src/evals.js +26 -18
  19. package/dist/src/frontmatter-fields.js +9 -0
  20. package/dist/src/frontmatter-schema.js +3 -0
  21. package/dist/src/history.d.ts +4 -0
  22. package/dist/src/history.js +15 -0
  23. package/dist/src/hooks.js +1 -1
  24. package/dist/src/insights.d.ts +30 -0
  25. package/dist/src/insights.js +152 -0
  26. package/dist/src/issues.d.ts +140 -0
  27. package/dist/src/issues.js +302 -0
  28. package/dist/src/library.js +1 -1
  29. package/dist/src/limits.d.ts +8 -0
  30. package/dist/src/limits.js +36 -5
  31. package/dist/src/linkable.js +1 -1
  32. package/dist/src/matter.d.ts +8 -0
  33. package/dist/src/matter.js +28 -0
  34. package/dist/src/model-credential.d.ts +14 -0
  35. package/dist/src/model-credential.js +40 -0
  36. package/dist/src/model-env.d.ts +14 -2
  37. package/dist/src/model-env.js +34 -9
  38. package/dist/src/network.d.ts +12 -0
  39. package/dist/src/network.js +25 -1
  40. package/dist/src/notify.d.ts +16 -0
  41. package/dist/src/notify.js +17 -7
  42. package/dist/src/okf.js +1 -1
  43. package/dist/src/online-checks.d.ts +76 -0
  44. package/dist/src/online-checks.js +432 -0
  45. package/dist/src/operator.d.ts +277 -0
  46. package/dist/src/operator.js +176 -2
  47. package/dist/src/otel-config.d.ts +18 -0
  48. package/dist/src/otel-config.js +69 -0
  49. package/dist/src/otel.d.ts +38 -0
  50. package/dist/src/otel.js +85 -0
  51. package/dist/src/outcome.d.ts +40 -0
  52. package/dist/src/outcome.js +77 -0
  53. package/dist/src/package.js +1 -1
  54. package/dist/src/platform.d.ts +1 -1
  55. package/dist/src/prompt-layers.d.ts +30 -0
  56. package/dist/src/prompt-layers.js +77 -0
  57. package/dist/src/provider-health.js +1 -1
  58. package/dist/src/providers.d.ts +5 -0
  59. package/dist/src/providers.js +2 -2
  60. package/dist/src/run-container.d.ts +16 -2
  61. package/dist/src/run-container.js +57 -5
  62. package/dist/src/runner.d.ts +53 -2
  63. package/dist/src/runner.js +356 -70
  64. package/dist/src/runs-filter.d.ts +48 -0
  65. package/dist/src/runs-filter.js +73 -0
  66. package/dist/src/scan.d.ts +19 -1
  67. package/dist/src/scan.js +60 -7
  68. package/dist/src/starter.js +4 -2
  69. package/dist/src/step-exec.d.ts +29 -1
  70. package/dist/src/step-exec.js +106 -6
  71. package/dist/src/store.d.ts +130 -3
  72. package/dist/src/store.js +145 -6
  73. package/dist/src/tokens.d.ts +38 -0
  74. package/dist/src/tokens.js +57 -0
  75. package/dist/src/tool-names.js +1 -1
  76. package/dist/src/tool-programs.js +1 -1
  77. package/dist/src/trace-tree.d.ts +40 -0
  78. package/dist/src/trace-tree.js +156 -0
  79. package/dist/src/translator.d.ts +8 -0
  80. package/dist/src/translator.js +7 -2
  81. package/dist/src/trigger-log.d.ts +1 -1
  82. package/dist/src/workspace-check.d.ts +18 -0
  83. package/dist/src/workspace-check.js +34 -0
  84. package/package.json +2 -2
  85. package/src/agent-activity.ts +32 -0
  86. package/src/agent-import.ts +1 -1
  87. package/src/agent-tools.ts +1 -1
  88. package/src/agents-md.ts +1 -1
  89. package/src/alerts.ts +190 -0
  90. package/src/browser-pod.ts +3 -0
  91. package/src/capture.ts +88 -0
  92. package/src/compare.ts +146 -0
  93. package/src/completions.ts +1 -0
  94. package/src/deploy.ts +184 -10
  95. package/src/egress.ts +12 -2
  96. package/src/evals.ts +26 -18
  97. package/src/frontmatter-fields.ts +9 -0
  98. package/src/frontmatter-schema.ts +3 -0
  99. package/src/history.ts +13 -0
  100. package/src/hooks.ts +1 -1
  101. package/src/insights.ts +174 -0
  102. package/src/issues.ts +396 -0
  103. package/src/library.ts +1 -1
  104. package/src/limits.ts +38 -5
  105. package/src/linkable.ts +1 -1
  106. package/src/matter.ts +33 -0
  107. package/src/model-credential.ts +45 -0
  108. package/src/model-env.ts +42 -10
  109. package/src/network.ts +25 -1
  110. package/src/notify.ts +22 -7
  111. package/src/okf.ts +1 -1
  112. package/src/online-checks.ts +450 -0
  113. package/src/operator.ts +213 -3
  114. package/src/otel-config.ts +67 -0
  115. package/src/otel.ts +100 -0
  116. package/src/outcome.ts +90 -0
  117. package/src/package.ts +1 -1
  118. package/src/platform.ts +1 -1
  119. package/src/prompt-layers.ts +90 -0
  120. package/src/provider-health.ts +1 -1
  121. package/src/providers.ts +7 -2
  122. package/src/run-container.ts +66 -7
  123. package/src/runner.ts +363 -46
  124. package/src/runs-filter.ts +88 -0
  125. package/src/scan.ts +68 -7
  126. package/src/starter.ts +4 -2
  127. package/src/step-exec.ts +147 -9
  128. package/src/store.ts +254 -8
  129. package/src/tokens.ts +68 -0
  130. package/src/tool-names.ts +1 -1
  131. package/src/tool-programs.ts +1 -1
  132. package/src/trace-tree.ts +187 -0
  133. package/src/translator.ts +13 -2
  134. package/src/trigger-log.ts +1 -0
  135. package/src/workspace-check.ts +41 -0
@@ -0,0 +1,187 @@
1
+ // A run as one tree (FOL-72 step 5): run → step → model call → tool call,
2
+ // each with when it started and ended, what it cost in tokens and how it
3
+ // ended — the waterfall every agent-tracing tool opens on. Built from the
4
+ // run record alone (steps, step.calls, the paired tool events), so the run
5
+ // page, GET …/runs/<id>/trace and `foldrun report --tree` read one function.
6
+ //
7
+ // A tool call nests under the model call that asked for it: its event names
8
+ // the call (`inCall`) since 19 Oct 2026; older records are matched by time,
9
+ // to the last model call that answered before the tool started. Pure: no
10
+ // node imports, so the browser can use it.
11
+
12
+ import type { RunRecord, StepRecord, RunEvent, ModelCall } from "./store.ts";
13
+
14
+ export type SpanKind = "run" | "step" | "model" | "tool" | "error";
15
+ export type SpanStatus = "ok" | "error" | "running" | "waiting" | "skipped" | "pending";
16
+
17
+ export interface TraceSpan {
18
+ /** Stable within the run: "run", "s2", "s2/m3", "s2/t:<call>", "s2/e5". */
19
+ id: string;
20
+ kind: SpanKind;
21
+ /** What the line says: the flow, the agent, the model, the tool. */
22
+ label: string;
23
+ /** Epoch ms; null when the record does not say. */
24
+ start: number | null;
25
+ end: number | null;
26
+ status: SpanStatus;
27
+ /** Model and step spans: tokens (input is all input, as on the record). */
28
+ tokens?: { input: number; output: number; cacheRead: number; cacheWrite: number };
29
+ /** Step spans: the provider's token cost, USD. */
30
+ costUsd?: number | null;
31
+ model?: string;
32
+ /** Model spans: why it stopped. */
33
+ stop?: string;
34
+ /** Tool spans: the call id, and whether its input and output were recorded. */
35
+ call?: string;
36
+ captured?: boolean;
37
+ /** Error spans and failed tools: the words. */
38
+ text?: string;
39
+ subagent?: string;
40
+ /** Index into run.steps, on a step and everything under it. */
41
+ step?: number;
42
+ children: TraceSpan[];
43
+ }
44
+
45
+ const ms = (iso: string | null | undefined) => {
46
+ if (!iso) return null;
47
+ const t = Date.parse(iso);
48
+ return Number.isNaN(t) ? null : t;
49
+ };
50
+
51
+ function stepStatus(s: StepRecord): SpanStatus {
52
+ if (s.status === "completed") return "ok";
53
+ if (s.status === "failed") return "error";
54
+ if (s.status === "running") return "running";
55
+ if (s.status === "awaiting-approval") return "waiting";
56
+ if (s.status === "skipped") return "skipped";
57
+ return "pending";
58
+ }
59
+
60
+ /** Tool events paired: the call's start, its duration, whether it erred. */
61
+ function toolCalls(events: RunEvent[]): { e: RunEvent; start: number | null; end: number | null; err: boolean; captured: boolean }[] {
62
+ const byCall = new Map<string, { e: RunEvent; start: number | null; end: number | null; err: boolean; captured: boolean }>();
63
+ const out: { e: RunEvent; start: number | null; end: number | null; err: boolean; captured: boolean }[] = [];
64
+ for (const e of events) {
65
+ if (e.type !== "tool") continue;
66
+ if (e.call && e.ms != null && byCall.has(e.call)) {
67
+ const open = byCall.get(e.call)!;
68
+ open.end = (open.start ?? ms(e.t) ?? 0) + e.ms;
69
+ open.err ||= e.err === true;
70
+ open.captured ||= e.captured === true;
71
+ continue;
72
+ }
73
+ const c = { e, start: ms(e.t), end: null as number | null, err: e.err === true, captured: e.captured === true };
74
+ if (e.call) byCall.set(e.call, c);
75
+ out.push(c);
76
+ }
77
+ return out;
78
+ }
79
+
80
+ function modelSpan(c: ModelCall, step: number): TraceSpan {
81
+ const at = ms(c.at);
82
+ return {
83
+ id: `s${step}/m${c.attempt && c.attempt > 1 ? `${c.attempt}.` : ""}${c.n}`,
84
+ kind: "model",
85
+ label: c.model ?? "model",
86
+ // The request went out `ms` before the response began arriving.
87
+ start: at !== null ? at - c.ms : null,
88
+ end: at,
89
+ status: "ok",
90
+ tokens: c.tokens,
91
+ ...(c.model ? { model: c.model } : {}),
92
+ ...(c.stop ? { stop: c.stop } : {}),
93
+ ...(c.subagent ? { subagent: c.subagent } : {}),
94
+ step,
95
+ children: [],
96
+ };
97
+ }
98
+
99
+ function stepSpan(s: StepRecord, i: number, now: number): TraceSpan {
100
+ const first = s.events[0] ? ms(s.events[0].t) : null;
101
+ const last = s.events.length ? ms(s.events[s.events.length - 1]!.t) : null;
102
+ const status = stepStatus(s);
103
+ const span: TraceSpan = {
104
+ id: `s${i}`,
105
+ kind: "step",
106
+ label: s.agent,
107
+ start: ms(s.startedAt) ?? first,
108
+ end: ms(s.finishedAt) ?? (status === "running" ? now : last),
109
+ status,
110
+ ...(s.tokens ? { tokens: { input: s.tokens.input, output: s.tokens.output, cacheRead: s.tokens.cacheRead ?? 0, cacheWrite: s.tokens.cacheWrite ?? 0 } } : {}),
111
+ costUsd: s.costUsd ?? null,
112
+ ...(s.model ? { model: s.model } : {}),
113
+ step: i,
114
+ children: [],
115
+ };
116
+
117
+ const models = (s.calls ?? []).map((c) => ({ c, span: modelSpan(c, i) }));
118
+ const byId = new Map(models.map((m) => [m.c.id, m.span]));
119
+ const homeOf = (e: RunEvent, start: number | null): TraceSpan => {
120
+ if (e.inCall && byId.has(e.inCall)) return byId.get(e.inCall)!;
121
+ // Older records: the last model call (of the same agent) that answered
122
+ // before the tool started.
123
+ const same = models.filter((m) => (m.c.subagent ?? null) === (e.subagent ?? null) && m.span.end !== null && start !== null && m.span.end <= start + 50);
124
+ return same.length ? same[same.length - 1]!.span : span;
125
+ };
126
+
127
+ for (const t of toolCalls(s.events)) {
128
+ const home = homeOf(t.e, t.start);
129
+ const tool: TraceSpan = {
130
+ id: `s${i}/t:${t.e.call ?? `${t.start}`}`,
131
+ kind: "tool",
132
+ label: t.e.text,
133
+ start: t.start,
134
+ end: t.end ?? (status === "running" ? now : t.start),
135
+ status: t.err ? "error" : t.end === null && status === "running" ? "running" : "ok",
136
+ ...(t.e.call ? { call: t.e.call } : {}),
137
+ ...(t.captured ? { captured: true } : {}),
138
+ ...(t.e.subagent ? { subagent: t.e.subagent } : {}),
139
+ step: i,
140
+ children: [],
141
+ };
142
+ home.children.push(tool);
143
+ }
144
+ // A model call lasts until the last tool it asked for has finished.
145
+ for (const m of models) {
146
+ const ends = m.span.children.map((c) => c.end).filter((x): x is number => x !== null);
147
+ if (ends.length && m.span.end !== null) m.span.end = Math.max(m.span.end, ...ends);
148
+ }
149
+ span.children.push(...models.map((m) => m.span));
150
+
151
+ // Errors, as leaves: why the step failed, read without opening the trace.
152
+ s.events.forEach((e, k) => {
153
+ if (e.type !== "error") return;
154
+ span.children.push({ id: `s${i}/e${k}`, kind: "error", label: "error", start: ms(e.t), end: ms(e.t), status: "error", text: e.text, step: i, children: [] });
155
+ });
156
+ span.children.sort((a, b) => (a.start ?? Infinity) - (b.start ?? Infinity));
157
+ return span;
158
+ }
159
+
160
+ export function buildTraceTree(run: RunRecord, now = Date.now()): TraceSpan {
161
+ const status: SpanStatus =
162
+ run.status === "completed" ? "ok" : run.status === "failed" ? "error" : run.status === "awaiting-approval" ? "waiting" : run.status === "queued" ? "pending" : "running";
163
+ return {
164
+ id: "run",
165
+ kind: "run",
166
+ label: run.flow,
167
+ start: ms(run.startedAt),
168
+ end: ms(run.finishedAt) ?? (status === "running" || status === "waiting" ? now : null),
169
+ status,
170
+ children: run.steps.map((s, i) => stepSpan(s, i, now)),
171
+ };
172
+ }
173
+
174
+ /** The ids from the root to the first thing that went wrong — what the
175
+ * view opens on; [] when nothing did. A failed tool beats the step's error
176
+ * line, which beats the failed step itself. */
177
+ export function failurePath(root: TraceSpan): string[] {
178
+ let best: { path: string[]; rank: number } | null = null;
179
+ const rank = (s: TraceSpan) => (s.kind === "tool" ? 3 : s.kind === "error" ? 2 : s.kind === "step" ? 1 : 0);
180
+ const walk = (s: TraceSpan, path: string[]) => {
181
+ const here = [...path, s.id];
182
+ if (s.status === "error" && s.kind !== "run" && (!best || rank(s) > best.rank)) best = { path: here, rank: rank(s) };
183
+ for (const c of s.children) walk(c, here);
184
+ };
185
+ walk(root, []);
186
+ return best ? (best as { path: string[] }).path : [];
187
+ }
package/src/translator.ts CHANGED
@@ -85,6 +85,11 @@ export interface TranslatorSpec {
85
85
  /** The provider block's `params:` — merged into the outgoing body after
86
86
  * the translation, so a file can set what this format does not model. */
87
87
  params?: Record<string, unknown>;
88
+ /** `prompt_cache_key` for an endpoint that routes its prompt cache by
89
+ * one (OpenAI, Mistral): the same key for every request that shares a
90
+ * prefix — one per agent (prompt-layers.ts). Unset where the preset
91
+ * does not say the parameter is understood. */
92
+ cacheKey?: string;
88
93
  /** How the endpoint is named in the trace. */
89
94
  label?: string;
90
95
  /** The step's egress lease, when the sandbox must not hold the key: the
@@ -185,12 +190,14 @@ export function toChatCompletions(
185
190
  maxTokensParam?: "max_tokens" | "max_completion_tokens";
186
191
  reasoningEffort?: boolean;
187
192
  stream: boolean;
193
+ cacheKey?: string;
188
194
  /** Merged in last, so the file wins over the translation. */
189
195
  params?: Record<string, unknown>;
190
196
  },
191
197
  drop: Dropped = dropped(),
192
198
  ): Json {
193
199
  const out: Json = { model: req.model, stream: opts.stream };
200
+ if (opts.cacheKey) out.prompt_cache_key = opts.cacheKey;
194
201
  const messages: Json[] = [];
195
202
 
196
203
  const system = textOf(req.system);
@@ -335,11 +342,13 @@ export function toResponses(
335
342
  opts: {
336
343
  reasoningEffort?: boolean;
337
344
  stream: boolean;
345
+ cacheKey?: string;
338
346
  params?: Record<string, unknown>;
339
347
  },
340
348
  drop: Dropped = dropped(),
341
349
  ): Json {
342
350
  const out: Json = { model: req.model, stream: opts.stream, store: false };
351
+ if (opts.cacheKey) out.prompt_cache_key = opts.cacheKey;
343
352
  const system = textOf(req.system);
344
353
  if (system) out.instructions = system;
345
354
 
@@ -948,8 +957,8 @@ export async function startTranslator(spec: TranslatorSpec): Promise<RunningTran
948
957
  const drop = dropped();
949
958
  const responses = spec.wire === "responses";
950
959
  const outbound = responses
951
- ? toResponses(body, { reasoningEffort: spec.reasoningEffort, params: spec.params, stream }, drop)
952
- : toChatCompletions(body, { maxTokensParam: spec.maxTokensParam, reasoningEffort: spec.reasoningEffort, params: spec.params, stream }, drop);
960
+ ? toResponses(body, { reasoningEffort: spec.reasoningEffort, params: spec.params, cacheKey: spec.cacheKey, stream }, drop)
961
+ : toChatCompletions(body, { maxTokensParam: spec.maxTokensParam, reasoningEffort: spec.reasoningEffort, params: spec.params, cacheKey: spec.cacheKey, stream }, drop);
953
962
  for (const line of drop.lines()) if (!log.includes(line)) log.push(line);
954
963
 
955
964
  let upstream: Response;
@@ -1089,6 +1098,7 @@ export function translatorSpecFor(spec: {
1089
1098
  name?: string | null;
1090
1099
  maxTokensParam?: "max_tokens" | "max_completion_tokens";
1091
1100
  reasoningEffort?: boolean;
1101
+ cacheKey?: string;
1092
1102
  }): TranslatorSpec | null {
1093
1103
  if (spec.format !== "openai" && spec.format !== "responses") return null;
1094
1104
  return {
@@ -1099,6 +1109,7 @@ export function translatorSpecFor(spec: {
1099
1109
  params: spec.params,
1100
1110
  maxTokensParam: spec.maxTokensParam,
1101
1111
  reasoningEffort: spec.reasoningEffort,
1112
+ ...(spec.cacheKey ? { cacheKey: spec.cacheKey } : {}),
1102
1113
  label: spec.name ? `${spec.name} (${spec.baseUrl})` : spec.baseUrl,
1103
1114
  };
1104
1115
  }
@@ -39,6 +39,7 @@ export type TriggerOutcome =
39
39
  | "debounced"
40
40
  | "quarantined"
41
41
  | "paused"
42
+ | "halted"
42
43
  | "overlap-skipped"
43
44
  | "missed"
44
45
  | "failed";
@@ -0,0 +1,41 @@
1
+ // One workspace's findings, as the Checks page, `GET …/check` and the daily
2
+ // rescan see them (FOL-58): the deploy's checks and the whole scanner, with
3
+ // what the files alone do not carry — the account's network: and the
4
+ // outward tools of the library and the workspace, nearest wins.
5
+
6
+ import { accountDir, workspaceDir, workspaceTools, type OutwardMark } from "./store.ts";
7
+ import { readAgentsMdKeys } from "./agents-md.ts";
8
+ import { libraryTools } from "./library.ts";
9
+ import { checkFiles, readTree } from "./deploy.ts";
10
+ import type { DeployFile } from "./store.ts";
11
+ import type { ScanFinding, ScanOptions } from "./scan.ts";
12
+
13
+ /** What the checks read of a workspace, at most. Data (state/ and memory/,
14
+ * what agents write) past these is left out and named; the rest past its
15
+ * budget is not read at all (TreeTooLarge). On 20 Oct 2026 the daily
16
+ * rescan read 1.2 GB of one desk's state/ into a 2 GB heap and crash-looped
17
+ * the worker. The rest is 64 MiB because a deploy is 10 MiB a push. */
18
+ export const CHECK_LIMITS = {
19
+ maxDataFileBytes: 5 * 1024 * 1024,
20
+ maxDataBytes: 32 * 1024 * 1024,
21
+ maxDataFiles: 5000,
22
+ maxSourceBytes: 64 * 1024 * 1024,
23
+ } as const;
24
+
25
+ /** A workspace as the checks read it: the deploy's tree within CHECK_LIMITS.
26
+ * Read once and hand it to everything that checks. */
27
+ export function checkTree(dir: string, onSkip?: (path: string, bytes: number) => void): DeployFile[] {
28
+ return readTree(dir, { ...CHECK_LIMITS, onSkip });
29
+ }
30
+
31
+ export function checkOptions(tenant: string, workspace: string): ScanOptions {
32
+ const tools = { ...libraryTools(tenant), ...workspaceTools(tenant, workspace) };
33
+ const outwardTools = Object.entries(tools).filter(([, d]) => (d as OutwardMark).outward === true).map(([n]) => n);
34
+ // AGENTS.md, or the older project.md, as the runner reads it (Codex review).
35
+ const accountNetwork = readAgentsMdKeys(accountDir(tenant)).network;
36
+ return { outwardTools, accountNetwork };
37
+ }
38
+
39
+ export function checkWorkspace(tenant: string, workspace: string, tree: DeployFile[] = checkTree(workspaceDir(tenant, workspace))): ScanFinding[] {
40
+ return checkFiles(tree, checkOptions(tenant, workspace));
41
+ }