@foldrun/core 1.4.2 → 2.1.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/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/dist/src/catalog.d.ts +7 -0
- package/dist/src/catalog.js +16 -1
- package/dist/src/deploy.d.ts +6 -27
- package/dist/src/deploy.js +14 -56
- package/dist/src/history.d.ts +30 -10
- package/dist/src/history.js +232 -99
- package/dist/src/library.d.ts +0 -13
- package/dist/src/library.js +0 -50
- package/dist/src/model-credential.js +1 -1
- package/dist/src/model-env.js +1 -1
- package/dist/src/platform.d.ts +1 -3
- package/dist/src/platform.js +0 -1
- package/dist/src/run-container.d.ts +31 -5
- package/dist/src/run-container.js +71 -3
- package/dist/src/runner.d.ts +2 -0
- package/dist/src/runner.js +20 -12
- package/dist/src/secrets.js +1 -12
- package/dist/src/starter.js +2 -2
- package/dist/src/step-exec.d.ts +23 -5
- package/dist/src/step-exec.js +30 -23
- package/dist/src/store.d.ts +61 -22
- package/dist/src/store.js +105 -25
- package/dist/src/tool-loop.d.ts +4 -1
- package/dist/src/tool-loop.js +2 -0
- package/dist/src/translator.d.ts +12 -0
- package/dist/src/translator.js +36 -13
- package/index.ts +0 -3
- package/package.json +1 -3
- package/src/catalog.ts +18 -2
- package/src/deploy.ts +13 -74
- package/src/history.ts +230 -108
- package/src/library.ts +0 -44
- package/src/model-credential.ts +1 -1
- package/src/model-env.ts +1 -1
- package/src/platform.ts +1 -4
- package/src/run-container.ts +78 -5
- package/src/runner.ts +22 -12
- package/src/secrets.ts +1 -11
- package/src/starter.ts +2 -2
- package/src/step-exec.ts +44 -27
- package/src/store.ts +145 -37
- package/src/tool-loop.ts +6 -1
- package/src/translator.ts +39 -15
- package/dist/src/git-hook.d.ts +0 -1
- package/dist/src/git-hook.js +0 -70
- package/dist/src/git.d.ts +0 -37
- package/dist/src/git.js +0 -101
- package/dist/src/gitrepo.d.ts +0 -113
- package/dist/src/gitrepo.js +0 -404
- package/dist/src/tar.d.ts +0 -30
- package/dist/src/tar.js +0 -112
- package/src/git-hook.ts +0 -73
- package/src/git.ts +0 -117
- package/src/gitrepo.ts +0 -439
- package/src/tar.ts +0 -125
package/dist/src/store.js
CHANGED
|
@@ -385,7 +385,6 @@ meta = {}) {
|
|
|
385
385
|
// about it. Before this, every deploy silently erased everything the agents
|
|
386
386
|
// had learned.
|
|
387
387
|
const shipped = new Set(files.map((f) => path.normalize(f.path)));
|
|
388
|
-
const isAgentOwned = (rel) => keptOnDeploy(rel, shipped);
|
|
389
388
|
// What is there now, for the revision: every editable file's content
|
|
390
389
|
// before this save replaces the tree.
|
|
391
390
|
const before = new Map();
|
|
@@ -399,6 +398,7 @@ meta = {}) {
|
|
|
399
398
|
}
|
|
400
399
|
}
|
|
401
400
|
}
|
|
401
|
+
const isAgentOwned = (rel) => keptOnDeploy(rel, shipped) || (meta.keepUntracked === true && !before.has(rel) && !shipped.has(rel));
|
|
402
402
|
const snapshot = fs.existsSync(dir) ? fs.mkdtempSync(path.join(dataRoot(), ".keep-")) : null;
|
|
403
403
|
const preserved = [];
|
|
404
404
|
if (snapshot) {
|
|
@@ -406,11 +406,15 @@ meta = {}) {
|
|
|
406
406
|
const rel = String(entry).split(path.sep).join("/");
|
|
407
407
|
const from = path.join(dir, rel);
|
|
408
408
|
try {
|
|
409
|
-
|
|
409
|
+
// lstat, never stat: a link is never followed. An agent folder's link
|
|
410
|
+
// to a file outside the workspace was copied in as that file's
|
|
411
|
+
// contents — on a restore with keepUntracked, any such link
|
|
412
|
+
// (Codex, 18 Oct 2026).
|
|
413
|
+
if (!fs.lstatSync(from).isFile() || !isAgentOwned(rel))
|
|
410
414
|
continue;
|
|
411
415
|
}
|
|
412
416
|
catch {
|
|
413
|
-
continue; // a
|
|
417
|
+
continue; // a file that vanished mid-deploy
|
|
414
418
|
}
|
|
415
419
|
const to = path.join(snapshot, rel);
|
|
416
420
|
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
@@ -1681,7 +1685,7 @@ export function mayApprove(flow, who) {
|
|
|
1681
1685
|
}
|
|
1682
1686
|
/** A live run of this flow — the fact overlap: decisions are made on. */
|
|
1683
1687
|
export function flowHasLiveRun(tenant, workspace, flowName) {
|
|
1684
|
-
return
|
|
1688
|
+
return listRunSummaries(tenant, workspace).some((r) => r.flow === flowName && (r.status === "running" || r.status === "queued" || r.status === "awaiting-approval"));
|
|
1685
1689
|
}
|
|
1686
1690
|
// Split a flow file into its frontmatter, any prose before the first step,
|
|
1687
1691
|
// and one block per step. Blocks come back in the same order as parseFlow's
|
|
@@ -2012,13 +2016,6 @@ export function listWorkspaces(tenant) {
|
|
|
2012
2016
|
}
|
|
2013
2017
|
}
|
|
2014
2018
|
const runsDir = path.join(pDir, "runs");
|
|
2015
|
-
let preview;
|
|
2016
|
-
try {
|
|
2017
|
-
preview = JSON.parse(fs.readFileSync(path.join(dir, `.${name}.preview.json`), "utf8"));
|
|
2018
|
-
}
|
|
2019
|
-
catch {
|
|
2020
|
-
// not a preview
|
|
2021
|
-
}
|
|
2022
2019
|
return {
|
|
2023
2020
|
name,
|
|
2024
2021
|
description,
|
|
@@ -2026,7 +2023,6 @@ export function listWorkspaces(tenant) {
|
|
|
2026
2023
|
flows: listFlows(tenant, name).length,
|
|
2027
2024
|
deployedAt: fs.statSync(pDir).mtime.toISOString(),
|
|
2028
2025
|
runCount: fs.existsSync(runsDir) ? fs.readdirSync(runsDir).filter((f) => f.endsWith(".json") && !f.startsWith(".")).length : 0,
|
|
2029
|
-
...(preview ? { preview } : {}),
|
|
2030
2026
|
};
|
|
2031
2027
|
})
|
|
2032
2028
|
.sort((a, b) => b.deployedAt.localeCompare(a.deployedAt));
|
|
@@ -2581,10 +2577,62 @@ export function sumAttempts(tries) {
|
|
|
2581
2577
|
const secs = tries.map((t) => t.computeSecs).filter((c) => typeof c === "number");
|
|
2582
2578
|
return {
|
|
2583
2579
|
costUsd: costs.length ? costs.reduce((a, b) => a + b, 0) : null,
|
|
2584
|
-
tokens: toks.length ? toks
|
|
2580
|
+
tokens: toks.length ? sumTokens(toks) : null,
|
|
2585
2581
|
computeSecs: secs.length ? secs.reduce((a, b) => a + b, 0) : null,
|
|
2586
2582
|
};
|
|
2587
2583
|
}
|
|
2584
|
+
/** The record's shape from what a step reports (ExecOutcome.usage). */
|
|
2585
|
+
export function tokensOf(usage) {
|
|
2586
|
+
if (!usage)
|
|
2587
|
+
return null;
|
|
2588
|
+
return {
|
|
2589
|
+
input: usage.inputTokens,
|
|
2590
|
+
output: usage.outputTokens,
|
|
2591
|
+
cacheRead: usage.cacheReadTokens ?? 0,
|
|
2592
|
+
cacheWrite: usage.cacheWriteTokens ?? 0,
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
/** Field-by-field sum; the cache fields are present in the result whenever
|
|
2596
|
+
* any input had them, so a run of old and new steps still reads whole. */
|
|
2597
|
+
export function sumTokens(list) {
|
|
2598
|
+
const out = { input: 0, output: 0 };
|
|
2599
|
+
for (const t of list) {
|
|
2600
|
+
out.input += t.input;
|
|
2601
|
+
out.output += t.output;
|
|
2602
|
+
if (t.cacheRead !== undefined || t.cacheWrite !== undefined) {
|
|
2603
|
+
out.cacheRead = (out.cacheRead ?? 0) + (t.cacheRead ?? 0);
|
|
2604
|
+
out.cacheWrite = (out.cacheWrite ?? 0) + (t.cacheWrite ?? 0);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
return out;
|
|
2608
|
+
}
|
|
2609
|
+
/** The share of all input that the prompt cache served, 0–1; null when
|
|
2610
|
+
* there was no input, or the record predates the split and cannot say. */
|
|
2611
|
+
export function cacheHitRate(t) {
|
|
2612
|
+
if (!t || t.cacheRead === undefined)
|
|
2613
|
+
return null;
|
|
2614
|
+
return t.input > 0 ? Math.min(1, t.cacheRead / t.input) : null;
|
|
2615
|
+
}
|
|
2616
|
+
/** The cache share over several counts, taking only those that report the
|
|
2617
|
+
* split: a step recorded before it holds its cache traffic inside `input`,
|
|
2618
|
+
* and counting that as uncached would understate the rate. The rule the
|
|
2619
|
+
* Usage page and the CLI's totals follow too. */
|
|
2620
|
+
export function measuredHitRate(list) {
|
|
2621
|
+
let read = 0;
|
|
2622
|
+
let all = 0;
|
|
2623
|
+
for (const t of list) {
|
|
2624
|
+
if (!t || t.cacheRead === undefined)
|
|
2625
|
+
continue;
|
|
2626
|
+
read += t.cacheRead;
|
|
2627
|
+
all += t.input;
|
|
2628
|
+
}
|
|
2629
|
+
return all > 0 ? read / all : null;
|
|
2630
|
+
}
|
|
2631
|
+
/** A run's token counts, summed over its steps; null when no step reported any. */
|
|
2632
|
+
export function runTokens(run) {
|
|
2633
|
+
const toks = run.steps.map((s) => s.tokens).filter((t) => !!t);
|
|
2634
|
+
return toks.length ? sumTokens(toks) : null;
|
|
2635
|
+
}
|
|
2588
2636
|
export function runFilePath(tenant, workspace, runId) {
|
|
2589
2637
|
assertSafeName(runId, "run id");
|
|
2590
2638
|
return path.join(workspaceDir(tenant, workspace), "runs", `${runId}.json`);
|
|
@@ -2697,6 +2745,24 @@ export function writeRun(tenant, workspace, run) {
|
|
|
2697
2745
|
fs.renameSync(tmp, p);
|
|
2698
2746
|
noteRunWritten(path.dirname(p), run, p);
|
|
2699
2747
|
}
|
|
2748
|
+
/**
|
|
2749
|
+
* The workspace's live runs — queued, running, or parked at a gate — read
|
|
2750
|
+
* whole, newest first. Off the run index: only the live records are opened,
|
|
2751
|
+
* where listRuns parses every record the workspace ever kept. What the
|
|
2752
|
+
* periodic sweeps want (reconcile, the watchdog), and on a box that keeps
|
|
2753
|
+
* ninety days of runs that is a handful of files instead of thousands.
|
|
2754
|
+
*/
|
|
2755
|
+
export function liveRuns(tenant, workspace) {
|
|
2756
|
+
const out = [];
|
|
2757
|
+
for (const s of listRunSummaries(tenant, workspace)) {
|
|
2758
|
+
if (!LIVE_STATUSES.has(s.status))
|
|
2759
|
+
continue;
|
|
2760
|
+
const run = readRun(tenant, workspace, s.id);
|
|
2761
|
+
if (run)
|
|
2762
|
+
out.push(run);
|
|
2763
|
+
}
|
|
2764
|
+
return out;
|
|
2765
|
+
}
|
|
2700
2766
|
/**
|
|
2701
2767
|
* Every run in the account, newest first, off the per-workspace index.
|
|
2702
2768
|
*
|
|
@@ -2721,6 +2787,13 @@ export function listAllRuns(tenant) {
|
|
|
2721
2787
|
}))
|
|
2722
2788
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
2723
2789
|
}
|
|
2790
|
+
/**
|
|
2791
|
+
* Which version of a file this is. mtime alone missed a rewrite inside one
|
|
2792
|
+
* mtime tick (coarse on some NFS servers), leaving a stale row that the
|
|
2793
|
+
* sweeps act on; every write here is a rename to a new file, so the inode
|
|
2794
|
+
* moves on each write whatever the clock says, and the size usually does.
|
|
2795
|
+
*/
|
|
2796
|
+
const fileStamp = (st) => `${st.mtimeMs}:${st.ino}:${st.size}`;
|
|
2724
2797
|
const RUN_INDEX = ".index.json";
|
|
2725
2798
|
const LIVE_STATUSES = new Set(["queued", "running", "awaiting-approval"]);
|
|
2726
2799
|
/** One run's row. */
|
|
@@ -2780,22 +2853,22 @@ export function runFromSummary(s) {
|
|
|
2780
2853
|
const runIndexCache = new Map();
|
|
2781
2854
|
function readRunIndex(dir) {
|
|
2782
2855
|
const file = path.join(dir, RUN_INDEX);
|
|
2783
|
-
let
|
|
2856
|
+
let stamp;
|
|
2784
2857
|
try {
|
|
2785
|
-
|
|
2858
|
+
stamp = fileStamp(fs.statSync(file));
|
|
2786
2859
|
}
|
|
2787
2860
|
catch {
|
|
2788
2861
|
runIndexCache.delete(dir);
|
|
2789
2862
|
return null;
|
|
2790
2863
|
}
|
|
2791
2864
|
const cached = runIndexCache.get(dir);
|
|
2792
|
-
if (cached && cached.
|
|
2865
|
+
if (cached && cached.stamp === stamp)
|
|
2793
2866
|
return cached.index;
|
|
2794
2867
|
try {
|
|
2795
2868
|
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2796
2869
|
if (parsed?.v !== 1 || typeof parsed.runs !== "object" || !parsed.runs)
|
|
2797
2870
|
return null;
|
|
2798
|
-
runIndexCache.set(dir, { index: parsed,
|
|
2871
|
+
runIndexCache.set(dir, { index: parsed, stamp });
|
|
2799
2872
|
return parsed;
|
|
2800
2873
|
}
|
|
2801
2874
|
catch {
|
|
@@ -2808,13 +2881,18 @@ function writeRunIndex(dir, index) {
|
|
|
2808
2881
|
fs.writeFileSync(tmp, JSON.stringify(index));
|
|
2809
2882
|
fs.renameSync(tmp, file);
|
|
2810
2883
|
try {
|
|
2811
|
-
runIndexCache.set(dir, { index,
|
|
2884
|
+
runIndexCache.set(dir, { index, stamp: fileStamp(fs.statSync(file)) });
|
|
2812
2885
|
}
|
|
2813
2886
|
catch {
|
|
2814
2887
|
runIndexCache.delete(dir);
|
|
2815
2888
|
}
|
|
2816
2889
|
}
|
|
2817
|
-
const sameRow = (a, b) =>
|
|
2890
|
+
const sameRow = (a, b) => {
|
|
2891
|
+
if (!a)
|
|
2892
|
+
return false;
|
|
2893
|
+
const { mtimeMs: _m, stamp: _s, ...row } = a;
|
|
2894
|
+
return JSON.stringify(row) === JSON.stringify(b);
|
|
2895
|
+
};
|
|
2818
2896
|
/** writeRun just saved `run` at `file`: bring its row up to date. The file
|
|
2819
2897
|
* is rewritten only when the row's content moved — a live step appends an
|
|
2820
2898
|
* event several times a second, and the row does not change for that;
|
|
@@ -2828,7 +2906,8 @@ function noteRunWritten(dir, run, file) {
|
|
|
2828
2906
|
const row = summarizeRun(run);
|
|
2829
2907
|
if (sameRow(index.runs[run.id], row))
|
|
2830
2908
|
return;
|
|
2831
|
-
|
|
2909
|
+
const st = fs.statSync(file);
|
|
2910
|
+
index.runs[run.id] = { ...row, mtimeMs: st.mtimeMs, stamp: fileStamp(st) };
|
|
2832
2911
|
writeRunIndex(dir, index);
|
|
2833
2912
|
}
|
|
2834
2913
|
catch {
|
|
@@ -2866,19 +2945,20 @@ export function listRunSummaries(tenant, workspace) {
|
|
|
2866
2945
|
continue;
|
|
2867
2946
|
const id = f.slice(0, -".json".length);
|
|
2868
2947
|
present.add(id);
|
|
2869
|
-
let
|
|
2948
|
+
let st;
|
|
2870
2949
|
try {
|
|
2871
|
-
|
|
2950
|
+
st = fs.statSync(path.join(dir, f));
|
|
2872
2951
|
}
|
|
2873
2952
|
catch {
|
|
2874
2953
|
continue; // pruned between the listing and the stat
|
|
2875
2954
|
}
|
|
2955
|
+
const stamp = fileStamp(st);
|
|
2876
2956
|
const row = index.runs[id];
|
|
2877
|
-
if (row && row.
|
|
2957
|
+
if (row && row.stamp === stamp)
|
|
2878
2958
|
continue;
|
|
2879
2959
|
try {
|
|
2880
2960
|
const run = JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"));
|
|
2881
|
-
index.runs[id] = { ...summarizeRun(run), mtimeMs };
|
|
2961
|
+
index.runs[id] = { ...summarizeRun(run), mtimeMs: st.mtimeMs, stamp };
|
|
2882
2962
|
changed = true;
|
|
2883
2963
|
}
|
|
2884
2964
|
catch {
|
|
@@ -2900,7 +2980,7 @@ export function listRunSummaries(tenant, workspace) {
|
|
|
2900
2980
|
}
|
|
2901
2981
|
}
|
|
2902
2982
|
return Object.values(index.runs)
|
|
2903
|
-
.map(({ mtimeMs: _m, ...row }) => row)
|
|
2983
|
+
.map(({ mtimeMs: _m, stamp: _s, ...row }) => row)
|
|
2904
2984
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
2905
2985
|
}
|
|
2906
2986
|
/** Every workspace's runs as summaries, tagged with the workspace. */
|
package/dist/src/tool-loop.d.ts
CHANGED
|
@@ -13,9 +13,12 @@ export interface LoopTool {
|
|
|
13
13
|
run: (args: Record<string, string | undefined>) => Promise<string> | string;
|
|
14
14
|
}
|
|
15
15
|
export interface LoopUsage {
|
|
16
|
-
/** Everything sent, cache reads and writes included
|
|
16
|
+
/** Everything sent, cache reads and writes included; the two cache
|
|
17
|
+
* fields say how much of it the prompt cache served or stored. */
|
|
17
18
|
inputTokens: number;
|
|
18
19
|
outputTokens: number;
|
|
20
|
+
cacheReadTokens: number;
|
|
21
|
+
cacheWriteTokens: number;
|
|
19
22
|
costUsd: number;
|
|
20
23
|
}
|
|
21
24
|
export interface LoopResult {
|
package/dist/src/tool-loop.js
CHANGED
|
@@ -170,6 +170,8 @@ export async function runToolLoop(o) {
|
|
|
170
170
|
usage = {
|
|
171
171
|
inputTokens: models.reduce((n, m) => n + (m.inputTokens ?? 0) + (m.cacheReadInputTokens ?? 0) + (m.cacheCreationInputTokens ?? 0), 0),
|
|
172
172
|
outputTokens: models.reduce((n, m) => n + (m.outputTokens ?? 0), 0),
|
|
173
|
+
cacheReadTokens: models.reduce((n, m) => n + (m.cacheReadInputTokens ?? 0), 0),
|
|
174
|
+
cacheWriteTokens: models.reduce((n, m) => n + (m.cacheCreationInputTokens ?? 0), 0),
|
|
173
175
|
costUsd,
|
|
174
176
|
};
|
|
175
177
|
if (message.subtype === "success") {
|
package/dist/src/translator.d.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
type Json = Record<string, unknown>;
|
|
2
|
+
/**
|
|
3
|
+
* An OpenAI-shaped usage object as Anthropic's: fresh input, cached input,
|
|
4
|
+
* output. Both OpenAI shapes count the cached part INSIDE the prompt total
|
|
5
|
+
* (Chat's prompt_tokens_details.cached_tokens, Responses'
|
|
6
|
+
* input_tokens_details.cached_tokens; DeepSeek says prompt_cache_hit_tokens),
|
|
7
|
+
* so it is taken out of input here or the step would count it twice.
|
|
8
|
+
*/
|
|
9
|
+
export declare function anthropicUsage(u: Json | undefined, wire: TranslatorWire): {
|
|
10
|
+
input: number;
|
|
11
|
+
cached: number;
|
|
12
|
+
output: number;
|
|
13
|
+
};
|
|
2
14
|
export type TranslatorWire = "chat" | "responses";
|
|
3
15
|
export interface TranslatorSpec {
|
|
4
16
|
/** The provider's base, e.g. https://api.openai.com/v1 — `/chat/completions`
|
package/dist/src/translator.js
CHANGED
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
// and their results, streaming — and drops what a Chat-Completions endpoint
|
|
23
23
|
// has no word for (cache_control, top_k, server-side tools, thinking on
|
|
24
24
|
// endpoints that lack reasoning_effort). Anything dropped is said once in
|
|
25
|
-
// the run trace.
|
|
25
|
+
// the run trace. Those endpoints cache prompt prefixes on their own side;
|
|
26
|
+
// the cached share they report comes back as cache_read_input_tokens, so a
|
|
27
|
+
// step's record says how much of its context was re-read. The translation is pure functions over JSON so the tests
|
|
26
28
|
// can drive every mapping without a network; the server around them is a
|
|
27
29
|
// few dozen lines.
|
|
28
30
|
import http from "node:http";
|
|
@@ -31,6 +33,33 @@ import { PROTECTED_PARAMS } from "./providers.js";
|
|
|
31
33
|
import { viaEgress } from "./egress.js";
|
|
32
34
|
import { trimSlashes } from "./paths.js";
|
|
33
35
|
import { captureRefusalHeaders, isRefusalStatus, refusalLine } from "./refusal.js";
|
|
36
|
+
/**
|
|
37
|
+
* An OpenAI-shaped usage object as Anthropic's: fresh input, cached input,
|
|
38
|
+
* output. Both OpenAI shapes count the cached part INSIDE the prompt total
|
|
39
|
+
* (Chat's prompt_tokens_details.cached_tokens, Responses'
|
|
40
|
+
* input_tokens_details.cached_tokens; DeepSeek says prompt_cache_hit_tokens),
|
|
41
|
+
* so it is taken out of input here or the step would count it twice.
|
|
42
|
+
*/
|
|
43
|
+
export function anthropicUsage(u, wire) {
|
|
44
|
+
if (!u)
|
|
45
|
+
return { input: 0, cached: 0, output: 0 };
|
|
46
|
+
const total = Number((wire === "chat" ? u.prompt_tokens : u.input_tokens) ?? 0);
|
|
47
|
+
const details = (wire === "chat" ? u.prompt_tokens_details : u.input_tokens_details);
|
|
48
|
+
const raw = Number(details?.cached_tokens ?? u.prompt_cache_hit_tokens ?? 0);
|
|
49
|
+
const cached = Number.isFinite(raw) ? Math.max(0, Math.min(raw, total)) : 0;
|
|
50
|
+
return {
|
|
51
|
+
input: total - cached,
|
|
52
|
+
cached,
|
|
53
|
+
output: Number((wire === "chat" ? u.completion_tokens : u.output_tokens) ?? 0),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function usageBlock(u) {
|
|
57
|
+
return {
|
|
58
|
+
input_tokens: u?.input ?? 0,
|
|
59
|
+
output_tokens: u?.output ?? 0,
|
|
60
|
+
...(u && u.cached > 0 ? { cache_read_input_tokens: u.cached } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
34
63
|
function dropped() {
|
|
35
64
|
const seen = new Set();
|
|
36
65
|
return {
|
|
@@ -466,10 +495,7 @@ export function fromChatCompletion(res, requestedModel, stopSequences) {
|
|
|
466
495
|
content,
|
|
467
496
|
stop_reason: stop.reason,
|
|
468
497
|
stop_sequence: stop.sequence,
|
|
469
|
-
usage:
|
|
470
|
-
input_tokens: Number(usage.prompt_tokens ?? 0),
|
|
471
|
-
output_tokens: Number(usage.completion_tokens ?? 0),
|
|
472
|
-
},
|
|
498
|
+
usage: usageBlock(anthropicUsage(usage, "chat")),
|
|
473
499
|
};
|
|
474
500
|
}
|
|
475
501
|
/**
|
|
@@ -532,7 +558,7 @@ export class StreamTranslator {
|
|
|
532
558
|
const out = this.start(chunk);
|
|
533
559
|
const u = chunk.usage;
|
|
534
560
|
if (u && (u.prompt_tokens !== undefined || u.completion_tokens !== undefined)) {
|
|
535
|
-
this.usage =
|
|
561
|
+
this.usage = anthropicUsage(u, "chat");
|
|
536
562
|
}
|
|
537
563
|
const choice = (chunk.choices ?? [])[0];
|
|
538
564
|
if (!choice)
|
|
@@ -609,7 +635,7 @@ export class StreamTranslator {
|
|
|
609
635
|
out.push(this.event("message_delta", {
|
|
610
636
|
type: "message_delta",
|
|
611
637
|
delta: { stop_reason: stop.reason, stop_sequence: stop.sequence },
|
|
612
|
-
usage:
|
|
638
|
+
usage: usageBlock(this.usage),
|
|
613
639
|
}));
|
|
614
640
|
out.push(this.event("message_stop", { type: "message_stop" }));
|
|
615
641
|
return out;
|
|
@@ -648,10 +674,7 @@ export function fromResponses(res, requestedModel) {
|
|
|
648
674
|
content,
|
|
649
675
|
stop_reason: stop,
|
|
650
676
|
stop_sequence: null,
|
|
651
|
-
usage:
|
|
652
|
-
input_tokens: Number(usage.input_tokens ?? 0),
|
|
653
|
-
output_tokens: Number(usage.output_tokens ?? 0),
|
|
654
|
-
},
|
|
677
|
+
usage: usageBlock(anthropicUsage(usage, "responses")),
|
|
655
678
|
};
|
|
656
679
|
}
|
|
657
680
|
/**
|
|
@@ -768,7 +791,7 @@ export class ResponsesStreamTranslator {
|
|
|
768
791
|
const r = ev.response ?? {};
|
|
769
792
|
const u = r.usage;
|
|
770
793
|
if (u)
|
|
771
|
-
this.usage =
|
|
794
|
+
this.usage = anthropicUsage(u, "responses");
|
|
772
795
|
const reason = r.incomplete_details?.reason;
|
|
773
796
|
if (typeof reason === "string")
|
|
774
797
|
this.incomplete = reason;
|
|
@@ -790,7 +813,7 @@ export class ResponsesStreamTranslator {
|
|
|
790
813
|
out.push(this.event("message_delta", {
|
|
791
814
|
type: "message_delta",
|
|
792
815
|
delta: { stop_reason: reason, stop_sequence: null },
|
|
793
|
-
usage:
|
|
816
|
+
usage: usageBlock(this.usage),
|
|
794
817
|
}));
|
|
795
818
|
out.push(this.event("message_stop", { type: "message_stop" }));
|
|
796
819
|
return out;
|
package/index.ts
CHANGED
|
@@ -36,7 +36,6 @@ export * from "./src/tool-names.ts";
|
|
|
36
36
|
export * from "./src/history.ts";
|
|
37
37
|
export * from "./src/zip.ts";
|
|
38
38
|
export * from "./src/package.ts";
|
|
39
|
-
export * from "./src/gitrepo.ts";
|
|
40
39
|
export * from "./src/storage.ts";
|
|
41
40
|
export * from "./src/secrets.ts";
|
|
42
41
|
export * from "./src/oauth-presets.ts";
|
|
@@ -47,5 +46,3 @@ export * from "./src/approvals.ts";
|
|
|
47
46
|
export * from "./src/deploy.ts";
|
|
48
47
|
export * from "./src/agent-import.ts";
|
|
49
48
|
export * from "./src/account.ts";
|
|
50
|
-
export * from "./src/tar.ts";
|
|
51
|
-
export * from "./src/git.ts";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foldrun/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Agents are just folders. The format, the runner, the checks.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"default": "./dist/index.js"
|
|
11
11
|
},
|
|
12
|
-
"./git-hook": "./dist/src/git-hook.js",
|
|
13
12
|
"./*": {
|
|
14
13
|
"types": "./dist/src/*.d.ts",
|
|
15
14
|
"default": "./dist/src/*.js"
|
|
@@ -18,7 +17,6 @@
|
|
|
18
17
|
"foldrun": {
|
|
19
18
|
"publicApi": [
|
|
20
19
|
".",
|
|
21
|
-
"./git-hook",
|
|
22
20
|
"./agents-md",
|
|
23
21
|
"./browser-pod",
|
|
24
22
|
"./flow-patterns",
|
package/src/catalog.ts
CHANGED
|
@@ -245,13 +245,29 @@ export function clampEffort(
|
|
|
245
245
|
export function catalogCost(
|
|
246
246
|
catalog: Catalog | null,
|
|
247
247
|
modelId: string,
|
|
248
|
-
usage: { inputTokens: number; outputTokens: number },
|
|
248
|
+
usage: { inputTokens: number; outputTokens: number; cacheReadTokens?: number; cacheWriteTokens?: number },
|
|
249
249
|
): number | null {
|
|
250
250
|
const m = findModel(catalog, modelId);
|
|
251
251
|
if (!m || m.promptPrice == null || m.completionPrice == null) return null;
|
|
252
|
-
|
|
252
|
+
// A catalogue quotes one input price; cache traffic is priced off it with
|
|
253
|
+
// the multipliers every gateway that bills for caching has copied from
|
|
254
|
+
// Anthropic. Before the counts were split (18 Oct 2026) every cached
|
|
255
|
+
// token was billed here at the full rate — a long cached step cost ten
|
|
256
|
+
// times its price on the ledger.
|
|
257
|
+
// inputTokens is all input; the cache fields are parts of it.
|
|
258
|
+
const read = usage.cacheReadTokens ?? 0;
|
|
259
|
+
const written = usage.cacheWriteTokens ?? 0;
|
|
260
|
+
const fresh = Math.max(0, usage.inputTokens - read - written);
|
|
261
|
+
const input = fresh + written * CACHE_WRITE_MULTIPLIER + read * CACHE_READ_MULTIPLIER;
|
|
262
|
+
return input * m.promptPrice + usage.outputTokens * m.completionPrice;
|
|
253
263
|
}
|
|
254
264
|
|
|
265
|
+
/** Anthropic's cache multipliers over the input price: a write costs a
|
|
266
|
+
* quarter more than fresh input, a read a tenth of it. One source for the
|
|
267
|
+
* mid-step ceiling (priceTurn) and the record's repricing (catalogCost). */
|
|
268
|
+
export const CACHE_WRITE_MULTIPLIER = 1.25;
|
|
269
|
+
export const CACHE_READ_MULTIPLIER = 0.1;
|
|
270
|
+
|
|
255
271
|
/** Catalogue entries as completion items: price per MTok, context, and the
|
|
256
272
|
* one capability that decides agent-worthiness. Tool-capable models first —
|
|
257
273
|
* on an agent platform they are almost always what the author wants. */
|
package/src/deploy.ts
CHANGED
|
@@ -25,7 +25,8 @@ import {
|
|
|
25
25
|
saveWorkspace,
|
|
26
26
|
keptOnDeploy,
|
|
27
27
|
workspaceDir,
|
|
28
|
-
|
|
28
|
+
listRunSummaries,
|
|
29
|
+
listWorkspaceFiles,
|
|
29
30
|
parseFlow,
|
|
30
31
|
parseToolDef,
|
|
31
32
|
WORKSPACE_DIRS,
|
|
@@ -44,7 +45,6 @@ import { networkProblems } from "./network.ts";
|
|
|
44
45
|
import { actionProblems } from "./web-actions.ts";
|
|
45
46
|
import { languageProblem } from "./language.ts";
|
|
46
47
|
import { localeProblems } from "./locale.ts";
|
|
47
|
-
import { repoDir } from "./gitrepo.ts";
|
|
48
48
|
import { entriesNoFollow } from "./paths.ts";
|
|
49
49
|
import { filterIgnored, findingFingerprint, ruleForWhere, scanFiles, type ScanFinding, type ScanOptions } from "./scan.ts";
|
|
50
50
|
|
|
@@ -455,13 +455,13 @@ export function deployIssues(files: DeployFile[]): DeployIssue[] {
|
|
|
455
455
|
*/
|
|
456
456
|
export function runsInFlight(tenant: string, workspace: string): string[] {
|
|
457
457
|
if (!fs.existsSync(workspaceDir(tenant, workspace))) return [];
|
|
458
|
-
return
|
|
458
|
+
return listRunSummaries(tenant, workspace)
|
|
459
459
|
.filter((r) => r.status === "queued" || r.status === "running" || r.status === "awaiting-approval")
|
|
460
460
|
.map((r) => r.id);
|
|
461
461
|
}
|
|
462
462
|
|
|
463
463
|
/** What a deploy would do, without doing it. */
|
|
464
|
-
export function planDeploy(tenant: string, workspace: string, files: DeployFile[]): DeployPlan {
|
|
464
|
+
export function planDeploy(tenant: string, workspace: string, files: DeployFile[], opts: { keepUntracked?: boolean } = {}): DeployPlan {
|
|
465
465
|
const dir = workspaceDir(tenant, workspace);
|
|
466
466
|
const shipped = new Map(files.map((f) => [path.normalize(f.path), f.content]));
|
|
467
467
|
|
|
@@ -499,7 +499,10 @@ export function planDeploy(tenant: string, workspace: string, files: DeployFile[
|
|
|
499
499
|
// memory, the platform's own files. Reporting these as removals would be
|
|
500
500
|
// a lie, and the alarming kind; one predicate serves both.
|
|
501
501
|
const shippedPaths = new Set(shipped.keys());
|
|
502
|
-
|
|
502
|
+
// keepUntracked (a restore): what is not editable source stays, as
|
|
503
|
+
// saveWorkspace keeps it — see its meta.keepUntracked.
|
|
504
|
+
const tracked = opts.keepUntracked ? new Set(listWorkspaceFiles(tenant, workspace)) : null;
|
|
505
|
+
const removed = [...present].filter((rel) => !shipped.has(rel) && !keptOnDeploy(rel, shippedPaths) && (!tracked || tracked.has(rel)));
|
|
503
506
|
|
|
504
507
|
return {
|
|
505
508
|
files,
|
|
@@ -529,6 +532,9 @@ export interface DeployOptions {
|
|
|
529
532
|
* no window between the check and the swap. Absent: no check, as before.
|
|
530
533
|
*/
|
|
531
534
|
expectRemoved?: string[];
|
|
535
|
+
/** Keep what is not editable source (planDeploy, saveWorkspace): a
|
|
536
|
+
* restore takes history's tree, and history tracks source only. */
|
|
537
|
+
keepUntracked?: boolean;
|
|
532
538
|
}
|
|
533
539
|
|
|
534
540
|
/**
|
|
@@ -539,73 +545,6 @@ export interface DeployOptions {
|
|
|
539
545
|
* webhook or a CLI that needs to report *why*, and every reason is already in
|
|
540
546
|
* the plan.
|
|
541
547
|
*/
|
|
542
|
-
/** What a push recorded about itself, where the Repository page reads it. */
|
|
543
|
-
export interface PushNote {
|
|
544
|
-
commit: string;
|
|
545
|
-
at: string;
|
|
546
|
-
applied: boolean;
|
|
547
|
-
issues?: unknown[];
|
|
548
|
-
blockedBy?: string[];
|
|
549
|
-
/** Set when a blocked push was applied later, by the retry. */
|
|
550
|
-
appliedAt?: string;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
export const pushNotePath = (tenant: string, workspace: string) =>
|
|
554
|
-
path.join(repoDir(tenant, workspace), "foldrun-last-push.json");
|
|
555
|
-
|
|
556
|
-
export function readPushNote(tenant: string, workspace: string): PushNote | null {
|
|
557
|
-
try {
|
|
558
|
-
return JSON.parse(fs.readFileSync(pushNotePath(tenant, workspace), "utf8")) as PushNote;
|
|
559
|
-
} catch {
|
|
560
|
-
return null;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
export function writePushNote(tenant: string, workspace: string, note: PushNote) {
|
|
565
|
-
try {
|
|
566
|
-
fs.writeFileSync(pushNotePath(tenant, workspace), JSON.stringify(note));
|
|
567
|
-
} catch {
|
|
568
|
-
// The note is a record, not the deploy. Losing it must not fail a push.
|
|
569
|
-
}
|
|
570
|
-
// A push waiting on a run is the scheduler's to apply; tell it there is one.
|
|
571
|
-
notifyWorkspaceChanged(tenant, workspace, "push");
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* A push whose deploy was refused only because a run was in flight is not a
|
|
576
|
-
* failure — it is a deploy that has not happened yet. Git already said the
|
|
577
|
-
* push succeeded (the deploy happens after the client is gone), so nothing
|
|
578
|
-
* would ever tell anyone the workspace on disk is behind `main`. This applies
|
|
579
|
-
* it once the runs are done.
|
|
580
|
-
*
|
|
581
|
-
* Only `blockedBy`: a push refused for `issues` is a broken workspace and
|
|
582
|
-
* wants a person, not a retry.
|
|
583
|
-
*/
|
|
584
|
-
export function applyPendingPush(
|
|
585
|
-
tenant: string,
|
|
586
|
-
workspace: string,
|
|
587
|
-
filesAt: (tenant: string, scope: string, ref: string) => DeployFile[],
|
|
588
|
-
): { commit: string } | null {
|
|
589
|
-
const note = readPushNote(tenant, workspace);
|
|
590
|
-
if (!note || note.applied) return null;
|
|
591
|
-
if (!note.blockedBy?.length || (note.issues?.length ?? 0) > 0) return null;
|
|
592
|
-
if (runsInFlight(tenant, workspace).length > 0) return null;
|
|
593
|
-
// Only the commit the note names, and only while it is still what main
|
|
594
|
-
// points at — a newer push supersedes it and wrote its own note.
|
|
595
|
-
const result = deployWorkspace(tenant, workspace, filesAt(tenant, workspace, note.commit), {
|
|
596
|
-
commit: note.commit,
|
|
597
|
-
force: false,
|
|
598
|
-
});
|
|
599
|
-
writePushNote(tenant, workspace, {
|
|
600
|
-
...note,
|
|
601
|
-
applied: result.applied,
|
|
602
|
-
issues: result.issues ?? [],
|
|
603
|
-
blockedBy: result.blockedBy ?? [],
|
|
604
|
-
...(result.applied ? { appliedAt: new Date().toISOString() } : {}),
|
|
605
|
-
});
|
|
606
|
-
return result.applied ? { commit: note.commit } : null;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
548
|
export function deployWorkspace(
|
|
610
549
|
tenant: string,
|
|
611
550
|
workspace: string,
|
|
@@ -615,7 +554,7 @@ export function deployWorkspace(
|
|
|
615
554
|
assertSafeName(tenant, "tenant");
|
|
616
555
|
assertSafeName(workspace, "workspace");
|
|
617
556
|
|
|
618
|
-
const plan = planDeploy(tenant, workspace, files);
|
|
557
|
+
const plan = planDeploy(tenant, workspace, files, { keepUntracked: opts.keepUntracked });
|
|
619
558
|
const blocked = plan.blockedBy.length > 0 && !opts.force;
|
|
620
559
|
if (plan.issues.length > 0 || blocked) {
|
|
621
560
|
return { ...plan, applied: false, commit: null, preserved: 0 };
|
|
@@ -628,7 +567,7 @@ export function deployWorkspace(
|
|
|
628
567
|
}
|
|
629
568
|
}
|
|
630
569
|
|
|
631
|
-
const { preserved } = saveWorkspace(tenant, workspace, files, { commit: opts.commit ?? null, by: opts.by ?? "deploy", message: opts.message });
|
|
570
|
+
const { preserved } = saveWorkspace(tenant, workspace, files, { commit: opts.commit ?? null, by: opts.by ?? "deploy", message: opts.message, keepUntracked: opts.keepUntracked });
|
|
632
571
|
const commit = opts.commit ?? null;
|
|
633
572
|
if (commit) writeDeployedCommit(tenant, workspace, commit);
|
|
634
573
|
return { ...plan, applied: true, commit, preserved }; // saveWorkspace told the platform
|