@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/dist/src/store.js
CHANGED
|
@@ -27,7 +27,8 @@ import { recordRevision, registerTreeReader } from "./history.js";
|
|
|
27
27
|
import { dataRoot, singleWorkspace, singleAccountRoot, entriesNoFollow } from "./paths.js";
|
|
28
28
|
import { platform } from "./platform.js";
|
|
29
29
|
import { toLF, asRaw } from "./eol.js";
|
|
30
|
-
import matter from "
|
|
30
|
+
import matter, { frontmatterErrorText, configMatter } from "./matter.js";
|
|
31
|
+
import { readAgentsMdKeys } from "./agents-md.js";
|
|
31
32
|
/**
|
|
32
33
|
* The frontmatter of one markdown file, as the runtime reads it. Exported
|
|
33
34
|
* for the CLI: `foldrun probe` used to import gray-matter itself, which is
|
|
@@ -38,7 +39,7 @@ import matter from "gray-matter";
|
|
|
38
39
|
* the runtime, and there is nothing to fail to import.
|
|
39
40
|
*/
|
|
40
41
|
export function readFrontmatter(file) {
|
|
41
|
-
return
|
|
42
|
+
return configMatter(fs.readFileSync(file, "utf8")).data;
|
|
42
43
|
}
|
|
43
44
|
import { ownToolNames, legacyUseNames, retiredToolNames } from "./tool-names.js";
|
|
44
45
|
import { refNames } from "./refs.js";
|
|
@@ -1118,7 +1119,18 @@ export function listAgents(tenant, workspace) {
|
|
|
1118
1119
|
.filter((n) => fs.existsSync(path.join(dir, n, "agent.md")))
|
|
1119
1120
|
.sort()
|
|
1120
1121
|
.map((name) => {
|
|
1121
|
-
|
|
1122
|
+
// One agent whose YAML does not parse is that agent's problem, not
|
|
1123
|
+
// the account's: listed with defaults and the reason, never thrown
|
|
1124
|
+
// out of a listing every page, the scheduler and the worker's boot
|
|
1125
|
+
// read.
|
|
1126
|
+
let data = {};
|
|
1127
|
+
let frontmatterError;
|
|
1128
|
+
try {
|
|
1129
|
+
({ data } = configMatter(fs.readFileSync(path.join(dir, name, "agent.md"), "utf8")));
|
|
1130
|
+
}
|
|
1131
|
+
catch (err) {
|
|
1132
|
+
frontmatterError = frontmatterErrorText(err);
|
|
1133
|
+
}
|
|
1122
1134
|
const tools = refNames(data.tools);
|
|
1123
1135
|
const actions = webConfig(data, { ...accountFront, ...workspaceFront }).actions;
|
|
1124
1136
|
const readsWeb = tools.includes("web") && (!actions || actions.some((a) => a === "fetch" || a === "browse"));
|
|
@@ -1141,6 +1153,7 @@ export function listAgents(tenant, workspace) {
|
|
|
1141
1153
|
network,
|
|
1142
1154
|
name,
|
|
1143
1155
|
description: data.description ?? "",
|
|
1156
|
+
...resolveOwner(data.owner, workspaceFront.owner, accountFront.owner),
|
|
1144
1157
|
model: data.model ?? "default",
|
|
1145
1158
|
effort: data.effort ?? null,
|
|
1146
1159
|
tools,
|
|
@@ -1164,9 +1177,19 @@ export function listAgents(tenant, workspace) {
|
|
|
1164
1177
|
...(() => { const l = readLimits(data.limits); return { limits: l.limits, limitProblems: l.problems }; })(),
|
|
1165
1178
|
networkProblems: networkProblems(data.network),
|
|
1166
1179
|
inlineTools: inlineToolNames(data),
|
|
1180
|
+
...(frontmatterError ? { frontmatterError } : {}),
|
|
1167
1181
|
};
|
|
1168
1182
|
});
|
|
1169
1183
|
}
|
|
1184
|
+
// Kept here for callers that import it from the store; it lives in matter.ts
|
|
1185
|
+
// so the scanner can use it without the store.
|
|
1186
|
+
export { frontmatterErrorText };
|
|
1187
|
+
/** The body of a file whose frontmatter would not parse: what follows the
|
|
1188
|
+
* closing fence, so a flow's steps can still be read and shown. */
|
|
1189
|
+
function bodyAfterFrontmatter(raw) {
|
|
1190
|
+
const m = raw.match(/^---\n[\s\S]*?\n---[ \t]*(?:\n|$)/);
|
|
1191
|
+
return m ? raw.slice(m[0].length) : raw;
|
|
1192
|
+
}
|
|
1170
1193
|
// A step targets an agent — `[[writer]]` — or another flow — `[[flow:digest]]`,
|
|
1171
1194
|
// which is how one flow triggers another.
|
|
1172
1195
|
// `(\S.*|)` rather than `(.*)`: the instruction either starts with a
|
|
@@ -1326,11 +1349,26 @@ export function parseFlow(file, raw) {
|
|
|
1326
1349
|
// every line kept its "\r", which STEP_RE's `.` will not match. Read it as
|
|
1327
1350
|
// LF; the line count, and so every step's `line`, is the same.
|
|
1328
1351
|
raw = raw.replace(/\r\n/g, "\n");
|
|
1329
|
-
|
|
1352
|
+
// Frontmatter that does not parse is a flow-level problem (an error in
|
|
1353
|
+
// check), not a throw: listFlows feeds every workspace listing, the
|
|
1354
|
+
// scheduler and the worker's boot. With no frontmatter read the flow is
|
|
1355
|
+
// manual, so a broken schedule never fires.
|
|
1356
|
+
let data = {};
|
|
1357
|
+
let content;
|
|
1358
|
+
let frontError = null;
|
|
1359
|
+
try {
|
|
1360
|
+
({ data, content } = configMatter(raw));
|
|
1361
|
+
}
|
|
1362
|
+
catch (err) {
|
|
1363
|
+
frontError = frontmatterErrorText(err);
|
|
1364
|
+
content = bodyAfterFrontmatter(raw);
|
|
1365
|
+
}
|
|
1330
1366
|
const steps = [];
|
|
1331
1367
|
// Flow-level problems: a malformed step target (below) and unreadable
|
|
1332
1368
|
// frontmatter enums (before the return). Reported as errors by flow-lint.
|
|
1333
1369
|
const frontProblems = [];
|
|
1370
|
+
if (frontError)
|
|
1371
|
+
frontProblems.push(frontError);
|
|
1334
1372
|
// Frontmatter is stripped by matter(), so add it back to keep line numbers
|
|
1335
1373
|
// pointing at the real file rather than the body.
|
|
1336
1374
|
const offset = raw.slice(0, raw.length - content.length).split("\n").length - 1;
|
|
@@ -1573,6 +1611,7 @@ export function parseFlow(file, raw) {
|
|
|
1573
1611
|
}
|
|
1574
1612
|
};
|
|
1575
1613
|
badEnum("overlap", data.overlap, ["skip", "queue"]);
|
|
1614
|
+
badEnum("issues", data.issues, ["stay-open", "auto-close"]);
|
|
1576
1615
|
badEnum("priority", data.priority, ["high", "normal", "low"]);
|
|
1577
1616
|
badEnum("catchup", data.catchup, ["none", "last"]);
|
|
1578
1617
|
badEnum("on", data.on, ["completed", "failed", "blocked", "any"]);
|
|
@@ -1587,6 +1626,7 @@ export function parseFlow(file, raw) {
|
|
|
1587
1626
|
model: data.model ?? null,
|
|
1588
1627
|
effort: data.effort ?? null,
|
|
1589
1628
|
overlap: data.overlap === "skip" || data.overlap === "queue" ? data.overlap : null,
|
|
1629
|
+
...(data.issues === "auto-close" || data.issues === "stay-open" ? { issues: data.issues } : {}),
|
|
1590
1630
|
live: data.live === true,
|
|
1591
1631
|
priority: data.priority === "high" || data.priority === "low" || data.priority === "normal" ? data.priority : null,
|
|
1592
1632
|
budget: runBudget(data.budget),
|
|
@@ -1622,6 +1662,7 @@ export function parseFlow(file, raw) {
|
|
|
1622
1662
|
approvers: approverList(data.approvers),
|
|
1623
1663
|
approveWithin: durationOf(data.approve_within),
|
|
1624
1664
|
description: typeof data.description === "string" ? data.description.trim() || null : null,
|
|
1665
|
+
...resolveOwner(data.owner, undefined, undefined, "flow"),
|
|
1625
1666
|
frontProblems,
|
|
1626
1667
|
steps,
|
|
1627
1668
|
};
|
|
@@ -1932,11 +1973,38 @@ export function listFlows(tenant, workspace) {
|
|
|
1932
1973
|
const dir = path.join(workspaceDir(tenant, workspace), "flows");
|
|
1933
1974
|
if (!fs.existsSync(dir))
|
|
1934
1975
|
return [];
|
|
1976
|
+
const owned = flowOwner(tenant, workspace);
|
|
1935
1977
|
return fs
|
|
1936
1978
|
.readdirSync(dir)
|
|
1937
1979
|
.filter((f) => f.endsWith(".md"))
|
|
1938
1980
|
.sort()
|
|
1939
|
-
.map((f) => parseFlow(f, fs.readFileSync(path.join(dir, f), "utf8")));
|
|
1981
|
+
.map((f) => owned(parseFlow(f, fs.readFileSync(path.join(dir, f), "utf8"))));
|
|
1982
|
+
}
|
|
1983
|
+
/** A flow with its owner filled from the workspace and account when its own
|
|
1984
|
+
* file names none. */
|
|
1985
|
+
function flowOwner(tenant, workspace) {
|
|
1986
|
+
let above = null;
|
|
1987
|
+
return (flow) => {
|
|
1988
|
+
if (flow.owner)
|
|
1989
|
+
return flow;
|
|
1990
|
+
above ??= {
|
|
1991
|
+
workspace: readAgentsMdKeys(workspaceDir(tenant, workspace)).owner,
|
|
1992
|
+
account: readAgentsMdKeys(accountDir(tenant)).owner,
|
|
1993
|
+
};
|
|
1994
|
+
return { ...flow, ...resolveOwner(undefined, above.workspace, above.account) };
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
/** `owner:` down the cascade: the file's own, the workspace AGENTS.md's, the
|
|
1998
|
+
* account's. A blank or non-string value is no value. `own` is labelled
|
|
1999
|
+
* "agent" or "flow" by the caller's kind; here it is whichever is given. */
|
|
2000
|
+
export function resolveOwner(own, workspace, account, ownKind = "agent") {
|
|
2001
|
+
const text = (v) => (typeof v === "string" && v.trim() ? v.trim() : null);
|
|
2002
|
+
for (const [v, from] of [[own, ownKind], [workspace, "workspace"], [account, "account"]]) {
|
|
2003
|
+
const t = text(v);
|
|
2004
|
+
if (t)
|
|
2005
|
+
return { owner: t, ownerFrom: from };
|
|
2006
|
+
}
|
|
2007
|
+
return { owner: null, ownerFrom: null };
|
|
1940
2008
|
}
|
|
1941
2009
|
/** One flow of a workspace by name, or null. The same read listFlows makes,
|
|
1942
2010
|
* named so callers that want a single flow — the approval rules, the
|
|
@@ -1948,7 +2016,7 @@ export function readFlow(tenant, workspace, flowName) {
|
|
|
1948
2016
|
for (const f of fs.readdirSync(dir).filter((f) => f.endsWith(".md"))) {
|
|
1949
2017
|
const flow = parseFlow(f, fs.readFileSync(path.join(dir, f), "utf8"));
|
|
1950
2018
|
if (flow.name === flowName)
|
|
1951
|
-
return flow;
|
|
2019
|
+
return flowOwner(tenant, workspace)(flow);
|
|
1952
2020
|
}
|
|
1953
2021
|
return null;
|
|
1954
2022
|
}
|
|
@@ -2568,6 +2636,8 @@ export function flowPatternTemplate(pattern, name, agents) {
|
|
|
2568
2636
|
return FLOW_TEMPLATE(name, a(0, "my-agent"));
|
|
2569
2637
|
}
|
|
2570
2638
|
}
|
|
2639
|
+
/** The most of one tool input or output a run records (FOL-54 step 4). */
|
|
2640
|
+
export const CAPTURE_CAP = 64 * 1024;
|
|
2571
2641
|
/** The step's totals across its attempts, from the rows. Null where no
|
|
2572
2642
|
* attempt reported the figure — an in-process step rents no sandbox, and
|
|
2573
2643
|
* billing reads null as zero, not as "unknown, charge something". */
|
|
@@ -2583,8 +2653,60 @@ export function sumAttempts(tries) {
|
|
|
2583
2653
|
...(turns.length ? { turns: turns.reduce((a, b) => a + b, 0) } : {}),
|
|
2584
2654
|
};
|
|
2585
2655
|
}
|
|
2586
|
-
export { tokensOf, sumTokens, cacheHitRate, measuredHitRate, runTokens } from "./tokens.js";
|
|
2656
|
+
export { tokensOf, sumTokens, cacheHitRate, measuredHitRate, runTokens, promptLayersChanged, promptCacheNote } from "./tokens.js";
|
|
2587
2657
|
import { sumTokens } from "./tokens.js";
|
|
2658
|
+
/** The marker rescueStep puts in a rescue's instruction; how a rescue record
|
|
2659
|
+
* written before `rescueOf` existed is still recognised. */
|
|
2660
|
+
export const RESCUE_MARKER = "<rescue>\nYou are taking this step over:";
|
|
2661
|
+
/** Is this record an on-fail rescue rather than a step of the flow? */
|
|
2662
|
+
export function isRescueRecord(step) {
|
|
2663
|
+
return !!step.rescueOf || (step.instruction ?? "").includes(RESCUE_MARKER);
|
|
2664
|
+
}
|
|
2665
|
+
// ---------------------------------------------------------------- captured calls
|
|
2666
|
+
//
|
|
2667
|
+
// What each tool call took and gave (FOL-54 step 4), beside the run under
|
|
2668
|
+
// runs/<run>/calls/<call>.json — out of the run record, which every list
|
|
2669
|
+
// and every page reads whole, and gone with the run (deleteRun removes
|
|
2670
|
+
// runs/<run>/). The runner writes them, secrets already masked.
|
|
2671
|
+
const SAFE_ID = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
2672
|
+
function capturedCallFile(workspaceRoot, runId, callId) {
|
|
2673
|
+
if (!SAFE_ID.test(runId) || !SAFE_ID.test(callId) || callId.startsWith(".") || runId.startsWith("."))
|
|
2674
|
+
return null;
|
|
2675
|
+
return path.join(workspaceRoot, "runs", runId, "calls", `${callId}.json`);
|
|
2676
|
+
}
|
|
2677
|
+
/** Merge one part (the input, on the call; the output, on its completion)
|
|
2678
|
+
* into the call's file. */
|
|
2679
|
+
export function writeCapturedCall(workspaceRoot, runId, part) {
|
|
2680
|
+
const file = capturedCallFile(workspaceRoot, runId, part.id);
|
|
2681
|
+
if (!file)
|
|
2682
|
+
return;
|
|
2683
|
+
let prev = {};
|
|
2684
|
+
try {
|
|
2685
|
+
prev = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2686
|
+
}
|
|
2687
|
+
catch {
|
|
2688
|
+
/* the first part */
|
|
2689
|
+
}
|
|
2690
|
+
const next = { ...prev, ...part, truncated: Boolean(prev.truncated || part.truncated) || undefined };
|
|
2691
|
+
if (!next.truncated)
|
|
2692
|
+
delete next.truncated;
|
|
2693
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
2694
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2695
|
+
fs.writeFileSync(tmp, JSON.stringify(next));
|
|
2696
|
+
fs.renameSync(tmp, file);
|
|
2697
|
+
}
|
|
2698
|
+
/** One recorded call of a run, or null (not recorded, or not a call). */
|
|
2699
|
+
export function readCapturedCall(tenant, workspace, runId, callId) {
|
|
2700
|
+
const file = capturedCallFile(workspaceDir(tenant, workspace), runId, callId);
|
|
2701
|
+
if (!file)
|
|
2702
|
+
return null;
|
|
2703
|
+
try {
|
|
2704
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2705
|
+
}
|
|
2706
|
+
catch {
|
|
2707
|
+
return null;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2588
2710
|
export function runFilePath(tenant, workspace, runId) {
|
|
2589
2711
|
assertSafeName(runId, "run id");
|
|
2590
2712
|
return path.join(workspaceDir(tenant, workspace), "runs", `${runId}.json`);
|
|
@@ -2763,6 +2885,7 @@ export function summarizeRun(run) {
|
|
|
2763
2885
|
...(run.tags?.length ? { tags: run.tags } : {}),
|
|
2764
2886
|
...(run.startedBy ? { startedBy: run.startedBy } : {}),
|
|
2765
2887
|
...(run.parkedAt ? { parkedAt: run.parkedAt } : {}),
|
|
2888
|
+
...(run.revision ? { revision: run.revision } : {}),
|
|
2766
2889
|
awaitingApproval: run.steps.some((s) => s.status === "awaiting-approval" && s.waitFor !== "event"),
|
|
2767
2890
|
steps: run.steps.map((s) => {
|
|
2768
2891
|
const error = s.status === "failed" ? s.events.filter((e) => e.type === "error").at(-1)?.text : undefined;
|
|
@@ -2781,10 +2904,30 @@ export function summarizeRun(run) {
|
|
|
2781
2904
|
...(s.status === "awaiting-approval" ? { instruction: s.instruction, waitingSince: s.events.at(-1)?.t ?? run.startedAt } : {}),
|
|
2782
2905
|
...(s.startedAt ? { startedAt: s.startedAt } : {}),
|
|
2783
2906
|
...(s.finishedAt ? { finishedAt: s.finishedAt } : {}),
|
|
2907
|
+
...searchFacts(s),
|
|
2784
2908
|
};
|
|
2785
2909
|
}),
|
|
2786
2910
|
};
|
|
2787
2911
|
}
|
|
2912
|
+
/** What a step's row carries for search: the tools it called (once each),
|
|
2913
|
+
* how many tool calls errored, and its model. */
|
|
2914
|
+
function searchFacts(s) {
|
|
2915
|
+
const tools = new Set();
|
|
2916
|
+
let errors = 0;
|
|
2917
|
+
for (const e of s.events) {
|
|
2918
|
+
if (e.type !== "tool")
|
|
2919
|
+
continue;
|
|
2920
|
+
if (e.ms == null)
|
|
2921
|
+
tools.add(e.text);
|
|
2922
|
+
else if (e.err)
|
|
2923
|
+
errors++;
|
|
2924
|
+
}
|
|
2925
|
+
return {
|
|
2926
|
+
...(tools.size ? { tools: [...tools].slice(0, 40) } : {}),
|
|
2927
|
+
...(errors ? { toolErrors: errors } : {}),
|
|
2928
|
+
...(s.model ? { model: s.model } : {}),
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2788
2931
|
/** A RunRecord with the shape a list reads and nothing else — see listAllRuns. */
|
|
2789
2932
|
export function runFromSummary(s) {
|
|
2790
2933
|
const { awaitingApproval: _awaiting, ...run } = s;
|
package/dist/src/tokens.d.ts
CHANGED
|
@@ -36,3 +36,41 @@ export declare function runTokens(run: {
|
|
|
36
36
|
tokens?: TokenUsage | null;
|
|
37
37
|
}[];
|
|
38
38
|
}): TokenUsage | null;
|
|
39
|
+
/**
|
|
40
|
+
* One layer of a step's system prompt as the record keeps it — no text,
|
|
41
|
+
* only what tells two steps' prompts apart. The layers are assembled in
|
|
42
|
+
* prompt-layers.ts; this is the shape the run page and the CLI read.
|
|
43
|
+
*/
|
|
44
|
+
export interface PromptLayerRecord {
|
|
45
|
+
name: string;
|
|
46
|
+
chars: number;
|
|
47
|
+
/** First 12 hex characters of the layer's SHA-256. */
|
|
48
|
+
hash: string;
|
|
49
|
+
/** False for a layer that changes between runs (memory, state, clock),
|
|
50
|
+
* placed after the cache boundary. Omitted when true. */
|
|
51
|
+
stable?: false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Which layers differ between two steps' prompts, by name: changed, added
|
|
55
|
+
* or removed. Empty when the prompts are the same, or either step has no
|
|
56
|
+
* layers on record.
|
|
57
|
+
*/
|
|
58
|
+
export declare function promptLayersChanged(prev: readonly PromptLayerRecord[] | null | undefined, next: readonly PromptLayerRecord[] | null | undefined): string[];
|
|
59
|
+
/** A step that wrote this share of its input to the cache, or more, gets a
|
|
60
|
+
* note saying why. Below it the cache did its job: a step's turns each
|
|
61
|
+
* write their new tail, which is a few percent of a long step's input. */
|
|
62
|
+
export declare const CACHE_WRITE_NOTE_SHARE = 0.2;
|
|
63
|
+
/**
|
|
64
|
+
* Why a step's prompt cache missed, in one line, or null when it did not:
|
|
65
|
+
* the share of input written to the cache, and what differed from the same
|
|
66
|
+
* agent's previous step in the run — "memory, clock changed since step 2".
|
|
67
|
+
* The agent's first step in a run writes its prefix whatever happens, and
|
|
68
|
+
* says so; the same prompt written again means the entry had expired
|
|
69
|
+
* (five minutes on an API key) or siblings started before it was readable.
|
|
70
|
+
* Steps without layers on record (before 20 Oct 2026) get no note.
|
|
71
|
+
*/
|
|
72
|
+
export declare function promptCacheNote(steps: readonly {
|
|
73
|
+
agent: string;
|
|
74
|
+
tokens?: TokenUsage | null;
|
|
75
|
+
promptLayers?: readonly PromptLayerRecord[] | null;
|
|
76
|
+
}[], index: number): string | null;
|
package/dist/src/tokens.js
CHANGED
|
@@ -56,3 +56,60 @@ export function runTokens(run) {
|
|
|
56
56
|
const toks = run.steps.map((s) => s.tokens).filter((t) => !!t);
|
|
57
57
|
return toks.length ? sumTokens(toks) : null;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Which layers differ between two steps' prompts, by name: changed, added
|
|
61
|
+
* or removed. Empty when the prompts are the same, or either step has no
|
|
62
|
+
* layers on record.
|
|
63
|
+
*/
|
|
64
|
+
export function promptLayersChanged(prev, next) {
|
|
65
|
+
if (!prev || !next)
|
|
66
|
+
return [];
|
|
67
|
+
const before = new Map(prev.map((l) => [l.name, l.hash]));
|
|
68
|
+
const after = new Map(next.map((l) => [l.name, l.hash]));
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const [name, hash] of after)
|
|
71
|
+
if (before.get(name) !== hash)
|
|
72
|
+
out.push(name);
|
|
73
|
+
for (const name of before.keys())
|
|
74
|
+
if (!after.has(name))
|
|
75
|
+
out.push(name);
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
/** A step that wrote this share of its input to the cache, or more, gets a
|
|
79
|
+
* note saying why. Below it the cache did its job: a step's turns each
|
|
80
|
+
* write their new tail, which is a few percent of a long step's input. */
|
|
81
|
+
export const CACHE_WRITE_NOTE_SHARE = 0.2;
|
|
82
|
+
/**
|
|
83
|
+
* Why a step's prompt cache missed, in one line, or null when it did not:
|
|
84
|
+
* the share of input written to the cache, and what differed from the same
|
|
85
|
+
* agent's previous step in the run — "memory, clock changed since step 2".
|
|
86
|
+
* The agent's first step in a run writes its prefix whatever happens, and
|
|
87
|
+
* says so; the same prompt written again means the entry had expired
|
|
88
|
+
* (five minutes on an API key) or siblings started before it was readable.
|
|
89
|
+
* Steps without layers on record (before 20 Oct 2026) get no note.
|
|
90
|
+
*/
|
|
91
|
+
export function promptCacheNote(steps, index) {
|
|
92
|
+
const step = steps[index];
|
|
93
|
+
if (!step?.promptLayers)
|
|
94
|
+
return null;
|
|
95
|
+
const t = step.tokens;
|
|
96
|
+
if (!t || t.cacheWrite === undefined || !(t.input > 0))
|
|
97
|
+
return null;
|
|
98
|
+
const share = t.cacheWrite / t.input;
|
|
99
|
+
if (share < CACHE_WRITE_NOTE_SHARE)
|
|
100
|
+
return null;
|
|
101
|
+
const head = `${Math.round(share * 100)}% of input written to the cache`;
|
|
102
|
+
let prev = -1;
|
|
103
|
+
for (let j = index - 1; j >= 0; j--) {
|
|
104
|
+
if (steps[j].agent === step.agent && steps[j].promptLayers) {
|
|
105
|
+
prev = j;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (prev < 0)
|
|
110
|
+
return `${head} — the agent's first step in this run`;
|
|
111
|
+
const changed = promptLayersChanged(steps[prev].promptLayers, step.promptLayers);
|
|
112
|
+
if (!changed.length)
|
|
113
|
+
return `${head} — the same prompt as step ${prev + 1}; the entry had expired or was still being written`;
|
|
114
|
+
return `${head} — ${changed.join(", ")} changed since step ${prev + 1}`;
|
|
115
|
+
}
|
package/dist/src/tool-names.js
CHANGED
|
@@ -47,7 +47,7 @@ export const BUILTIN_TOOLS = new Set([
|
|
|
47
47
|
/** The platform's own groups, served in-process rather than by the SDK.
|
|
48
48
|
* `install` is granted to every agent anyway (install-tool.ts); naming it is
|
|
49
49
|
* accepted so a file that says so is not flagged. */
|
|
50
|
-
export const PLATFORM_GROUPS = new Set(["search", "history", "desks", "ask", "install"]);
|
|
50
|
+
export const PLATFORM_GROUPS = new Set(["search", "history", "desks", "ask", "peers", "install"]);
|
|
51
51
|
/** Would this name resolve to something the runtime provides? Built-ins win a
|
|
52
52
|
* clash, so a tool file of the same name is shadowed rather than granted. */
|
|
53
53
|
export function isRuntimeTool(name) {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { builtinModules } from "node:module";
|
|
18
|
-
import matter from "
|
|
18
|
+
import matter from "./matter.js";
|
|
19
19
|
import { parseRuntime } from "./runtime.js";
|
|
20
20
|
import { workspaceDir, workspaceTools } from "./store.js";
|
|
21
21
|
import { libraryDir, libraryTools } from "./library.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { RunRecord } from "./store.ts";
|
|
2
|
+
export type SpanKind = "run" | "step" | "model" | "tool" | "error";
|
|
3
|
+
export type SpanStatus = "ok" | "error" | "running" | "waiting" | "skipped" | "pending";
|
|
4
|
+
export interface TraceSpan {
|
|
5
|
+
/** Stable within the run: "run", "s2", "s2/m3", "s2/t:<call>", "s2/e5". */
|
|
6
|
+
id: string;
|
|
7
|
+
kind: SpanKind;
|
|
8
|
+
/** What the line says: the flow, the agent, the model, the tool. */
|
|
9
|
+
label: string;
|
|
10
|
+
/** Epoch ms; null when the record does not say. */
|
|
11
|
+
start: number | null;
|
|
12
|
+
end: number | null;
|
|
13
|
+
status: SpanStatus;
|
|
14
|
+
/** Model and step spans: tokens (input is all input, as on the record). */
|
|
15
|
+
tokens?: {
|
|
16
|
+
input: number;
|
|
17
|
+
output: number;
|
|
18
|
+
cacheRead: number;
|
|
19
|
+
cacheWrite: number;
|
|
20
|
+
};
|
|
21
|
+
/** Step spans: the provider's token cost, USD. */
|
|
22
|
+
costUsd?: number | null;
|
|
23
|
+
model?: string;
|
|
24
|
+
/** Model spans: why it stopped. */
|
|
25
|
+
stop?: string;
|
|
26
|
+
/** Tool spans: the call id, and whether its input and output were recorded. */
|
|
27
|
+
call?: string;
|
|
28
|
+
captured?: boolean;
|
|
29
|
+
/** Error spans and failed tools: the words. */
|
|
30
|
+
text?: string;
|
|
31
|
+
subagent?: string;
|
|
32
|
+
/** Index into run.steps, on a step and everything under it. */
|
|
33
|
+
step?: number;
|
|
34
|
+
children: TraceSpan[];
|
|
35
|
+
}
|
|
36
|
+
export declare function buildTraceTree(run: RunRecord, now?: number): TraceSpan;
|
|
37
|
+
/** The ids from the root to the first thing that went wrong — what the
|
|
38
|
+
* view opens on; [] when nothing did. A failed tool beats the step's error
|
|
39
|
+
* line, which beats the failed step itself. */
|
|
40
|
+
export declare function failurePath(root: TraceSpan): string[];
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
const ms = (iso) => {
|
|
12
|
+
if (!iso)
|
|
13
|
+
return null;
|
|
14
|
+
const t = Date.parse(iso);
|
|
15
|
+
return Number.isNaN(t) ? null : t;
|
|
16
|
+
};
|
|
17
|
+
function stepStatus(s) {
|
|
18
|
+
if (s.status === "completed")
|
|
19
|
+
return "ok";
|
|
20
|
+
if (s.status === "failed")
|
|
21
|
+
return "error";
|
|
22
|
+
if (s.status === "running")
|
|
23
|
+
return "running";
|
|
24
|
+
if (s.status === "awaiting-approval")
|
|
25
|
+
return "waiting";
|
|
26
|
+
if (s.status === "skipped")
|
|
27
|
+
return "skipped";
|
|
28
|
+
return "pending";
|
|
29
|
+
}
|
|
30
|
+
/** Tool events paired: the call's start, its duration, whether it erred. */
|
|
31
|
+
function toolCalls(events) {
|
|
32
|
+
const byCall = new Map();
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const e of events) {
|
|
35
|
+
if (e.type !== "tool")
|
|
36
|
+
continue;
|
|
37
|
+
if (e.call && e.ms != null && byCall.has(e.call)) {
|
|
38
|
+
const open = byCall.get(e.call);
|
|
39
|
+
open.end = (open.start ?? ms(e.t) ?? 0) + e.ms;
|
|
40
|
+
open.err ||= e.err === true;
|
|
41
|
+
open.captured ||= e.captured === true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const c = { e, start: ms(e.t), end: null, err: e.err === true, captured: e.captured === true };
|
|
45
|
+
if (e.call)
|
|
46
|
+
byCall.set(e.call, c);
|
|
47
|
+
out.push(c);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function modelSpan(c, step) {
|
|
52
|
+
const at = ms(c.at);
|
|
53
|
+
return {
|
|
54
|
+
id: `s${step}/m${c.attempt && c.attempt > 1 ? `${c.attempt}.` : ""}${c.n}`,
|
|
55
|
+
kind: "model",
|
|
56
|
+
label: c.model ?? "model",
|
|
57
|
+
// The request went out `ms` before the response began arriving.
|
|
58
|
+
start: at !== null ? at - c.ms : null,
|
|
59
|
+
end: at,
|
|
60
|
+
status: "ok",
|
|
61
|
+
tokens: c.tokens,
|
|
62
|
+
...(c.model ? { model: c.model } : {}),
|
|
63
|
+
...(c.stop ? { stop: c.stop } : {}),
|
|
64
|
+
...(c.subagent ? { subagent: c.subagent } : {}),
|
|
65
|
+
step,
|
|
66
|
+
children: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function stepSpan(s, i, now) {
|
|
70
|
+
const first = s.events[0] ? ms(s.events[0].t) : null;
|
|
71
|
+
const last = s.events.length ? ms(s.events[s.events.length - 1].t) : null;
|
|
72
|
+
const status = stepStatus(s);
|
|
73
|
+
const span = {
|
|
74
|
+
id: `s${i}`,
|
|
75
|
+
kind: "step",
|
|
76
|
+
label: s.agent,
|
|
77
|
+
start: ms(s.startedAt) ?? first,
|
|
78
|
+
end: ms(s.finishedAt) ?? (status === "running" ? now : last),
|
|
79
|
+
status,
|
|
80
|
+
...(s.tokens ? { tokens: { input: s.tokens.input, output: s.tokens.output, cacheRead: s.tokens.cacheRead ?? 0, cacheWrite: s.tokens.cacheWrite ?? 0 } } : {}),
|
|
81
|
+
costUsd: s.costUsd ?? null,
|
|
82
|
+
...(s.model ? { model: s.model } : {}),
|
|
83
|
+
step: i,
|
|
84
|
+
children: [],
|
|
85
|
+
};
|
|
86
|
+
const models = (s.calls ?? []).map((c) => ({ c, span: modelSpan(c, i) }));
|
|
87
|
+
const byId = new Map(models.map((m) => [m.c.id, m.span]));
|
|
88
|
+
const homeOf = (e, start) => {
|
|
89
|
+
if (e.inCall && byId.has(e.inCall))
|
|
90
|
+
return byId.get(e.inCall);
|
|
91
|
+
// Older records: the last model call (of the same agent) that answered
|
|
92
|
+
// before the tool started.
|
|
93
|
+
const same = models.filter((m) => (m.c.subagent ?? null) === (e.subagent ?? null) && m.span.end !== null && start !== null && m.span.end <= start + 50);
|
|
94
|
+
return same.length ? same[same.length - 1].span : span;
|
|
95
|
+
};
|
|
96
|
+
for (const t of toolCalls(s.events)) {
|
|
97
|
+
const home = homeOf(t.e, t.start);
|
|
98
|
+
const tool = {
|
|
99
|
+
id: `s${i}/t:${t.e.call ?? `${t.start}`}`,
|
|
100
|
+
kind: "tool",
|
|
101
|
+
label: t.e.text,
|
|
102
|
+
start: t.start,
|
|
103
|
+
end: t.end ?? (status === "running" ? now : t.start),
|
|
104
|
+
status: t.err ? "error" : t.end === null && status === "running" ? "running" : "ok",
|
|
105
|
+
...(t.e.call ? { call: t.e.call } : {}),
|
|
106
|
+
...(t.captured ? { captured: true } : {}),
|
|
107
|
+
...(t.e.subagent ? { subagent: t.e.subagent } : {}),
|
|
108
|
+
step: i,
|
|
109
|
+
children: [],
|
|
110
|
+
};
|
|
111
|
+
home.children.push(tool);
|
|
112
|
+
}
|
|
113
|
+
// A model call lasts until the last tool it asked for has finished.
|
|
114
|
+
for (const m of models) {
|
|
115
|
+
const ends = m.span.children.map((c) => c.end).filter((x) => x !== null);
|
|
116
|
+
if (ends.length && m.span.end !== null)
|
|
117
|
+
m.span.end = Math.max(m.span.end, ...ends);
|
|
118
|
+
}
|
|
119
|
+
span.children.push(...models.map((m) => m.span));
|
|
120
|
+
// Errors, as leaves: why the step failed, read without opening the trace.
|
|
121
|
+
s.events.forEach((e, k) => {
|
|
122
|
+
if (e.type !== "error")
|
|
123
|
+
return;
|
|
124
|
+
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: [] });
|
|
125
|
+
});
|
|
126
|
+
span.children.sort((a, b) => (a.start ?? Infinity) - (b.start ?? Infinity));
|
|
127
|
+
return span;
|
|
128
|
+
}
|
|
129
|
+
export function buildTraceTree(run, now = Date.now()) {
|
|
130
|
+
const status = run.status === "completed" ? "ok" : run.status === "failed" ? "error" : run.status === "awaiting-approval" ? "waiting" : run.status === "queued" ? "pending" : "running";
|
|
131
|
+
return {
|
|
132
|
+
id: "run",
|
|
133
|
+
kind: "run",
|
|
134
|
+
label: run.flow,
|
|
135
|
+
start: ms(run.startedAt),
|
|
136
|
+
end: ms(run.finishedAt) ?? (status === "running" || status === "waiting" ? now : null),
|
|
137
|
+
status,
|
|
138
|
+
children: run.steps.map((s, i) => stepSpan(s, i, now)),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** The ids from the root to the first thing that went wrong — what the
|
|
142
|
+
* view opens on; [] when nothing did. A failed tool beats the step's error
|
|
143
|
+
* line, which beats the failed step itself. */
|
|
144
|
+
export function failurePath(root) {
|
|
145
|
+
let best = null;
|
|
146
|
+
const rank = (s) => (s.kind === "tool" ? 3 : s.kind === "error" ? 2 : s.kind === "step" ? 1 : 0);
|
|
147
|
+
const walk = (s, path) => {
|
|
148
|
+
const here = [...path, s.id];
|
|
149
|
+
if (s.status === "error" && s.kind !== "run" && (!best || rank(s) > best.rank))
|
|
150
|
+
best = { path: here, rank: rank(s) };
|
|
151
|
+
for (const c of s.children)
|
|
152
|
+
walk(c, here);
|
|
153
|
+
};
|
|
154
|
+
walk(root, []);
|
|
155
|
+
return best ? best.path : [];
|
|
156
|
+
}
|
package/dist/src/translator.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ export interface TranslatorSpec {
|
|
|
28
28
|
/** The provider block's `params:` — merged into the outgoing body after
|
|
29
29
|
* the translation, so a file can set what this format does not model. */
|
|
30
30
|
params?: Record<string, unknown>;
|
|
31
|
+
/** `prompt_cache_key` for an endpoint that routes its prompt cache by
|
|
32
|
+
* one (OpenAI, Mistral): the same key for every request that shares a
|
|
33
|
+
* prefix — one per agent (prompt-layers.ts). Unset where the preset
|
|
34
|
+
* does not say the parameter is understood. */
|
|
35
|
+
cacheKey?: string;
|
|
31
36
|
/** How the endpoint is named in the trace. */
|
|
32
37
|
label?: string;
|
|
33
38
|
/** The step's egress lease, when the sandbox must not hold the key: the
|
|
@@ -77,6 +82,7 @@ export declare function toChatCompletions(req: Json, opts: {
|
|
|
77
82
|
maxTokensParam?: "max_tokens" | "max_completion_tokens";
|
|
78
83
|
reasoningEffort?: boolean;
|
|
79
84
|
stream: boolean;
|
|
85
|
+
cacheKey?: string;
|
|
80
86
|
/** Merged in last, so the file wins over the translation. */
|
|
81
87
|
params?: Record<string, unknown>;
|
|
82
88
|
}, drop?: Dropped): Json;
|
|
@@ -93,6 +99,7 @@ export declare function toChatCompletions(req: Json, opts: {
|
|
|
93
99
|
export declare function toResponses(req: Json, opts: {
|
|
94
100
|
reasoningEffort?: boolean;
|
|
95
101
|
stream: boolean;
|
|
102
|
+
cacheKey?: string;
|
|
96
103
|
params?: Record<string, unknown>;
|
|
97
104
|
}, drop?: Dropped): Json;
|
|
98
105
|
/** A finished Chat Completion as an Anthropic message. Pure. */
|
|
@@ -173,5 +180,6 @@ export declare function translatorSpecFor(spec: {
|
|
|
173
180
|
name?: string | null;
|
|
174
181
|
maxTokensParam?: "max_tokens" | "max_completion_tokens";
|
|
175
182
|
reasoningEffort?: boolean;
|
|
183
|
+
cacheKey?: string;
|
|
176
184
|
}): TranslatorSpec | null;
|
|
177
185
|
export {};
|
package/dist/src/translator.js
CHANGED
|
@@ -123,6 +123,8 @@ function imagePart(block) {
|
|
|
123
123
|
*/
|
|
124
124
|
export function toChatCompletions(req, opts, drop = dropped()) {
|
|
125
125
|
const out = { model: req.model, stream: opts.stream };
|
|
126
|
+
if (opts.cacheKey)
|
|
127
|
+
out.prompt_cache_key = opts.cacheKey;
|
|
126
128
|
const messages = [];
|
|
127
129
|
const system = textOf(req.system);
|
|
128
130
|
if (system)
|
|
@@ -285,6 +287,8 @@ export function toChatCompletions(req, opts, drop = dropped()) {
|
|
|
285
287
|
*/
|
|
286
288
|
export function toResponses(req, opts, drop = dropped()) {
|
|
287
289
|
const out = { model: req.model, stream: opts.stream, store: false };
|
|
290
|
+
if (opts.cacheKey)
|
|
291
|
+
out.prompt_cache_key = opts.cacheKey;
|
|
288
292
|
const system = textOf(req.system);
|
|
289
293
|
if (system)
|
|
290
294
|
out.instructions = system;
|
|
@@ -904,8 +908,8 @@ export async function startTranslator(spec) {
|
|
|
904
908
|
const drop = dropped();
|
|
905
909
|
const responses = spec.wire === "responses";
|
|
906
910
|
const outbound = responses
|
|
907
|
-
? toResponses(body, { reasoningEffort: spec.reasoningEffort, params: spec.params, stream }, drop)
|
|
908
|
-
: toChatCompletions(body, { maxTokensParam: spec.maxTokensParam, reasoningEffort: spec.reasoningEffort, params: spec.params, stream }, drop);
|
|
911
|
+
? toResponses(body, { reasoningEffort: spec.reasoningEffort, params: spec.params, cacheKey: spec.cacheKey, stream }, drop)
|
|
912
|
+
: toChatCompletions(body, { maxTokensParam: spec.maxTokensParam, reasoningEffort: spec.reasoningEffort, params: spec.params, cacheKey: spec.cacheKey, stream }, drop);
|
|
909
913
|
for (const line of drop.lines())
|
|
910
914
|
if (!log.includes(line))
|
|
911
915
|
log.push(line);
|
|
@@ -1051,6 +1055,7 @@ export function translatorSpecFor(spec) {
|
|
|
1051
1055
|
params: spec.params,
|
|
1052
1056
|
maxTokensParam: spec.maxTokensParam,
|
|
1053
1057
|
reasoningEffort: spec.reasoningEffort,
|
|
1058
|
+
...(spec.cacheKey ? { cacheKey: spec.cacheKey } : {}),
|
|
1054
1059
|
label: spec.name ? `${spec.name} (${spec.baseUrl})` : spec.baseUrl,
|
|
1055
1060
|
};
|
|
1056
1061
|
}
|