@sweny-ai/core 0.1.198 → 0.1.200
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/agent-env.d.ts +94 -2
- package/dist/agent-env.js +144 -7
- package/dist/budget.d.ts +77 -11
- package/dist/budget.js +132 -16
- package/dist/cli/renderer.js +5 -0
- package/dist/cli/run-history.d.ts +12 -0
- package/dist/cli/run-history.js +16 -0
- package/dist/cli/run-output.d.ts +16 -1
- package/dist/cli/run-output.js +46 -4
- package/dist/cli/runs.js +38 -1
- package/dist/cli/ticket.js +7 -2
- package/dist/eval/index.d.ts +10 -0
- package/dist/eval/index.js +27 -6
- package/dist/eval/judge.d.ts +3 -0
- package/dist/eval/judge.js +12 -2
- package/dist/executor.d.ts +5 -1
- package/dist/executor.js +276 -31
- package/dist/harness/acp.d.ts +5 -2
- package/dist/harness/acp.js +11 -23
- package/dist/harness/auth.js +2 -1
- package/dist/harness/claude-code.d.ts +18 -13
- package/dist/harness/claude-code.js +34 -15
- package/dist/harness/codex.d.ts +22 -5
- package/dist/harness/codex.js +67 -11
- package/dist/harness/compat.js +1 -0
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.js +1 -1
- package/dist/harness/pi.d.ts +11 -5
- package/dist/harness/pi.js +27 -9
- package/dist/harness/prompts.d.ts +11 -1
- package/dist/harness/prompts.js +21 -2
- package/dist/harness/sandbox-wrapper.d.ts +13 -2
- package/dist/harness/sandbox-wrapper.js +29 -4
- package/dist/harness/types.d.ts +9 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/journal.d.ts +48 -10
- package/dist/journal.js +164 -16
- package/dist/safe-outputs.d.ts +9 -0
- package/dist/safe-outputs.js +41 -26
- package/dist/skills/linear.js +3 -1
- package/dist/studio.js +2 -1
- package/dist/types.d.ts +34 -1
- package/package.json +1 -1
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
* `ClaudeClient.ask` / `ClaudeClient.evaluate` built before the harness seam
|
|
5
5
|
* (#330); prompts.test.ts pins them against the old builders.
|
|
6
6
|
*/
|
|
7
|
-
import type { Claude, JSONSchema, Logger } from "../types.js";
|
|
7
|
+
import type { Claude, JSONSchema, Logger, NodeUsage } from "../types.js";
|
|
8
8
|
import type { AgentHarness } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* Hand a completion's usage to the caller's `onUsage`, once, before the
|
|
11
|
+
* adapter looks at whether the completion succeeded (a failed one was still
|
|
12
|
+
* billed). A hook that throws is logged and ignored: it never changes the
|
|
13
|
+
* completion's result, so a good answer is never turned into a failure (and
|
|
14
|
+
* a judge or route call is never repeated because of it).
|
|
15
|
+
*/
|
|
16
|
+
export declare function reportCompletionUsage(onUsage: ((usage: NodeUsage) => void) | undefined, usage: NodeUsage | undefined, logger?: Logger): void;
|
|
9
17
|
/** @internal Registers the legacy object behind a compat harness. */
|
|
10
18
|
export declare function registerLegacyClaude(harness: AgentHarness, claude: Claude): void;
|
|
11
19
|
/** @internal The legacy `Claude` behind a compat harness, if any. */
|
|
@@ -48,6 +56,7 @@ export declare function ask(harness: AgentHarness, opts: {
|
|
|
48
56
|
model?: string;
|
|
49
57
|
timeoutMs?: number;
|
|
50
58
|
signal?: AbortSignal;
|
|
59
|
+
onUsage?: (usage: NodeUsage) => void;
|
|
51
60
|
}): Promise<string>;
|
|
52
61
|
/**
|
|
53
62
|
* Route evaluation: pick one of N choices. Fails closed: null when the
|
|
@@ -62,4 +71,5 @@ export declare function evaluate(harness: AgentHarness, opts: {
|
|
|
62
71
|
}[];
|
|
63
72
|
timeoutMs?: number;
|
|
64
73
|
signal?: AbortSignal;
|
|
74
|
+
onUsage?: (usage: NodeUsage) => void;
|
|
65
75
|
}, logger?: Logger): Promise<string | null>;
|
package/dist/harness/prompts.js
CHANGED
|
@@ -6,6 +6,23 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { consoleLogger } from "../types.js";
|
|
8
8
|
import { fenceUntrustedJson } from "../untrusted.js";
|
|
9
|
+
/**
|
|
10
|
+
* Hand a completion's usage to the caller's `onUsage`, once, before the
|
|
11
|
+
* adapter looks at whether the completion succeeded (a failed one was still
|
|
12
|
+
* billed). A hook that throws is logged and ignored: it never changes the
|
|
13
|
+
* completion's result, so a good answer is never turned into a failure (and
|
|
14
|
+
* a judge or route call is never repeated because of it).
|
|
15
|
+
*/
|
|
16
|
+
export function reportCompletionUsage(onUsage, usage, logger) {
|
|
17
|
+
if (!onUsage || !usage)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
onUsage(usage);
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
(logger ?? consoleLogger).warn(`usage hook failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
9
26
|
/** Legacy `Claude` objects wrapped by `claudeCompat()`, so their own ask/evaluate still win. */
|
|
10
27
|
const LEGACY = new WeakMap();
|
|
11
28
|
/** @internal Registers the legacy object behind a compat harness. */
|
|
@@ -76,13 +93,14 @@ export async function ask(harness, opts) {
|
|
|
76
93
|
const legacy = legacyClaudeOf(harness);
|
|
77
94
|
if (legacy)
|
|
78
95
|
return legacy.ask(opts);
|
|
79
|
-
const { instruction, context, model, timeoutMs, signal } = opts;
|
|
96
|
+
const { instruction, context, model, timeoutMs, signal, onUsage } = opts;
|
|
80
97
|
const response = await harness.complete({
|
|
81
98
|
prompt: buildAskPrompt(instruction, context),
|
|
82
99
|
model,
|
|
83
100
|
timeoutMs,
|
|
84
101
|
signal,
|
|
85
102
|
purpose: "ask",
|
|
103
|
+
...(onUsage ? { onUsage } : {}),
|
|
86
104
|
});
|
|
87
105
|
return (response ?? "").trim();
|
|
88
106
|
}
|
|
@@ -95,12 +113,13 @@ export async function evaluate(harness, opts, logger) {
|
|
|
95
113
|
if (legacy)
|
|
96
114
|
return legacy.evaluate(opts);
|
|
97
115
|
const log = logger ?? harness.logger ?? consoleLogger;
|
|
98
|
-
const { question, context, choices, timeoutMs, signal } = opts;
|
|
116
|
+
const { question, context, choices, timeoutMs, signal, onUsage } = opts;
|
|
99
117
|
const response = await harness.complete({
|
|
100
118
|
prompt: buildEvaluatePrompt(question, context, choices),
|
|
101
119
|
timeoutMs,
|
|
102
120
|
signal,
|
|
103
121
|
purpose: "evaluate",
|
|
122
|
+
...(onUsage ? { onUsage } : {}),
|
|
104
123
|
});
|
|
105
124
|
// The adapter already logged why; fail closed with no decision.
|
|
106
125
|
if (response === null)
|
|
@@ -107,11 +107,22 @@ export interface SrtSettings {
|
|
|
107
107
|
* each with an optional `:port`. Dropping an entry only narrows egress.
|
|
108
108
|
*/
|
|
109
109
|
export declare function isSrtDomainPattern(entry: string): boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Credential files an agent env points at outside `credentialHome`: the
|
|
112
|
+
* {@link CREDENTIAL_PATHS} under its `HOME` when that is another directory (a
|
|
113
|
+
* harness `env` overlay), `$CLAUDE_CONFIG_DIR/.credentials.json` and
|
|
114
|
+
* `$CODEX_HOME/auth.json`. A relative value resolves against the agent's
|
|
115
|
+
* `cwd`, where the agent would resolve it, never sweny's. Absolute paths.
|
|
116
|
+
*/
|
|
117
|
+
export declare function envCredentialPaths(env: Record<string, string> | undefined, credentialHome: string, cwd: string): string[];
|
|
110
118
|
/**
|
|
111
119
|
* The srt settings for one wrapped spawn. Pure apart from `exists`.
|
|
112
|
-
* `credentialHome` is the operator's real HOME (not the scratch HOME)
|
|
120
|
+
* `credentialHome` is the operator's real HOME (not the scratch HOME); the
|
|
121
|
+
* login files `req.env` selects elsewhere are denied too ({@link envCredentialPaths}).
|
|
113
122
|
*/
|
|
114
|
-
export declare function buildSrtSettings(req: Pick<SandboxWrapRequest, "cwd" | "egress" | "readOnly" | "denyRead"
|
|
123
|
+
export declare function buildSrtSettings(req: Pick<SandboxWrapRequest, "cwd" | "egress" | "readOnly" | "denyRead"> & {
|
|
124
|
+
env?: Record<string, string>;
|
|
125
|
+
}, opts: {
|
|
115
126
|
home: string;
|
|
116
127
|
credentialHome: string;
|
|
117
128
|
exists?: (p: string) => boolean;
|
|
@@ -116,16 +116,41 @@ function real(p) {
|
|
|
116
116
|
return p;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Credential files an agent env points at outside `credentialHome`: the
|
|
121
|
+
* {@link CREDENTIAL_PATHS} under its `HOME` when that is another directory (a
|
|
122
|
+
* harness `env` overlay), `$CLAUDE_CONFIG_DIR/.credentials.json` and
|
|
123
|
+
* `$CODEX_HOME/auth.json`. A relative value resolves against the agent's
|
|
124
|
+
* `cwd`, where the agent would resolve it, never sweny's. Absolute paths.
|
|
125
|
+
*/
|
|
126
|
+
export function envCredentialPaths(env, credentialHome, cwd) {
|
|
127
|
+
if (!env)
|
|
128
|
+
return [];
|
|
129
|
+
const at = (dir, ...rest) => path.resolve(cwd, dir, ...rest);
|
|
130
|
+
const out = [];
|
|
131
|
+
if (env.HOME && at(env.HOME) !== path.resolve(credentialHome)) {
|
|
132
|
+
out.push(...CREDENTIAL_PATHS.map((rel) => at(env.HOME, rel)));
|
|
133
|
+
}
|
|
134
|
+
if (env.CLAUDE_CONFIG_DIR)
|
|
135
|
+
out.push(at(env.CLAUDE_CONFIG_DIR, ".credentials.json"));
|
|
136
|
+
if (env.CODEX_HOME)
|
|
137
|
+
out.push(at(env.CODEX_HOME, "auth.json"));
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
119
140
|
/**
|
|
120
141
|
* The srt settings for one wrapped spawn. Pure apart from `exists`.
|
|
121
|
-
* `credentialHome` is the operator's real HOME (not the scratch HOME)
|
|
142
|
+
* `credentialHome` is the operator's real HOME (not the scratch HOME); the
|
|
143
|
+
* login files `req.env` selects elsewhere are denied too ({@link envCredentialPaths}).
|
|
122
144
|
*/
|
|
123
145
|
export function buildSrtSettings(req, opts) {
|
|
124
146
|
const exists = opts.exists ?? existsSync;
|
|
125
147
|
const allowedDomains = [...new Set(req.egress.map((d) => d.trim()).filter(isSrtDomainPattern))];
|
|
126
|
-
const denyRead =
|
|
127
|
-
|
|
128
|
-
|
|
148
|
+
const denyRead = [
|
|
149
|
+
...new Set([
|
|
150
|
+
...(opts.credentialPaths ?? CREDENTIAL_PATHS).map((rel) => path.isAbsolute(rel) ? rel : path.join(opts.credentialHome, rel)),
|
|
151
|
+
...envCredentialPaths(req.env, opts.credentialHome, req.cwd),
|
|
152
|
+
]),
|
|
153
|
+
].filter((p) => exists(p));
|
|
129
154
|
// Deny the parent, not a snapshot of sibling homes: later-created runs
|
|
130
155
|
// must stay hidden too. srt allowRead carves out only our own HOME.
|
|
131
156
|
if (opts.isolationRoot)
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Browser-safe: types only, no runtime imports.
|
|
10
10
|
*/
|
|
11
11
|
import type { AgentAccess } from "../agent-env.js";
|
|
12
|
-
import type { Claude, Logger, NodeResult } from "../types.js";
|
|
12
|
+
import type { Claude, Logger, NodeResult, NodeUsage } from "../types.js";
|
|
13
13
|
export type HarnessId = "claude-code" | "mock" | "codex" | "pi" | "hermes" | "gemini" | `acp:${string}`;
|
|
14
14
|
/** Portable tool vocabulary for node policy. */
|
|
15
15
|
export type ToolClass = "shell" | "write" | "edit" | "net" | "subagent";
|
|
@@ -163,6 +163,14 @@ export interface HarnessCompleteRequest {
|
|
|
163
163
|
* logs ("claude.ask: ..." vs "claude.evaluate: ..."); it never changes the query.
|
|
164
164
|
*/
|
|
165
165
|
purpose?: "ask" | "evaluate";
|
|
166
|
+
/**
|
|
167
|
+
* Called at most once per completion with that completion's total usage
|
|
168
|
+
* (not cumulative, unlike the run request's `onUsage`), before any failure
|
|
169
|
+
* check: a failed completion was still billed. An adapter that cannot report
|
|
170
|
+
* usage never calls it. Guard the call so a throwing hook never changes the
|
|
171
|
+
* result or repeats the completion (in-tree adapters use `reportCompletionUsage`).
|
|
172
|
+
*/
|
|
173
|
+
onUsage?: (usage: NodeUsage) => void;
|
|
166
174
|
}
|
|
167
175
|
export interface AgentHarness {
|
|
168
176
|
readonly id: HarnessId;
|
package/dist/index.d.ts
CHANGED
|
@@ -29,14 +29,14 @@ export { execute, RouteEvaluationError } from "./executor.js";
|
|
|
29
29
|
export type { ExecuteOptions } from "./executor.js";
|
|
30
30
|
export { systemOneProvider } from "./decider.js";
|
|
31
31
|
export type { DeciderConfig, DeciderOperatorConfig } from "./decider.js";
|
|
32
|
-
export { BudgetGuard, describeOverrun, minLimits, toLimits } from "./budget.js";
|
|
33
|
-
export type { Budget, BudgetOverrun, BudgetUnit, SpendLimits } from "./budget.js";
|
|
32
|
+
export { BudgetGuard, SPEND_KINDS, describeOverrun, minLimits, toLimits } from "./budget.js";
|
|
33
|
+
export type { Budget, BudgetOverrun, BudgetUnit, KindSpend, SeedSpend, SpendBreakdown, SpendKind, SpendLimits, } from "./budget.js";
|
|
34
34
|
export { resolveNodePermissions, buildNodePolicy } from "./node-policy.js";
|
|
35
35
|
export type { ResolvedPermissions } from "./node-policy.js";
|
|
36
36
|
export { applySafeOutputs, createEmitOutputTool, resolveActor, EMIT_OUTPUT_TOOL } from "./safe-outputs.js";
|
|
37
37
|
export type { SafeOutputIntent, ActorInfo, WriteStageState } from "./safe-outputs.js";
|
|
38
|
-
export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness, CodexHarness, PiHarness, SUPPORTED_AGENTS, } from "./harness/index.js";
|
|
39
|
-
export type { AgentHarness, HarnessCapabilities, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, ClaudeCodeHarnessOptions, CodexHarnessOptions, PiHarnessOptions, } from "./harness/index.js";
|
|
38
|
+
export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness, CodexHarness, PiHarness, SUPPORTED_AGENTS, reportCompletionUsage, } from "./harness/index.js";
|
|
39
|
+
export type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, ClaudeCodeHarnessOptions, CodexHarnessOptions, PiHarnessOptions, } from "./harness/index.js";
|
|
40
40
|
export { ClaudeClient, resolveAuthEnv } from "./harness/claude-code.js";
|
|
41
41
|
export type { ClaudeClientOptions, SwenyAuthMode, ResolveAuthEnvOpts } from "./harness/claude-code.js";
|
|
42
42
|
export { resolveExecutionModel } from "./model.js";
|
package/dist/index.js
CHANGED
|
@@ -30,11 +30,11 @@ export { execute, RouteEvaluationError } from "./executor.js";
|
|
|
30
30
|
// Decision models (#357): config types and the provider adapter only
|
|
31
31
|
export { systemOneProvider } from "./decider.js";
|
|
32
32
|
// Least-privilege nodes and safe outputs (#365)
|
|
33
|
-
export { BudgetGuard, describeOverrun, minLimits, toLimits } from "./budget.js";
|
|
33
|
+
export { BudgetGuard, SPEND_KINDS, describeOverrun, minLimits, toLimits } from "./budget.js";
|
|
34
34
|
export { resolveNodePermissions, buildNodePolicy } from "./node-policy.js";
|
|
35
35
|
export { applySafeOutputs, createEmitOutputTool, resolveActor, EMIT_OUTPUT_TOOL } from "./safe-outputs.js";
|
|
36
36
|
// Agent harness (the seam) and its adapters: Claude Code, Codex and pi
|
|
37
|
-
export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness, CodexHarness, PiHarness, SUPPORTED_AGENTS, } from "./harness/index.js";
|
|
37
|
+
export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness, CodexHarness, PiHarness, SUPPORTED_AGENTS, reportCompletionUsage, } from "./harness/index.js";
|
|
38
38
|
// Claude client (back-compat: ClaudeClient is ClaudeCodeHarness, @deprecated)
|
|
39
39
|
export { ClaudeClient, resolveAuthEnv } from "./harness/claude-code.js";
|
|
40
40
|
// Execution model resolution
|
package/dist/journal.d.ts
CHANGED
|
@@ -53,6 +53,9 @@
|
|
|
53
53
|
* route the edge taken after the visit (null = the run ended), and the rung that chose it
|
|
54
54
|
* usage spend of one agent attempt (cumulative; one per model turn that raised it, written
|
|
55
55
|
* synchronously, and the final figure when the attempt returns)
|
|
56
|
+
* meter spend of one of the executor's own completions (judge, route, reflect, screen),
|
|
57
|
+
* with a unique call id, written when it is counted. Judge, reflect and screen
|
|
58
|
+
* sit inside the visit; route sits after its node:end, before its route record
|
|
56
59
|
* agent:unsandboxed the visit's agent ran without an enforced sandbox
|
|
57
60
|
* run:end success | failed | crashed
|
|
58
61
|
*
|
|
@@ -61,8 +64,9 @@
|
|
|
61
64
|
* the node with the reason, so a resume never under-counts spend or repeats a
|
|
62
65
|
* write the journal did not see.
|
|
63
66
|
*
|
|
64
|
-
* Spend: a resume seeds the run budget with every attempt's journaled usage
|
|
65
|
-
*
|
|
67
|
+
* Spend: a resume seeds the run budget with every attempt's journaled usage
|
|
68
|
+
* and every metered completion, by kind, from authenticated records only, so
|
|
69
|
+
* a crash never resets the whole-run ceiling.
|
|
66
70
|
*
|
|
67
71
|
* Recovery: an unterminated tail that is not complete JSON (power loss
|
|
68
72
|
* mid-append) is truncated. An unterminated tail that IS complete JSON is
|
|
@@ -99,9 +103,9 @@
|
|
|
99
103
|
* `--allow-repeat-writes` does not override a refusal: its outcome is known,
|
|
100
104
|
* not unconfirmed.
|
|
101
105
|
*/
|
|
102
|
-
import type { Logger, NodeResult, NodeUsage, RouteRung, Skill, Workflow } from "./types.js";
|
|
106
|
+
import type { Logger, NodeResult, NodeUsage, RouteRung, Skill, ToolContext, Workflow } from "./types.js";
|
|
103
107
|
import type { SafeOutputIntent, WriteStageState } from "./safe-outputs.js";
|
|
104
|
-
import { type Spend } from "./budget.js";
|
|
108
|
+
import { type SeedSpend, type Spend, type SpendKind } from "./budget.js";
|
|
105
109
|
import type { DeciderCounters } from "./decider.js";
|
|
106
110
|
/**
|
|
107
111
|
* v3: each record's HMAC chains the run id and the previous record's HMAC.
|
|
@@ -386,8 +390,14 @@ export interface ResumePlan {
|
|
|
386
390
|
receipts: Map<string, OutputRecord>;
|
|
387
391
|
/** Writes whose handler refused (`output:refused`, #544): replayed on resume, never taken for applied. */
|
|
388
392
|
refusals: Map<string, OutputRecord>;
|
|
389
|
-
/** Spend every earlier attempt journaled: the run budget starts here on resume. */
|
|
390
|
-
priorSpend:
|
|
393
|
+
/** Spend every earlier attempt and metered completion journaled, by kind: the run budget starts here on resume. */
|
|
394
|
+
priorSpend: SeedSpend;
|
|
395
|
+
/**
|
|
396
|
+
* Spend journaled per visit (`node#iteration`): its agent attempts and its
|
|
397
|
+
* judge, reflect and screen calls, every segment. A checkpointed visit's
|
|
398
|
+
* node budget resumes from it.
|
|
399
|
+
*/
|
|
400
|
+
visitSpend: Map<string, Spend>;
|
|
391
401
|
/**
|
|
392
402
|
* Nodes whose agent ran without an enforced sandbox in an earlier attempt.
|
|
393
403
|
* Such an agent could read the run key, so the journal's spend and write
|
|
@@ -402,7 +412,10 @@ export interface ResumePlan {
|
|
|
402
412
|
* first; nothing but run:resume after run:end; a visit starts only where the
|
|
403
413
|
* last route pointed (or the entry), or re-starts an unfinished or failed
|
|
404
414
|
* visit after a resume; checkpoints, writes, usage and node:end only inside
|
|
405
|
-
* the open visit; one route per finished visit, from that visit's node.
|
|
415
|
+
* the open visit; one route per finished visit, from that visit's node. A
|
|
416
|
+
* judge, reflect or screen meter record sits inside the open visit; a route
|
|
417
|
+
* meter record after the visit's node:end and before its route record (the
|
|
418
|
+
* route call is made then). Every meter record has its own call id.
|
|
406
419
|
* Throws {@link JournalIntegrityError}.
|
|
407
420
|
*/
|
|
408
421
|
export declare function checkRecordSequence(records: JournalRecord[]): void;
|
|
@@ -463,6 +476,7 @@ export type JournalReplay = {
|
|
|
463
476
|
rung?: RouteRung;
|
|
464
477
|
} | ({
|
|
465
478
|
kind: "checkpoint";
|
|
479
|
+
spent?: Spend;
|
|
466
480
|
} & JournalCheckpoint);
|
|
467
481
|
/**
|
|
468
482
|
* The executor's view of a journal (`ExecuteOptions.journal`). A hook that
|
|
@@ -477,6 +491,15 @@ export interface ExecutionJournal {
|
|
|
477
491
|
checkpoint(node: string, iteration: number, cp: JournalCheckpoint): void;
|
|
478
492
|
/** The skill map the write stage applies through: write tools journaled and made idempotent. */
|
|
479
493
|
wrapWrites(node: string, iteration: number, skills: Map<string, Skill>): Map<string, Skill>;
|
|
494
|
+
/** Spend an earlier segment journaled for this visit (resume): its node budget starts there. */
|
|
495
|
+
visitSpent?(node: string, iteration: number): Spend | undefined;
|
|
496
|
+
/**
|
|
497
|
+
* Resume: whether this write was applied before the crash, from its receipt
|
|
498
|
+
* or, for an outstanding intent, the provider's read-only probes (a found
|
|
499
|
+
* write is recorded as recovered). `unknown` when the probe could not tell
|
|
500
|
+
* and repeat writes are not allowed.
|
|
501
|
+
*/
|
|
502
|
+
reconcile?(node: string, iteration: number, skill: Skill, tool: string, args: unknown, ctx: ToolContext): Promise<"applied" | "unknown" | "absent">;
|
|
480
503
|
nodeEnd(node: string, iteration: number, result: NodeResult, writeState: WriteStageState): void;
|
|
481
504
|
/** The edge taken after a visit, and for a decision the rung that chose it (#357), so resume replays it. */
|
|
482
505
|
route(from: string, to: string | null, rung?: RouteRung, decider?: DeciderCounters): void;
|
|
@@ -485,10 +508,16 @@ export interface ExecutionJournal {
|
|
|
485
508
|
* attempt returned, otherwise a live report (written at once when it raises the spend).
|
|
486
509
|
*/
|
|
487
510
|
usage?(node: string, iteration: number, attempt: number, usage: NodeUsage, final: boolean): void;
|
|
511
|
+
/**
|
|
512
|
+
* Spend of one of the executor's own completions (judge, route, reflect,
|
|
513
|
+
* screen), written when it is counted. `node`/`iteration` name the visit it
|
|
514
|
+
* belongs to; for a route call, the visit being routed from.
|
|
515
|
+
*/
|
|
516
|
+
meter?(kind: Exclude<SpendKind, "node">, node: string, iteration: number, usage: NodeUsage): void;
|
|
488
517
|
/** The visit's agent ran without an enforced sandbox: recorded so a resume can say so. */
|
|
489
518
|
uncontained?(node: string, iteration: number): void;
|
|
490
|
-
/** Spend earlier attempts of this run already journaled (resume); the run budget starts from it. */
|
|
491
|
-
priorSpend?():
|
|
519
|
+
/** Spend earlier attempts of this run already journaled (resume), by kind when known; the run budget starts from it. */
|
|
520
|
+
priorSpend?(): SeedSpend | undefined;
|
|
492
521
|
/** Receipt `degraded` entries this journal adds to the run (e.g. `journal_unsandboxed`). */
|
|
493
522
|
degraded?(): string[];
|
|
494
523
|
/** The decider state earlier attempts of this run journaled (resume); undefined otherwise. */
|
|
@@ -607,12 +636,21 @@ export declare class RunJournal implements ExecutionJournal {
|
|
|
607
636
|
* at most the turn it was in.
|
|
608
637
|
*/
|
|
609
638
|
usage(node: string, iteration: number, attempt: number, usage: NodeUsage, final: boolean): void;
|
|
639
|
+
/** Journal one metered completion, synchronously, before the executor acts on it. */
|
|
640
|
+
meter(kind: Exclude<SpendKind, "node">, node: string, iteration: number, usage: NodeUsage): void;
|
|
610
641
|
uncontained(node: string, iteration: number): void;
|
|
611
642
|
degraded(): string[];
|
|
612
|
-
priorSpend():
|
|
643
|
+
priorSpend(): SeedSpend | undefined;
|
|
613
644
|
/** Close the run: `run:end` (when the journal still works), then release the lock. Never throws. */
|
|
614
645
|
end(status: "success" | "failed" | "crashed"): void;
|
|
615
646
|
wrapWrites(node: string, iteration: number, skills: Map<string, Skill>): Map<string, Skill>;
|
|
647
|
+
visitSpent(node: string, iteration: number): Spend | undefined;
|
|
648
|
+
/** Outputs of writes {@link reconcile} found on the provider, replayed by the write handler. */
|
|
649
|
+
private recoveredOutputs;
|
|
650
|
+
/** Provider probes already made for a write key: the handler reuses reconcile's, never probes twice. */
|
|
651
|
+
private probes;
|
|
652
|
+
reconcile(node: string, iteration: number, skill: Skill, tool: string, args: unknown, ctx: ToolContext): Promise<"applied" | "unknown" | "absent">;
|
|
653
|
+
private probe;
|
|
616
654
|
private wrapTool;
|
|
617
655
|
}
|
|
618
656
|
export {};
|
package/dist/journal.js
CHANGED
|
@@ -53,6 +53,9 @@
|
|
|
53
53
|
* route the edge taken after the visit (null = the run ended), and the rung that chose it
|
|
54
54
|
* usage spend of one agent attempt (cumulative; one per model turn that raised it, written
|
|
55
55
|
* synchronously, and the final figure when the attempt returns)
|
|
56
|
+
* meter spend of one of the executor's own completions (judge, route, reflect, screen),
|
|
57
|
+
* with a unique call id, written when it is counted. Judge, reflect and screen
|
|
58
|
+
* sit inside the visit; route sits after its node:end, before its route record
|
|
56
59
|
* agent:unsandboxed the visit's agent ran without an enforced sandbox
|
|
57
60
|
* run:end success | failed | crashed
|
|
58
61
|
*
|
|
@@ -61,8 +64,9 @@
|
|
|
61
64
|
* the node with the reason, so a resume never under-counts spend or repeats a
|
|
62
65
|
* write the journal did not see.
|
|
63
66
|
*
|
|
64
|
-
* Spend: a resume seeds the run budget with every attempt's journaled usage
|
|
65
|
-
*
|
|
67
|
+
* Spend: a resume seeds the run budget with every attempt's journaled usage
|
|
68
|
+
* and every metered completion, by kind, from authenticated records only, so
|
|
69
|
+
* a crash never resets the whole-run ceiling.
|
|
66
70
|
*
|
|
67
71
|
* Recovery: an unterminated tail that is not complete JSON (power loss
|
|
68
72
|
* mid-append) is truncated. An unterminated tail that IS complete JSON is
|
|
@@ -917,9 +921,12 @@ const RECORD_TYPES = new Set([
|
|
|
917
921
|
"node:end",
|
|
918
922
|
"route",
|
|
919
923
|
"usage",
|
|
924
|
+
"meter",
|
|
920
925
|
"agent:unsandboxed",
|
|
921
926
|
"run:end",
|
|
922
927
|
]);
|
|
928
|
+
/** The executor's own completions a `meter` record can name. */
|
|
929
|
+
const METER_KINDS = new Set(["judge", "route", "reflect", "screen"]);
|
|
923
930
|
const isPosInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
|
|
924
931
|
const isName = (v) => typeof v === "string" && v.length > 0;
|
|
925
932
|
const isAmount = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
@@ -935,7 +942,10 @@ const isCounters = (v) => typeof v === "object" &&
|
|
|
935
942
|
* first; nothing but run:resume after run:end; a visit starts only where the
|
|
936
943
|
* last route pointed (or the entry), or re-starts an unfinished or failed
|
|
937
944
|
* visit after a resume; checkpoints, writes, usage and node:end only inside
|
|
938
|
-
* the open visit; one route per finished visit, from that visit's node.
|
|
945
|
+
* the open visit; one route per finished visit, from that visit's node. A
|
|
946
|
+
* judge, reflect or screen meter record sits inside the open visit; a route
|
|
947
|
+
* meter record after the visit's node:end and before its route record (the
|
|
948
|
+
* route call is made then). Every meter record has its own call id.
|
|
939
949
|
* Throws {@link JournalIntegrityError}.
|
|
940
950
|
*/
|
|
941
951
|
export function checkRecordSequence(records) {
|
|
@@ -949,6 +959,7 @@ export function checkRecordSequence(records) {
|
|
|
949
959
|
const visits = new Map();
|
|
950
960
|
const checkpointed = new Set();
|
|
951
961
|
const intentKeys = new Set();
|
|
962
|
+
const meterCalls = new Set();
|
|
952
963
|
const inVisit = (r) => {
|
|
953
964
|
if (!cur || cur.ended)
|
|
954
965
|
bad(r, "is outside any open node visit");
|
|
@@ -1029,6 +1040,25 @@ export function checkRecordSequence(records) {
|
|
|
1029
1040
|
bad(r, "has invalid amounts");
|
|
1030
1041
|
}
|
|
1031
1042
|
break;
|
|
1043
|
+
case "meter": {
|
|
1044
|
+
if (!METER_KINDS.has(r.kind))
|
|
1045
|
+
bad(r, "has an unknown kind");
|
|
1046
|
+
if (!isAmount(r.tokens) || !isAmount(r.cost_usd))
|
|
1047
|
+
bad(r, "has invalid amounts");
|
|
1048
|
+
if (!isName(r.call) || meterCalls.has(r.call))
|
|
1049
|
+
bad(r, "has no call id, or one already used");
|
|
1050
|
+
meterCalls.add(r.call);
|
|
1051
|
+
if (r.kind !== "route") {
|
|
1052
|
+
inVisit(r);
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
if (!cur || !cur.ended || cur.routed)
|
|
1056
|
+
bad(r, "meters a route call outside a finished, unrouted visit");
|
|
1057
|
+
if (r.node !== cur.node || r.iteration !== cur.iteration) {
|
|
1058
|
+
bad(r, `names ${String(r.node)}#${String(r.iteration)}, but the visit being routed is ${cur.node}#${cur.iteration}`);
|
|
1059
|
+
}
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1032
1062
|
case "agent:unsandboxed":
|
|
1033
1063
|
inVisit(r);
|
|
1034
1064
|
break;
|
|
@@ -1102,10 +1132,18 @@ export function checkJournalAgainstWorkflow(records, workflow) {
|
|
|
1102
1132
|
*/
|
|
1103
1133
|
function journaledSpend(records) {
|
|
1104
1134
|
const perAttempt = new Map();
|
|
1135
|
+
const metered = new Map();
|
|
1105
1136
|
let segment = 1;
|
|
1106
1137
|
for (const r of records) {
|
|
1107
1138
|
if (r.type === "run:resume")
|
|
1108
1139
|
segment++;
|
|
1140
|
+
if (r.type === "meter") {
|
|
1141
|
+
// One record per completion, each counted once (call ids are unique).
|
|
1142
|
+
const kind = r.kind;
|
|
1143
|
+
const prev = metered.get(kind) ?? { tokens: 0, costUsd: 0 };
|
|
1144
|
+
metered.set(kind, { tokens: prev.tokens + r.tokens, costUsd: prev.costUsd + r.cost_usd });
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1109
1147
|
if (r.type !== "usage")
|
|
1110
1148
|
continue;
|
|
1111
1149
|
const k = `${segment}|${visitKey(r.node, r.iteration)}|${String(r.attempt)}`;
|
|
@@ -1115,12 +1153,50 @@ function journaledSpend(records) {
|
|
|
1115
1153
|
costUsd: Math.max(prev.costUsd, r.cost_usd),
|
|
1116
1154
|
});
|
|
1117
1155
|
}
|
|
1118
|
-
const
|
|
1156
|
+
const node = { tokens: 0, costUsd: 0 };
|
|
1119
1157
|
for (const s of perAttempt.values()) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1158
|
+
node.tokens += s.tokens;
|
|
1159
|
+
node.costUsd += s.costUsd;
|
|
1160
|
+
}
|
|
1161
|
+
if (metered.size === 0)
|
|
1162
|
+
return node;
|
|
1163
|
+
const byKind = { node: { ...node } };
|
|
1164
|
+
const total = { ...node };
|
|
1165
|
+
for (const [k, v] of metered) {
|
|
1166
|
+
byKind[k] = v;
|
|
1167
|
+
total.tokens += v.tokens;
|
|
1168
|
+
total.costUsd += v.costUsd;
|
|
1169
|
+
}
|
|
1170
|
+
return { ...total, byKind };
|
|
1171
|
+
}
|
|
1172
|
+
/** Per visit: the largest usage of each agent attempt (per segment) plus its own judge, reflect and screen meters. */
|
|
1173
|
+
function visitSpendOf(records) {
|
|
1174
|
+
const attempts = new Map();
|
|
1175
|
+
const out = new Map();
|
|
1176
|
+
const add = (visit, s) => {
|
|
1177
|
+
const prev = out.get(visit) ?? { tokens: 0, costUsd: 0 };
|
|
1178
|
+
out.set(visit, { tokens: prev.tokens + s.tokens, costUsd: prev.costUsd + s.costUsd });
|
|
1179
|
+
};
|
|
1180
|
+
let segment = 1;
|
|
1181
|
+
for (const r of records) {
|
|
1182
|
+
if (r.type === "run:resume")
|
|
1183
|
+
segment++;
|
|
1184
|
+
const visit = visitKey(r.node, r.iteration);
|
|
1185
|
+
if (r.type === "meter" && r.kind !== "route") {
|
|
1186
|
+
add(visit, { tokens: r.tokens, costUsd: r.cost_usd });
|
|
1187
|
+
}
|
|
1188
|
+
else if (r.type === "usage") {
|
|
1189
|
+
const k = `${segment}|${visit}|${String(r.attempt)}`;
|
|
1190
|
+
const prev = attempts.get(k)?.s ?? { tokens: 0, costUsd: 0 };
|
|
1191
|
+
attempts.set(k, {
|
|
1192
|
+
visit,
|
|
1193
|
+
s: { tokens: Math.max(prev.tokens, r.tokens), costUsd: Math.max(prev.costUsd, r.cost_usd) },
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1122
1196
|
}
|
|
1123
|
-
|
|
1197
|
+
for (const { visit, s } of attempts.values())
|
|
1198
|
+
add(visit, s);
|
|
1199
|
+
return out;
|
|
1124
1200
|
}
|
|
1125
1201
|
/** Work out what a resume replays and where it picks up. Throws {@link JournalIntegrityError} for an impossible sequence. */
|
|
1126
1202
|
export function buildResumePlan(records) {
|
|
@@ -1268,6 +1344,7 @@ export function buildResumePlan(records) {
|
|
|
1268
1344
|
receipts,
|
|
1269
1345
|
refusals,
|
|
1270
1346
|
priorSpend: journaledSpend(records),
|
|
1347
|
+
visitSpend: visitSpendOf(records),
|
|
1271
1348
|
unsandboxed: [...new Set(records.filter((r) => r.type === "agent:unsandboxed").map((r) => String(r.node)))],
|
|
1272
1349
|
...(start.decider
|
|
1273
1350
|
? { decider: { ...start.decider, ...(deciderCounters ? { counters: { ...deciderCounters } } : {}) } }
|
|
@@ -1394,13 +1471,19 @@ async function probeProvider(toolName, args, key, skill, ctx, since) {
|
|
|
1394
1471
|
return { state: "applied", output: toolName === "github_create_issue" ? hit : {} };
|
|
1395
1472
|
}
|
|
1396
1473
|
case "linear_create_issue": {
|
|
1397
|
-
const out = (await callRead(skill, "linear_search_issues", { query: token, limit: 5 }, ctx));
|
|
1474
|
+
const out = (await callRead(skill, "linear_search_issues", { query: token, limit: 5, withDescription: true }, ctx));
|
|
1398
1475
|
const nodes = out?.searchIssues?.nodes;
|
|
1399
1476
|
if (!Array.isArray(nodes))
|
|
1400
1477
|
return { state: "unknown", reason: "search returned no result list" };
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1478
|
+
// Search is fuzzy: only an issue whose description carries this write's marker is ours.
|
|
1479
|
+
const hit = nodes.find((n) => typeof n?.description === "string" && n.description.includes(token));
|
|
1480
|
+
if (hit)
|
|
1481
|
+
return { state: "applied", output: { issueCreate: { issue: hit } } };
|
|
1482
|
+
// A hit without a description cannot be checked for the marker: unknown, never assumed either way.
|
|
1483
|
+
if (nodes.some((n) => typeof n?.description !== "string")) {
|
|
1484
|
+
return { state: "unknown", reason: "search hits carry no description to check for the marker" };
|
|
1485
|
+
}
|
|
1486
|
+
return { state: "absent" };
|
|
1404
1487
|
}
|
|
1405
1488
|
case "linear_add_comment": {
|
|
1406
1489
|
const out = (await callRead(skill, "linear_list_comments", { issueId: args.issueId }, ctx));
|
|
@@ -1791,6 +1874,7 @@ export class RunJournal {
|
|
|
1791
1874
|
intents: structuredClone(cp.intents),
|
|
1792
1875
|
agentRunFailed: cp.agent_failed,
|
|
1793
1876
|
attempt: cp.attempt,
|
|
1877
|
+
...(plan.visitSpend.has(k) ? { spent: { ...plan.visitSpend.get(k) } } : {}),
|
|
1794
1878
|
};
|
|
1795
1879
|
}
|
|
1796
1880
|
return undefined;
|
|
@@ -1845,6 +1929,20 @@ export class RunJournal {
|
|
|
1845
1929
|
});
|
|
1846
1930
|
this.append("usage", { node, iteration, attempt, tokens, cost_usd: costUsd, final });
|
|
1847
1931
|
}
|
|
1932
|
+
/** Journal one metered completion, synchronously, before the executor acts on it. */
|
|
1933
|
+
meter(kind, node, iteration, usage) {
|
|
1934
|
+
const s = spendOf(usage);
|
|
1935
|
+
if (s.tokens === undefined && s.costUsd === undefined)
|
|
1936
|
+
return;
|
|
1937
|
+
this.append("meter", {
|
|
1938
|
+
kind,
|
|
1939
|
+
call: crypto.randomUUID(),
|
|
1940
|
+
node,
|
|
1941
|
+
iteration,
|
|
1942
|
+
tokens: s.tokens ?? 0,
|
|
1943
|
+
cost_usd: s.costUsd ?? 0,
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1848
1946
|
uncontained(node, iteration) {
|
|
1849
1947
|
const k = visitKey(node, iteration);
|
|
1850
1948
|
if (this.uncontainedVisits.has(k))
|
|
@@ -1862,7 +1960,16 @@ export class RunJournal {
|
|
|
1862
1960
|
];
|
|
1863
1961
|
}
|
|
1864
1962
|
priorSpend() {
|
|
1865
|
-
|
|
1963
|
+
if (!this.resume)
|
|
1964
|
+
return undefined;
|
|
1965
|
+
const p = this.resume.plan.priorSpend;
|
|
1966
|
+
return {
|
|
1967
|
+
tokens: p.tokens,
|
|
1968
|
+
costUsd: p.costUsd,
|
|
1969
|
+
...(p.byKind
|
|
1970
|
+
? { byKind: Object.fromEntries(Object.entries(p.byKind).map(([k, v]) => [k, { ...v }])) }
|
|
1971
|
+
: {}),
|
|
1972
|
+
};
|
|
1866
1973
|
}
|
|
1867
1974
|
/** Close the run: `run:end` (when the journal still works), then release the lock. Never throws. */
|
|
1868
1975
|
end(status) {
|
|
@@ -1888,12 +1995,56 @@ export class RunJournal {
|
|
|
1888
1995
|
}
|
|
1889
1996
|
return out;
|
|
1890
1997
|
}
|
|
1998
|
+
visitSpent(node, iteration) {
|
|
1999
|
+
const s = this.resume?.plan.visitSpend.get(visitKey(node, iteration));
|
|
2000
|
+
return s ? { ...s } : undefined;
|
|
2001
|
+
}
|
|
2002
|
+
/** Outputs of writes {@link reconcile} found on the provider, replayed by the write handler. */
|
|
2003
|
+
recoveredOutputs = new Map();
|
|
2004
|
+
/** Provider probes already made for a write key: the handler reuses reconcile's, never probes twice. */
|
|
2005
|
+
probes = new Map();
|
|
2006
|
+
async reconcile(node, iteration, skill, tool, args, ctx) {
|
|
2007
|
+
const plan = this.resume?.plan;
|
|
2008
|
+
if (!plan)
|
|
2009
|
+
return "absent";
|
|
2010
|
+
const key = canonicalHash({ node, iteration, tool, args });
|
|
2011
|
+
if (plan.receipts.has(key) || this.recoveredOutputs.has(key))
|
|
2012
|
+
return "applied";
|
|
2013
|
+
const refusal = plan.refusals.get(key);
|
|
2014
|
+
if (refusal && refusal.retry !== true)
|
|
2015
|
+
return "absent";
|
|
2016
|
+
const pending = plan.intents.has(key);
|
|
2017
|
+
if (!pending && !plan.checkpoints.has(visitKey(node, iteration)))
|
|
2018
|
+
return "absent";
|
|
2019
|
+
const probe = await this.probe(key, tool, args, skill, ctx);
|
|
2020
|
+
if (probe.state === "applied") {
|
|
2021
|
+
const output = slimOutput(probe.output);
|
|
2022
|
+
this.append("output:applied", { node, iteration, key, tool, output, recovered: true });
|
|
2023
|
+
this.recoveredOutputs.set(key, output);
|
|
2024
|
+
this.logger.info(` run journal: ${tool} found on the provider; not re-sent`, { node });
|
|
2025
|
+
return "applied";
|
|
2026
|
+
}
|
|
2027
|
+
return probe.state === "unknown" && pending && !this.resume?.allowRepeatWrites ? "unknown" : "absent";
|
|
2028
|
+
}
|
|
2029
|
+
probe(key, tool, args, skill, ctx) {
|
|
2030
|
+
const cached = this.probes.get(key);
|
|
2031
|
+
if (cached)
|
|
2032
|
+
return cached;
|
|
2033
|
+
// Look back from a little before the intent (clock skew between this host and the provider).
|
|
2034
|
+
const intentAt = Date.parse(String(this.resume?.plan.intents.get(key)?.at ?? ""));
|
|
2035
|
+
const since = Number.isFinite(intentAt) ? new Date(intentAt - 5 * 60_000).toISOString() : undefined;
|
|
2036
|
+
const p = probeProvider(tool, (args ?? {}), key, skill, ctx, since);
|
|
2037
|
+
this.probes.set(key, p);
|
|
2038
|
+
return p;
|
|
2039
|
+
}
|
|
1891
2040
|
wrapTool(node, iteration, skill, tool) {
|
|
1892
2041
|
return {
|
|
1893
2042
|
...tool,
|
|
1894
2043
|
handler: async (args, ctx) => {
|
|
1895
2044
|
const key = canonicalHash({ node, iteration, tool: tool.name, args });
|
|
1896
2045
|
const plan = this.resume?.plan;
|
|
2046
|
+
if (this.recoveredOutputs.has(key))
|
|
2047
|
+
return this.recoveredOutputs.get(key) ?? null;
|
|
1897
2048
|
const receipt = plan?.receipts.get(key);
|
|
1898
2049
|
if (receipt) {
|
|
1899
2050
|
this.logger.info(` run journal: ${tool.name} already applied before the crash; not re-sent`, { node });
|
|
@@ -1908,10 +2059,7 @@ export class RunJournal {
|
|
|
1908
2059
|
const replayingStage = !!plan?.checkpoints.has(visitKey(node, iteration));
|
|
1909
2060
|
const pending = plan?.intents.has(key) === true;
|
|
1910
2061
|
if (pending || replayingStage) {
|
|
1911
|
-
|
|
1912
|
-
const intentAt = Date.parse(String(plan?.intents.get(key)?.at ?? ""));
|
|
1913
|
-
const since = Number.isFinite(intentAt) ? new Date(intentAt - 5 * 60_000).toISOString() : undefined;
|
|
1914
|
-
const probe = await probeProvider(tool.name, (args ?? {}), key, skill, ctx, since);
|
|
2062
|
+
const probe = await this.probe(key, tool.name, args, skill, ctx);
|
|
1915
2063
|
if (probe.state === "applied") {
|
|
1916
2064
|
const output = slimOutput(probe.output);
|
|
1917
2065
|
this.append("output:applied", { node, iteration, key, tool: tool.name, output, recovered: true });
|