@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.
- package/dist/src/agent-activity.d.ts +5 -0
- package/dist/src/agent-activity.js +31 -0
- package/dist/src/agent-import.js +1 -1
- package/dist/src/agent-tools.js +1 -1
- package/dist/src/agents-md.js +1 -1
- 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 +1 -0
- package/dist/src/deploy.d.ts +36 -1
- package/dist/src/deploy.js +170 -11
- package/dist/src/egress.d.ts +16 -1
- package/dist/src/evals.d.ts +5 -1
- package/dist/src/evals.js +26 -18
- package/dist/src/frontmatter-fields.js +9 -0
- package/dist/src/frontmatter-schema.js +3 -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/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 +8 -0
- package/dist/src/matter.js +28 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +40 -0
- package/dist/src/model-env.d.ts +14 -2
- package/dist/src/model-env.js +34 -9
- package/dist/src/network.d.ts +12 -0
- package/dist/src/network.js +25 -1
- 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 +1 -1
- 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 +1 -1
- package/dist/src/providers.d.ts +5 -0
- package/dist/src/providers.js +2 -2
- package/dist/src/run-container.d.ts +16 -2
- package/dist/src/run-container.js +57 -5
- package/dist/src/runner.d.ts +53 -2
- package/dist/src/runner.js +356 -70
- package/dist/src/runs-filter.d.ts +48 -0
- package/dist/src/runs-filter.js +73 -0
- package/dist/src/scan.d.ts +19 -1
- package/dist/src/scan.js +60 -7
- package/dist/src/starter.js +4 -2
- package/dist/src/step-exec.d.ts +29 -1
- package/dist/src/step-exec.js +106 -6
- package/dist/src/store.d.ts +130 -3
- package/dist/src/store.js +145 -6
- 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 +1 -1
- package/src/agent-tools.ts +1 -1
- package/src/agents-md.ts +1 -1
- 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 +1 -0
- package/src/deploy.ts +184 -10
- package/src/egress.ts +12 -2
- package/src/evals.ts +26 -18
- package/src/frontmatter-fields.ts +9 -0
- package/src/frontmatter-schema.ts +3 -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/library.ts +1 -1
- package/src/limits.ts +38 -5
- package/src/linkable.ts +1 -1
- package/src/matter.ts +33 -0
- package/src/model-credential.ts +45 -0
- package/src/model-env.ts +42 -10
- package/src/network.ts +25 -1
- 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 +1 -1
- package/src/platform.ts +1 -1
- package/src/prompt-layers.ts +90 -0
- package/src/provider-health.ts +1 -1
- package/src/providers.ts +7 -2
- package/src/run-container.ts +66 -7
- package/src/runner.ts +363 -46
- package/src/runs-filter.ts +88 -0
- package/src/scan.ts +68 -7
- package/src/starter.ts +4 -2
- package/src/step-exec.ts +147 -9
- package/src/store.ts +254 -8
- 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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Insights (FOL-72 step 8): the problems that recur without failing anything,
|
|
2
|
+
// found in the runs with no setup — each with its evidence, what it costs and
|
|
3
|
+
// what to do. The operator's "3 problems, here is why" instead of a list of
|
|
4
|
+
// runs. Pure: the API, the Observe page and `foldrun insights` read the same.
|
|
5
|
+
//
|
|
6
|
+
// cache an agent re-reads a big prompt uncached, step after step
|
|
7
|
+
// big-result a tool's result is large and every later model call sends it again
|
|
8
|
+
// retry-loop a tool errors again and again inside one step
|
|
9
|
+
// timeout steps that use most of their `timeout:` — the next bigger input fails
|
|
10
|
+
//
|
|
11
|
+
// Costs are estimates in credits from the steps' own token costs (a credit
|
|
12
|
+
// is a cent of USD), said as "about".
|
|
13
|
+
/** A cached input token costs about a tenth of a fresh one. */
|
|
14
|
+
const CACHE_SAVING = 0.9;
|
|
15
|
+
/** Characters to tokens, roughly, for text in a conversation. */
|
|
16
|
+
const CHARS_PER_TOKEN = 4;
|
|
17
|
+
const BIG_RESULT = 20_000;
|
|
18
|
+
const stepRefs = (hits) => [...hits].sort((a, b) => b.run.startedAt.localeCompare(a.run.startedAt)).slice(0, 5).map((h) => ({ workspace: h.run.workspace, runId: h.run.id, step: h.index + 1 }));
|
|
19
|
+
const credits = (usd) => Math.round(usd * 100);
|
|
20
|
+
export function findInsights(runs, opts = {}) {
|
|
21
|
+
const min = opts.minSteps ?? 3;
|
|
22
|
+
const steps = [];
|
|
23
|
+
for (const run of runs)
|
|
24
|
+
run.steps.forEach((s, index) => {
|
|
25
|
+
if (s.status === "completed" || s.status === "failed")
|
|
26
|
+
steps.push({ run, index, s });
|
|
27
|
+
});
|
|
28
|
+
const out = [];
|
|
29
|
+
const byAgent = new Map();
|
|
30
|
+
for (const x of steps) {
|
|
31
|
+
const k = `${x.run.workspace}\u0000${x.s.agent}`;
|
|
32
|
+
(byAgent.get(k) ?? byAgent.set(k, []).get(k)).push(x);
|
|
33
|
+
}
|
|
34
|
+
for (const [k, list] of byAgent) {
|
|
35
|
+
const [workspace, agent] = k.split("\u0000");
|
|
36
|
+
// cache: big inputs, little of it from the cache, on most of its steps.
|
|
37
|
+
const measured = list.filter((x) => x.s.tokens && x.s.tokens.cacheRead !== undefined && x.s.tokens.input >= 20_000);
|
|
38
|
+
const poor = measured.filter((x) => (x.s.tokens.cacheRead ?? 0) / x.s.tokens.input < 0.3);
|
|
39
|
+
if (poor.length >= min && poor.length >= measured.length / 2) {
|
|
40
|
+
const usd = poor.reduce((n, x) => {
|
|
41
|
+
const t = x.s.tokens;
|
|
42
|
+
const uncached = Math.max(0, t.input - (t.cacheRead ?? 0)) / Math.max(1, t.input);
|
|
43
|
+
return n + (x.s.costUsd ?? 0) * uncached * CACHE_SAVING * 0.5;
|
|
44
|
+
}, 0);
|
|
45
|
+
const rate = Math.round((100 * poor.reduce((n, x) => n + (x.s.tokens.cacheRead ?? 0), 0)) / Math.max(1, poor.reduce((n, x) => n + x.s.tokens.input, 0)));
|
|
46
|
+
out.push({
|
|
47
|
+
id: `cache/${workspace}/${agent}`,
|
|
48
|
+
kind: "cache",
|
|
49
|
+
workspace,
|
|
50
|
+
agent,
|
|
51
|
+
title: `${agent} reads its prompt mostly uncached (${rate}% from the cache)`,
|
|
52
|
+
why: "Each call sends a large prompt that the model's cache rarely serves: something near its start changes from call to call (a date, a run id, a file listing), or the steps are spaced further apart than the cache lasts.",
|
|
53
|
+
fix: "Keep what changes (dates, ids, the task) at the end of the agent's prompt and the large fixed part first; run its steps closer together.",
|
|
54
|
+
credits: credits(usd),
|
|
55
|
+
steps: poor.length,
|
|
56
|
+
evidence: stepRefs(poor),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// big-result: a tool's result over 20k characters with calls after it,
|
|
60
|
+
// each of which sends it again.
|
|
61
|
+
const big = new Map();
|
|
62
|
+
for (const x of list) {
|
|
63
|
+
const callsAfter = (at) => (x.s.calls ?? []).filter((c) => c.at > at).length;
|
|
64
|
+
const turns = x.s.calls?.length ?? x.s.turns ?? 0;
|
|
65
|
+
for (const e of x.s.events) {
|
|
66
|
+
if (e.type !== "tool" || e.ms == null || (e.bytes ?? 0) < BIG_RESULT)
|
|
67
|
+
continue;
|
|
68
|
+
const after = x.s.calls ? callsAfter(e.t) : Math.max(0, turns - 1);
|
|
69
|
+
if (after < 2)
|
|
70
|
+
continue;
|
|
71
|
+
const tokensResent = ((e.bytes ?? 0) / CHARS_PER_TOKEN) * after;
|
|
72
|
+
const input = Math.max(1, x.s.tokens?.input ?? 0);
|
|
73
|
+
const share = Math.min(1, tokensResent / input);
|
|
74
|
+
const b = big.get(e.text) ?? { hits: [], usd: 0, chars: 0 };
|
|
75
|
+
if (!b.hits.includes(x))
|
|
76
|
+
b.hits.push(x);
|
|
77
|
+
b.usd += (x.s.costUsd ?? 0) * share;
|
|
78
|
+
b.chars = Math.max(b.chars, e.bytes ?? 0);
|
|
79
|
+
big.set(e.text, b);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const [tool, b] of big) {
|
|
83
|
+
if (b.hits.length < min)
|
|
84
|
+
continue;
|
|
85
|
+
out.push({
|
|
86
|
+
id: `big-result/${workspace}/${agent}/${tool}`,
|
|
87
|
+
kind: "big-result",
|
|
88
|
+
workspace,
|
|
89
|
+
agent,
|
|
90
|
+
tool,
|
|
91
|
+
title: `${tool} hands ${agent} results up to ${Math.round(b.chars / 1000)}k characters, sent again on every later call`,
|
|
92
|
+
why: "A tool result stays in the conversation, so every model call after it pays for it again.",
|
|
93
|
+
fix: `Have ${tool} return less (filter, a summary, or write the bulk to a file and return the path), or split the step so the big read ends it.`,
|
|
94
|
+
credits: credits(b.usd),
|
|
95
|
+
steps: b.hits.length,
|
|
96
|
+
evidence: stepRefs(b.hits),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// retry-loop: one tool erroring three or more times in a step.
|
|
100
|
+
const loops = new Map();
|
|
101
|
+
for (const x of list) {
|
|
102
|
+
const errs = new Map();
|
|
103
|
+
for (const e of x.s.events)
|
|
104
|
+
if (e.type === "tool" && e.ms != null && e.err)
|
|
105
|
+
errs.set(e.text, (errs.get(e.text) ?? 0) + 1);
|
|
106
|
+
for (const [tool, n] of errs)
|
|
107
|
+
if (n >= 3)
|
|
108
|
+
(loops.get(tool) ?? loops.set(tool, []).get(tool)).push(x);
|
|
109
|
+
}
|
|
110
|
+
for (const [tool, hits] of loops) {
|
|
111
|
+
if (hits.length < Math.min(min, 2))
|
|
112
|
+
continue;
|
|
113
|
+
out.push({
|
|
114
|
+
id: `retry-loop/${workspace}/${agent}/${tool}`,
|
|
115
|
+
kind: "retry-loop",
|
|
116
|
+
workspace,
|
|
117
|
+
agent,
|
|
118
|
+
tool,
|
|
119
|
+
title: `${agent} keeps retrying ${tool} after it errors`,
|
|
120
|
+
why: "The tool fails the same way and the agent tries again instead of stopping — every attempt is a model call and a tool call.",
|
|
121
|
+
fix: `Read one of the errors (the trace's input / output): fix the tool or its input, or tell ${agent} in its prompt to stop after one failure and say BLOCKED with why.`,
|
|
122
|
+
credits: credits(hits.reduce((n, x) => n + (x.s.costUsd ?? 0) * 0.3, 0)),
|
|
123
|
+
steps: hits.length,
|
|
124
|
+
evidence: stepRefs(hits),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// timeout: steps that took 80% or more of their timeout.
|
|
128
|
+
const tight = list.filter((x) => {
|
|
129
|
+
if (!x.s.timeout || !x.s.startedAt || !x.s.finishedAt)
|
|
130
|
+
return false;
|
|
131
|
+
const secs = (Date.parse(x.s.finishedAt) - Date.parse(x.s.startedAt)) / 1000;
|
|
132
|
+
return secs >= 0.8 * x.s.timeout;
|
|
133
|
+
});
|
|
134
|
+
if (tight.length >= Math.min(min, 2)) {
|
|
135
|
+
const limit = tight[0].s.timeout;
|
|
136
|
+
out.push({
|
|
137
|
+
id: `timeout/${workspace}/${agent}`,
|
|
138
|
+
kind: "timeout",
|
|
139
|
+
workspace,
|
|
140
|
+
agent,
|
|
141
|
+
title: `${agent} uses most of its ${limit}s timeout`,
|
|
142
|
+
why: "Steps finish just inside their limit; a slightly bigger input or a slow site and they are cut off.",
|
|
143
|
+
fix: "Raise timeout: on the step, or give the agent less to do per step (each: to fan out, a smaller batch).",
|
|
144
|
+
credits: null,
|
|
145
|
+
steps: tight.length,
|
|
146
|
+
evidence: stepRefs(tight),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const order = { "retry-loop": 0, timeout: 1, "big-result": 2, cache: 3 };
|
|
151
|
+
return out.sort((a, b) => (b.credits ?? -1) - (a.credits ?? -1) || order[a.kind] - order[b.kind]);
|
|
152
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Outcome } from "./outcome.ts";
|
|
2
|
+
export type IssueKind = "platform" | "failed" | "blocked" | "bad" | "missed";
|
|
3
|
+
export type IssueStatus = "open" | "acknowledged" | "snoozed" | "resolved";
|
|
4
|
+
export type IssueAction = "acknowledge" | "snooze" | "resolve" | "reopen";
|
|
5
|
+
/** As much of a run as an issue needs (the overview's RunLite carries it). */
|
|
6
|
+
export interface IssueRun {
|
|
7
|
+
workspace: string;
|
|
8
|
+
id: string;
|
|
9
|
+
/** The flow's name, or "agent: x" / "eval: y". */
|
|
10
|
+
what: string;
|
|
11
|
+
outcome: Outcome;
|
|
12
|
+
startedAt: string;
|
|
13
|
+
finishedAt?: string | null;
|
|
14
|
+
failure?: {
|
|
15
|
+
agent: string;
|
|
16
|
+
reason: string;
|
|
17
|
+
} | null;
|
|
18
|
+
headline?: string | null;
|
|
19
|
+
/** The revision the run ran (RunRecord.revision), for "regressed since". */
|
|
20
|
+
revision?: string | null;
|
|
21
|
+
/** Not a run: a scheduled fire that started none (FOL-72 step 7). Its
|
|
22
|
+
* `startedAt` is when it was due; its id is `missed-<when>`. */
|
|
23
|
+
missed?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface IssueEvent {
|
|
26
|
+
at: string;
|
|
27
|
+
/** Who: a person's email, or a key's label. */
|
|
28
|
+
by: string;
|
|
29
|
+
action: IssueAction;
|
|
30
|
+
/** Snoozed until, ISO. */
|
|
31
|
+
until?: string;
|
|
32
|
+
/** Or snoozed until it happens this many more times. */
|
|
33
|
+
untilCount?: number;
|
|
34
|
+
note?: string;
|
|
35
|
+
}
|
|
36
|
+
/** What people did to one issue. Stored; the rest is derived. */
|
|
37
|
+
export interface IssueState {
|
|
38
|
+
history: IssueEvent[];
|
|
39
|
+
}
|
|
40
|
+
export interface Issue {
|
|
41
|
+
id: string;
|
|
42
|
+
kind: IssueKind;
|
|
43
|
+
status: IssueStatus;
|
|
44
|
+
/** The desk; for a platform issue, the newest desk it hit. */
|
|
45
|
+
workspace: string;
|
|
46
|
+
/** For a platform issue, every desk it hit. */
|
|
47
|
+
workspaces: string[];
|
|
48
|
+
/** The flow (or agent run); for a platform issue, what broke. */
|
|
49
|
+
what: string;
|
|
50
|
+
/** The agent the failure was at, for a failed issue. */
|
|
51
|
+
agent: string | null;
|
|
52
|
+
/** Why, from the newest run: its failure, or its summary line. */
|
|
53
|
+
reason: string;
|
|
54
|
+
/** What to do about it, in plain words. */
|
|
55
|
+
action: string;
|
|
56
|
+
/** Since it was last resolved (or ever): when the first and the latest of
|
|
57
|
+
* its runs went wrong (their end, else their start), how many. */
|
|
58
|
+
firstSeen: string;
|
|
59
|
+
lastSeen: string;
|
|
60
|
+
count: number;
|
|
61
|
+
/** The newest runs it covers, newest first, at most 20; a missed
|
|
62
|
+
* schedule's are the fires that started nothing (`missed`). */
|
|
63
|
+
runs: {
|
|
64
|
+
workspace: string;
|
|
65
|
+
id: string;
|
|
66
|
+
at: string;
|
|
67
|
+
missed?: boolean;
|
|
68
|
+
}[];
|
|
69
|
+
/** The newest run of the same flow went fine after the last occurrence. */
|
|
70
|
+
recoveredAt: string | null;
|
|
71
|
+
acknowledged: {
|
|
72
|
+
at: string;
|
|
73
|
+
by: string;
|
|
74
|
+
} | null;
|
|
75
|
+
snoozedUntil: string | null;
|
|
76
|
+
/** Snoozed until it happens this many more times: how many are left. */
|
|
77
|
+
snoozedFor: number | null;
|
|
78
|
+
/** It was resolved before and came back: this episode follows a resolve. */
|
|
79
|
+
regressed: boolean;
|
|
80
|
+
/** The revision the first run of this episode ran — "since abc1234". */
|
|
81
|
+
sinceRevision: string | null;
|
|
82
|
+
resolved: {
|
|
83
|
+
at: string;
|
|
84
|
+
by: string;
|
|
85
|
+
} | null;
|
|
86
|
+
history: IssueEvent[];
|
|
87
|
+
}
|
|
88
|
+
/** The model refusing the credential, as the runner sees it (runner.ts
|
|
89
|
+
* isAuthRefusal reads this one pattern, so a refusal the runner retries on
|
|
90
|
+
* is the same refusal an issue groups by — Codex review). */
|
|
91
|
+
export declare const AUTH_REFUSAL: RegExp;
|
|
92
|
+
/** A failure that is the platform's, not the desk's. */
|
|
93
|
+
export interface PlatformCause {
|
|
94
|
+
key: string;
|
|
95
|
+
/** What broke, in the words a person would use. */
|
|
96
|
+
label: string;
|
|
97
|
+
/** What to do about it. None of it is editing the desk. */
|
|
98
|
+
fix: string;
|
|
99
|
+
}
|
|
100
|
+
/** Whether a failure reason is the platform's, and which kind. */
|
|
101
|
+
export declare function platformCause(reason: string | null | undefined): PlatformCause | null;
|
|
102
|
+
/** Which issue a run belongs to, or null when it is not a problem. */
|
|
103
|
+
export declare function issueKeyOf(r: IssueRun): {
|
|
104
|
+
id: string;
|
|
105
|
+
kind: IssueKind;
|
|
106
|
+
agent: string | null;
|
|
107
|
+
cause: PlatformCause | null;
|
|
108
|
+
} | null;
|
|
109
|
+
/**
|
|
110
|
+
* Every issue the runs show, with what people did to them folded in.
|
|
111
|
+
* `states` is keyed by issue id. Resolved issues are kept (with their last
|
|
112
|
+
* occurrences) while their runs are still in the window, so their history
|
|
113
|
+
* can be read; a status filter is the caller's.
|
|
114
|
+
*/
|
|
115
|
+
export declare function foldIssues(runs: Iterable<IssueRun>, states: Record<string, IssueState>, now: number,
|
|
116
|
+
/** `autoClose(workspace, flow)`: the flow says `issues: auto-close`, so a
|
|
117
|
+
* run that goes fine after a failure closes it (FOL-72 step 7). Never a
|
|
118
|
+
* platform cause or a missed schedule — those stay until a person says. */
|
|
119
|
+
opts?: {
|
|
120
|
+
autoClose?: (workspace: string, what: string) => boolean;
|
|
121
|
+
}): Issue[];
|
|
122
|
+
/** An issue still asking for someone: open or acknowledged, not snoozed or resolved. */
|
|
123
|
+
export declare const isActiveIssue: (i: Issue) => boolean;
|
|
124
|
+
/**
|
|
125
|
+
* What the run doctor needs to look at an issue: its newest run, the
|
|
126
|
+
* workspace that run is in (the assistant works inside one), and the
|
|
127
|
+
* question the run page's Doctor button asks — so the issue's button, the
|
|
128
|
+
* CLI's `issues doctor` and the run page put it the same way. Null for a
|
|
129
|
+
* missed schedule, which has no run to read.
|
|
130
|
+
*/
|
|
131
|
+
export declare function issueDoctor(issue: Pick<Issue, "kind" | "runs">): {
|
|
132
|
+
workspace: string;
|
|
133
|
+
runId: string;
|
|
134
|
+
prompt: string;
|
|
135
|
+
} | null;
|
|
136
|
+
/** Parse "30m", "4h", "1d", "1w" into milliseconds; null if not one. */
|
|
137
|
+
export declare function snoozeMs(s: string): number | null;
|
|
138
|
+
/** One person's act on an issue, appended to its history. Refuses a snooze
|
|
139
|
+
* with no future `until`. Pure: the caller stores the result. */
|
|
140
|
+
export declare function applyIssueAction(state: IssueState | null, ev: IssueEvent, now: number): IssueState;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// Issues: what went wrong, kept open until a person closes it (FOL-54 step 3).
|
|
2
|
+
//
|
|
3
|
+
// The overview used to list "needs attention" from each flow's newest run in
|
|
4
|
+
// the last day, so a later success hid a failure and anything older than a
|
|
5
|
+
// day vanished on its own. An issue is the same thing going wrong again —
|
|
6
|
+
// one desk's flow failing at one agent, or ending BLOCKED or BAD, or one
|
|
7
|
+
// platform cause across every desk — with when it was first and last seen,
|
|
8
|
+
// how many times, which runs, and what to do. It stays open until someone
|
|
9
|
+
// resolves it; a later good run is shown ("fine since …") but closes nothing.
|
|
10
|
+
// (Closing on its own is opt-in, per flow — step 4.)
|
|
11
|
+
//
|
|
12
|
+
// Issues are derived from the runs; only what people did to them is stored
|
|
13
|
+
// (IssueState: acknowledged, snoozed, resolved, reopened, with a history).
|
|
14
|
+
// A run after the issue was resolved opens it again. Pure: the browser, the
|
|
15
|
+
// API and the CLI read the same function.
|
|
16
|
+
/** The model refusing the credential, as the runner sees it (runner.ts
|
|
17
|
+
* isAuthRefusal reads this one pattern, so a refusal the runner retries on
|
|
18
|
+
* is the same refusal an issue groups by — Codex review). */
|
|
19
|
+
export const AUTH_REFUSAL = /API Error: 401|access token has (been revoked|expired)|OAuth token (has )?(been )?(revoked|expired)|Failed to authenticate/i;
|
|
20
|
+
// On 2026-09-30 every browser step on every desk failed "login-checker: pod
|
|
21
|
+
// never became ready" — the nightly prune had deleted the runner image — and
|
|
22
|
+
// the overview listed it as a dozen separate desk failures, each looking like
|
|
23
|
+
// that desk's login-checker was broken; on 2026-09-29 the worker could not
|
|
24
|
+
// reach the Kubernetes API for eight minutes ("pod create failed … dial tcp
|
|
25
|
+
// 10.43.0.1:443: i/o timeout"). These are the failures no edit to a
|
|
26
|
+
// flow can cause or fix; they are grouped into one item that says so.
|
|
27
|
+
const PLATFORM_CAUSES = [
|
|
28
|
+
{
|
|
29
|
+
key: "sandbox",
|
|
30
|
+
label: "Run sandboxes cannot start",
|
|
31
|
+
fix: "the runner image or the node — not the desks. Rerun once it is fixed.",
|
|
32
|
+
test: /pod never became ready|pod create failed|ImagePullBackOff|ErrImagePull|failed to pull image|sandbox (never started|failed to start)/i,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "model-auth",
|
|
36
|
+
label: "The model credential is refused",
|
|
37
|
+
fix: "the Claude sign-in expired or was revoked; sign in again, then rerun.",
|
|
38
|
+
test: new RegExp(`${AUTH_REFUSAL.source}|refresh token expired|authentication_error|invalid x-api-key|(claude|anthropic|model)[^\\n]{0,40}\\b401\\b|\\b401\\b[^\\n]{0,40}(claude|anthropic|model)`, "i"),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "model-cap",
|
|
42
|
+
label: "The model usage cap is reached",
|
|
43
|
+
fix: "runs resume when the cap resets; nothing to change in the desks.",
|
|
44
|
+
test: /weekly (usage )?limit|usage limit (reached|exceeded)|reached your (weekly|usage) limit/i,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
// The provider out of capacity is not the account's cap: it passes in
|
|
48
|
+
// minutes, not at a reset (Codex review).
|
|
49
|
+
key: "model-busy",
|
|
50
|
+
label: "The model provider is overloaded",
|
|
51
|
+
fix: "the provider's capacity, not the desks; rerun in a few minutes.",
|
|
52
|
+
test: /overloaded_error|\b529\b[^\n]{0,40}overloaded|overloaded[^\n]{0,40}\b529\b/i,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
key: "worker",
|
|
56
|
+
label: "The platform lost the run",
|
|
57
|
+
fix: "a worker restarted mid-run; rerun it.",
|
|
58
|
+
test: /worker (restarted|was restarted|died|lost)|run was orphaned|lost its worker/i,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
/** Whether a failure reason is the platform's, and which kind. */
|
|
62
|
+
export function platformCause(reason) {
|
|
63
|
+
if (!reason)
|
|
64
|
+
return null;
|
|
65
|
+
const hit = PLATFORM_CAUSES.find((c) => c.test.test(reason));
|
|
66
|
+
return hit ? { key: hit.key, label: hit.label, fix: hit.fix } : null;
|
|
67
|
+
}
|
|
68
|
+
/** A short stable id from the issue's key (FNV-1a, 64 bits as two halves). */
|
|
69
|
+
function idOf(key) {
|
|
70
|
+
let a = 0x811c9dc5, b = 0x01000193 ^ key.length;
|
|
71
|
+
for (let i = 0; i < key.length; i++) {
|
|
72
|
+
const c = key.charCodeAt(i);
|
|
73
|
+
a = Math.imul(a ^ c, 0x01000193) >>> 0;
|
|
74
|
+
b = Math.imul(b ^ c, 0x5bd1e995) >>> 0;
|
|
75
|
+
}
|
|
76
|
+
return `iss_${a.toString(36)}${b.toString(36)}`;
|
|
77
|
+
}
|
|
78
|
+
/** Which issue a run belongs to, or null when it is not a problem. */
|
|
79
|
+
export function issueKeyOf(r) {
|
|
80
|
+
if (r.missed)
|
|
81
|
+
return { id: idOf(`missed\u0000${r.workspace}\u0000${r.what}`), kind: "missed", agent: null, cause: null };
|
|
82
|
+
if (r.outcome === "failed") {
|
|
83
|
+
const cause = platformCause(r.failure?.reason);
|
|
84
|
+
if (cause)
|
|
85
|
+
return { id: idOf(`platform\u0000${cause.key}`), kind: "platform", agent: null, cause };
|
|
86
|
+
const agent = r.failure?.agent ?? null;
|
|
87
|
+
return { id: idOf(`failed\u0000${r.workspace}\u0000${r.what}\u0000${agent ?? ""}`), kind: "failed", agent, cause: null };
|
|
88
|
+
}
|
|
89
|
+
if (r.outcome === "blocked" || r.outcome === "bad") {
|
|
90
|
+
return { id: idOf(`${r.outcome}\u0000${r.workspace}\u0000${r.what}`), kind: r.outcome, agent: null, cause: null };
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const ACTION = {
|
|
95
|
+
failed: "Read why it failed, fix the flow or agent, then rerun",
|
|
96
|
+
blocked: "Clear what it is waiting on (a login, a key, an input), then rerun",
|
|
97
|
+
bad: "Read what it found and decide what to do",
|
|
98
|
+
missed: "Check why it did not start: the flow or the account paused, out of credit, or the scheduler down — then run it by hand",
|
|
99
|
+
};
|
|
100
|
+
const lastOf = (h, actions) => [...h].reverse().find((e) => actions.includes(e.action)) ?? null;
|
|
101
|
+
/**
|
|
102
|
+
* Every issue the runs show, with what people did to them folded in.
|
|
103
|
+
* `states` is keyed by issue id. Resolved issues are kept (with their last
|
|
104
|
+
* occurrences) while their runs are still in the window, so their history
|
|
105
|
+
* can be read; a status filter is the caller's.
|
|
106
|
+
*/
|
|
107
|
+
export function foldIssues(runs, states, now,
|
|
108
|
+
/** `autoClose(workspace, flow)`: the flow says `issues: auto-close`, so a
|
|
109
|
+
* run that goes fine after a failure closes it (FOL-72 step 7). Never a
|
|
110
|
+
* platform cause or a missed schedule — those stay until a person says. */
|
|
111
|
+
opts = {}) {
|
|
112
|
+
// When it went wrong (or right): its end, else its start. A run that
|
|
113
|
+
// started before a resolve and failed after it is news, and of two
|
|
114
|
+
// overlapping failures the one that ended last is the newest (Codex review).
|
|
115
|
+
const occurred = (r) => r.finishedAt ?? r.startedAt;
|
|
116
|
+
const all = [...runs].sort((a, b) => occurred(a).localeCompare(occurred(b)));
|
|
117
|
+
// The last run of each desk's flow to finish, to say "fine since" — only
|
|
118
|
+
// when it is itself fine, and finished after the issue's last occurrence
|
|
119
|
+
// (a success between two failures, or one that overlapped a later
|
|
120
|
+
// failure, is not a recovery — Codex review).
|
|
121
|
+
const newestDone = new Map();
|
|
122
|
+
for (const r of all) {
|
|
123
|
+
if (r.outcome === "running" || r.outcome === "queued" || r.outcome === "waiting")
|
|
124
|
+
continue;
|
|
125
|
+
const k = `${r.workspace}\u0000${r.what}`;
|
|
126
|
+
const cur = newestDone.get(k);
|
|
127
|
+
if (!cur || occurred(r) >= occurred(cur))
|
|
128
|
+
newestDone.set(k, r);
|
|
129
|
+
}
|
|
130
|
+
const fine = (o) => o === "good" || o === "done" || o === "quiet";
|
|
131
|
+
const groups = new Map();
|
|
132
|
+
for (const r of all) {
|
|
133
|
+
const key = issueKeyOf(r);
|
|
134
|
+
if (!key)
|
|
135
|
+
continue;
|
|
136
|
+
const g = groups.get(key.id) ?? { key, runs: [] };
|
|
137
|
+
g.runs.push(r);
|
|
138
|
+
groups.set(key.id, g);
|
|
139
|
+
}
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const [id, { key, runs: hit }] of groups) {
|
|
142
|
+
const history = states[id]?.history ?? [];
|
|
143
|
+
const res = lastOf(history, ["resolve", "reopen"]);
|
|
144
|
+
// Replay the runs and the resolves/reopens in time order: a run going
|
|
145
|
+
// wrong after a resolve starts a new episode, a resolve closes the
|
|
146
|
+
// current one, a reopen carries it on. The issue shows its current (or
|
|
147
|
+
// last closed) episode — never every one ever closed. A run that ended
|
|
148
|
+
// at the moment of a resolve was closed by it. (Codex review, in rounds.)
|
|
149
|
+
// In a flow that says `issues: auto-close`, every run of it that went
|
|
150
|
+
// fine closes the episode as a resolve would, marked as the platform's.
|
|
151
|
+
const auto = key.kind !== "platform" && key.kind !== "missed" && hit[0] && opts.autoClose?.(hit[0].workspace, hit[0].what) === true;
|
|
152
|
+
const fineRuns = auto ? all.filter((r) => r.workspace === hit[0].workspace && r.what === hit[0].what && fine(r.outcome)) : [];
|
|
153
|
+
const timeline = [
|
|
154
|
+
...hit.map((r) => ({ t: occurred(r), order: 0, run: r, act: null })),
|
|
155
|
+
...history.filter((e) => e.action === "resolve" || e.action === "reopen").map((e) => ({ t: e.at, order: 1, run: null, act: e.action })),
|
|
156
|
+
...fineRuns.map((r) => ({ t: occurred(r), order: 1, run: null, act: "auto" })),
|
|
157
|
+
].sort((a, b) => a.t.localeCompare(b.t) || a.order - b.order);
|
|
158
|
+
let shown = [];
|
|
159
|
+
let closed = false;
|
|
160
|
+
let closedBy = null;
|
|
161
|
+
let autoAt = null;
|
|
162
|
+
// The last time the platform closed it: an acknowledge or a snooze from
|
|
163
|
+
// before then belongs to the episode it closed (Codex review).
|
|
164
|
+
let lastAutoClose = null;
|
|
165
|
+
let episodes = 0;
|
|
166
|
+
for (const ev of timeline) {
|
|
167
|
+
if (ev.run) {
|
|
168
|
+
if (closed || !shown.length) {
|
|
169
|
+
shown = [];
|
|
170
|
+
episodes++;
|
|
171
|
+
}
|
|
172
|
+
closed = false;
|
|
173
|
+
shown.push(ev.run);
|
|
174
|
+
}
|
|
175
|
+
else if (ev.act === "resolve" || (ev.act === "auto" && shown.length)) {
|
|
176
|
+
closed = true;
|
|
177
|
+
closedBy = ev.act === "auto" ? "auto" : "person";
|
|
178
|
+
if (ev.act === "auto") {
|
|
179
|
+
autoAt = ev.t;
|
|
180
|
+
lastAutoClose = ev.t;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (ev.act === "reopen")
|
|
184
|
+
closed = false;
|
|
185
|
+
}
|
|
186
|
+
if (!shown.length)
|
|
187
|
+
continue;
|
|
188
|
+
const resolvedAt = closed ? (closedBy === "auto" ? autoAt : res?.action === "resolve" ? res.at : null) : null;
|
|
189
|
+
const newest = shown[shown.length - 1];
|
|
190
|
+
// A snooze holds while its time has not come and, for "until N more",
|
|
191
|
+
// while fewer than N have happened since it.
|
|
192
|
+
const current = (e) => (e && (!lastAutoClose || e.at > lastAutoClose) ? e : null);
|
|
193
|
+
const snoozeEv = current(lastOf(history, ["snooze", "resolve", "reopen"]));
|
|
194
|
+
const since = snoozeEv?.action === "snooze" ? shown.filter((r) => occurred(r) > snoozeEv.at).length : 0;
|
|
195
|
+
const snoozeLeft = snoozeEv?.action === "snooze" && snoozeEv.untilCount ? Math.max(0, snoozeEv.untilCount - since) : null;
|
|
196
|
+
const snoozeHolds = snoozeEv?.action === "snooze" &&
|
|
197
|
+
(!snoozeEv.until || Date.parse(snoozeEv.until) > now) &&
|
|
198
|
+
(snoozeLeft === null || snoozeLeft > 0) &&
|
|
199
|
+
Boolean(snoozeEv.until || snoozeEv.untilCount);
|
|
200
|
+
const status = (() => {
|
|
201
|
+
if (resolvedAt)
|
|
202
|
+
return "resolved";
|
|
203
|
+
if (snoozeHolds)
|
|
204
|
+
return "snoozed";
|
|
205
|
+
const ack = current(lastOf(history, ["acknowledge", "resolve", "reopen"]));
|
|
206
|
+
if (ack?.action === "acknowledge")
|
|
207
|
+
return "acknowledged";
|
|
208
|
+
return "open";
|
|
209
|
+
})();
|
|
210
|
+
const ack = current(lastOf(history, ["acknowledge", "resolve", "reopen"]));
|
|
211
|
+
const flowKey = `${newest.workspace}\u0000${newest.what}`;
|
|
212
|
+
const last = key.kind === "platform" ? null : newestDone.get(flowKey) ?? null;
|
|
213
|
+
const ok = last && fine(last.outcome) && occurred(last) > occurred(newest) ? occurred(last) : null;
|
|
214
|
+
const workspaces = [...new Set(shown.map((r) => r.workspace))].sort();
|
|
215
|
+
out.push({
|
|
216
|
+
id,
|
|
217
|
+
kind: key.kind,
|
|
218
|
+
status,
|
|
219
|
+
workspace: newest.workspace,
|
|
220
|
+
workspaces,
|
|
221
|
+
what: key.cause ? key.cause.label : newest.what,
|
|
222
|
+
agent: key.agent,
|
|
223
|
+
reason: key.kind === "missed"
|
|
224
|
+
? newest.headline ?? `due ${newest.startedAt}, no run started`
|
|
225
|
+
: key.cause
|
|
226
|
+
? newest.failure ? `${newest.failure.agent}: ${newest.failure.reason}` : key.cause.label
|
|
227
|
+
: newest.failure ? `${newest.failure.agent}: ${newest.failure.reason}` : newest.headline?.trim() || newest.outcome,
|
|
228
|
+
action: key.cause ? `Fix ${key.cause.fix}` : ACTION[key.kind],
|
|
229
|
+
firstSeen: occurred(shown[0]),
|
|
230
|
+
lastSeen: occurred(newest),
|
|
231
|
+
count: shown.length,
|
|
232
|
+
runs: shown.slice(-20).reverse().map((r) => ({ workspace: r.workspace, id: r.id, at: occurred(r), ...(r.missed ? { missed: true } : {}) })),
|
|
233
|
+
recoveredAt: ok,
|
|
234
|
+
acknowledged: ack?.action === "acknowledge" ? { at: ack.at, by: ack.by } : null,
|
|
235
|
+
snoozedUntil: status === "snoozed" && snoozeEv?.until ? snoozeEv.until : null,
|
|
236
|
+
snoozedFor: status === "snoozed" ? snoozeLeft : null,
|
|
237
|
+
regressed: episodes > 1,
|
|
238
|
+
sinceRevision: shown[0].revision ?? null,
|
|
239
|
+
// Only while it is resolved; once it happens again, the history says it was.
|
|
240
|
+
resolved: status === "resolved"
|
|
241
|
+
? closedBy === "auto"
|
|
242
|
+
? { at: resolvedAt, by: "auto: a later run went fine" }
|
|
243
|
+
: res
|
|
244
|
+
? { at: res.at, by: res.by }
|
|
245
|
+
: null
|
|
246
|
+
: null,
|
|
247
|
+
history,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const rank = { open: 0, acknowledged: 1, snoozed: 2, resolved: 3 };
|
|
251
|
+
const kindRank = { platform: 0, missed: 1, failed: 2, blocked: 3, bad: 4 };
|
|
252
|
+
return out.sort((a, b) => rank[a.status] - rank[b.status] || kindRank[a.kind] - kindRank[b.kind] || b.lastSeen.localeCompare(a.lastSeen));
|
|
253
|
+
}
|
|
254
|
+
/** An issue still asking for someone: open or acknowledged, not snoozed or resolved. */
|
|
255
|
+
export const isActiveIssue = (i) => i.status === "open" || i.status === "acknowledged";
|
|
256
|
+
/**
|
|
257
|
+
* What the run doctor needs to look at an issue: its newest run, the
|
|
258
|
+
* workspace that run is in (the assistant works inside one), and the
|
|
259
|
+
* question the run page's Doctor button asks — so the issue's button, the
|
|
260
|
+
* CLI's `issues doctor` and the run page put it the same way. Null for a
|
|
261
|
+
* missed schedule, which has no run to read.
|
|
262
|
+
*/
|
|
263
|
+
export function issueDoctor(issue) {
|
|
264
|
+
const run = issue.runs.find((r) => !r.missed);
|
|
265
|
+
if (!run || issue.kind === "missed")
|
|
266
|
+
return null;
|
|
267
|
+
const ended = issue.kind === "blocked" ? "end BLOCKED" : issue.kind === "bad" ? "end BAD" : "fail";
|
|
268
|
+
return { workspace: run.workspace, runId: run.id, prompt: `Doctor: why did run ${run.id} ${ended}, and how do I fix it?` };
|
|
269
|
+
}
|
|
270
|
+
/** Parse "30m", "4h", "1d", "1w" into milliseconds; null if not one. */
|
|
271
|
+
export function snoozeMs(s) {
|
|
272
|
+
const m = /^(\d+)\s*(m|h|d|w)$/.exec(s.trim());
|
|
273
|
+
if (!m)
|
|
274
|
+
return null;
|
|
275
|
+
const n = Number(m[1]);
|
|
276
|
+
const unit = { m: 60_000, h: 3_600_000, d: 86_400_000, w: 7 * 86_400_000 }[m[2]];
|
|
277
|
+
return n > 0 ? n * unit : null;
|
|
278
|
+
}
|
|
279
|
+
/** One person's act on an issue, appended to its history. Refuses a snooze
|
|
280
|
+
* with no future `until`. Pure: the caller stores the result. */
|
|
281
|
+
export function applyIssueAction(state, ev, now) {
|
|
282
|
+
if (ev.action === "snooze" && !(ev.until && Date.parse(ev.until) > now) && !(typeof ev.untilCount === "number" && ev.untilCount > 0)) {
|
|
283
|
+
throw new Error("a snooze needs a time in the future, or a number of times it may happen again");
|
|
284
|
+
}
|
|
285
|
+
const history = [...(state?.history ?? []), ev];
|
|
286
|
+
// Bounded: an issue snoozed every morning for a year is still one row. The
|
|
287
|
+
// newest of each kind of act is always kept, whatever its age, because the
|
|
288
|
+
// issue's state is read from them — dropping the last resolve would bring
|
|
289
|
+
// back what it closed (Codex review).
|
|
290
|
+
if (history.length <= 100)
|
|
291
|
+
return { history };
|
|
292
|
+
const keep = new Set(history.map((_, i) => i).slice(-100));
|
|
293
|
+
for (const a of ["acknowledge", "snooze", "resolve", "reopen"]) {
|
|
294
|
+
const i = history.map((e) => e.action).lastIndexOf(a);
|
|
295
|
+
if (i >= 0)
|
|
296
|
+
keep.add(i);
|
|
297
|
+
}
|
|
298
|
+
// Every resolve and reopen: the episodes an issue shows are replayed from
|
|
299
|
+
// them, and each one is a person's act, so there are few (Codex review).
|
|
300
|
+
history.forEach((e, i) => (e.action === "resolve" || e.action === "reopen") && keep.add(i));
|
|
301
|
+
return { history: history.filter((_, i) => keep.has(i)) };
|
|
302
|
+
}
|
package/dist/src/library.js
CHANGED
|
@@ -14,7 +14,7 @@ import { platform } from "./platform.js";
|
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { dataRoot, singleWorkspace, singleAccountRoot, MAX_EDITABLE_FILE } from "./paths.js";
|
|
17
|
-
import matter from "
|
|
17
|
+
import matter from "./matter.js";
|
|
18
18
|
import { KINDS } from "./kinds.js";
|
|
19
19
|
import { recordRevision, registerTreeReader } from "./history.js";
|
|
20
20
|
import { assertSafeName, assertCanonicalCase, buildMemoryIndex, parseToolDef, readToolDir, syncBundleFor, listWorkspaces, listAgents, workspaceDir, } from "./store.js";
|
package/dist/src/limits.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export type Limits = Record<string, number>;
|
|
2
2
|
/** The key that caps every call in the step. */
|
|
3
3
|
export declare const TOTAL_KEY = "calls";
|
|
4
|
+
/** The key that caps one identical call: a model asking the same tool the
|
|
5
|
+
* same thing over and over is stuck, and every repeat is spend. Each call
|
|
6
|
+
* also counts under `repeat:<hash of tool and input>`; `repeat: N` caps
|
|
7
|
+
* every one of those. */
|
|
8
|
+
export declare const REPEAT_KEY = "repeat";
|
|
4
9
|
/** One spelling for comparing: `desk-email` and `desk_email` are the same
|
|
5
10
|
* tool (a script tool's SDK name has its dashes made underscores). */
|
|
6
11
|
export declare function normKey(key: string): string;
|
|
@@ -34,6 +39,9 @@ export declare function toolOwners(apiOwners: Record<string, string>, scripts: {
|
|
|
34
39
|
* name, its input, and the owners map.
|
|
35
40
|
*/
|
|
36
41
|
export declare function limitKeysFor(toolName: string, input: Record<string, unknown> | undefined, owners?: Record<string, string>): string[];
|
|
42
|
+
/** What step-exec counts one call under: limitKeysFor's keys and the
|
|
43
|
+
* call's own `repeat:` key, for `limits: repeat`. */
|
|
44
|
+
export declare function callKeysFor(toolName: string, input: Record<string, unknown> | undefined, owners?: Record<string, string>): string[];
|
|
37
45
|
export interface LimitRefusal {
|
|
38
46
|
key: string;
|
|
39
47
|
used: number;
|