@open-cr-agent/core 0.1.1 → 0.1.2
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.
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Usage } from "../contracts.js";
|
|
2
2
|
export declare const REVIEW_BUDGET_SHARE = 0.8;
|
|
3
|
+
export declare class SpendLimitReached extends Error {
|
|
4
|
+
constructor(maxCostUsd: number);
|
|
5
|
+
}
|
|
3
6
|
export interface SpendTracker {
|
|
4
7
|
reviewExhausted(): boolean;
|
|
5
8
|
exhausted(): boolean;
|
package/dist/pipeline/budget.js
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
// and Judge, so the run that hits the limit still gets its findings checked.
|
|
3
3
|
// Those stages are one short call per file with findings plus one top-tier
|
|
4
4
|
// call, while every agent step resends a task's whole conversation, so a fifth
|
|
5
|
-
// covers them in typical runs.
|
|
6
|
-
//
|
|
5
|
+
// covers them in typical runs. Review tasks still running when their share
|
|
6
|
+
// runs out are stopped; what they spend while stopping (the step in flight,
|
|
7
|
+
// and whatever the runtime had not reported yet) comes out of the reserve.
|
|
7
8
|
export const REVIEW_BUDGET_SHARE = 0.8;
|
|
9
|
+
// Why running review tasks were stopped.
|
|
10
|
+
export class SpendLimitReached extends Error {
|
|
11
|
+
constructor(maxCostUsd) {
|
|
12
|
+
super(`stopped at the spend limit of $${maxCostUsd}`);
|
|
13
|
+
this.name = "SpendLimitReached";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
8
16
|
export function spendTracker(maxCostUsd, initial) {
|
|
9
17
|
let spent = initial.reduce((sum, u) => sum + u.costUsd, 0);
|
|
10
18
|
const over = (limit) => maxCostUsd !== undefined && spent >= limit;
|
|
@@ -19,6 +19,7 @@ export interface ExecuteOptions {
|
|
|
19
19
|
relocate?: AnchorContext["relocate"] | undefined;
|
|
20
20
|
plans?: Map<string, Promise<PlannedBundle>>;
|
|
21
21
|
emit: (event: ReviewEvent) => void;
|
|
22
|
+
onUsage?: ((usage: Usage) => void) | undefined;
|
|
22
23
|
signal: AbortSignal;
|
|
23
24
|
}
|
|
24
25
|
type PlannedBundle = Awaited<ReturnType<typeof planBundle>>;
|
package/dist/pipeline/execute.js
CHANGED
|
@@ -43,6 +43,8 @@ export async function runJob(job, plan, options) {
|
|
|
43
43
|
// The sample that made the call pays for it and reports its warning.
|
|
44
44
|
if (!shared) {
|
|
45
45
|
extraUsage.push(...planned.usage);
|
|
46
|
+
for (const usage of planned.usage)
|
|
47
|
+
options.onUsage?.(usage);
|
|
46
48
|
if (planned.warning)
|
|
47
49
|
extraWarnings.push(planned.warning);
|
|
48
50
|
}
|
|
@@ -66,6 +68,7 @@ export async function runJob(job, plan, options) {
|
|
|
66
68
|
timeoutMs: options.taskTimeoutMs,
|
|
67
69
|
}, options.signal, {
|
|
68
70
|
onProgress: (message) => emit({ type: "task_progress", taskId: job.taskId, message }),
|
|
71
|
+
onUsage: options.onUsage,
|
|
69
72
|
category: job.reviewer.category,
|
|
70
73
|
abortGraceMs: options.abortGraceMs,
|
|
71
74
|
});
|
package/dist/pipeline/run.js
CHANGED
|
@@ -6,7 +6,7 @@ import { priorCodePresence } from "../rereview/presence.js";
|
|
|
6
6
|
import { reconcile, stillOpen } from "../rereview/reconcile.js";
|
|
7
7
|
import { correctnessReviewer } from "../review/reviewers/correctness.js";
|
|
8
8
|
import { markUnchecked, verifyFindings } from "../verify/verify.js";
|
|
9
|
-
import { spendTracker } from "./budget.js";
|
|
9
|
+
import { SpendLimitReached, spendTracker } from "./budget.js";
|
|
10
10
|
import { runJob } from "./execute.js";
|
|
11
11
|
import { dedupeFindings } from "./findings.js";
|
|
12
12
|
import { DEFAULT_MAX_TASKS, planTasks } from "./matrix.js";
|
|
@@ -52,6 +52,15 @@ export async function runReview(options) {
|
|
|
52
52
|
relocationUsage.push(u);
|
|
53
53
|
budget.add(u);
|
|
54
54
|
}));
|
|
55
|
+
// Tasks report spend while they run, so the one that uses up the review
|
|
56
|
+
// share stops every task still running, not only the ones not yet started.
|
|
57
|
+
const spendLimit = new AbortController();
|
|
58
|
+
const spend = (usage) => {
|
|
59
|
+
budget.add(usage);
|
|
60
|
+
if (budget.reviewExhausted() && !spendLimit.signal.aborted) {
|
|
61
|
+
spendLimit.abort(new SpendLimitReached(options.maxCostUsd ?? 0));
|
|
62
|
+
}
|
|
63
|
+
};
|
|
55
64
|
const execute = {
|
|
56
65
|
runtime: options.runtime,
|
|
57
66
|
taskTimeoutMs: options.taskTimeoutMs ?? DEFAULTS.taskTimeoutMs,
|
|
@@ -62,7 +71,8 @@ export async function runReview(options) {
|
|
|
62
71
|
ultra: options.ultra === true,
|
|
63
72
|
plans: new Map(),
|
|
64
73
|
emit,
|
|
65
|
-
|
|
74
|
+
onUsage: spend,
|
|
75
|
+
signal: AbortSignal.any([signal, spendLimit.signal]),
|
|
66
76
|
};
|
|
67
77
|
const results = await mapWithConcurrency(matrix.cells, options.concurrency ?? DEFAULTS.concurrency, async (cell) => {
|
|
68
78
|
// Once the run is cancelled or timed out, remaining cells are not started.
|
|
@@ -71,9 +81,7 @@ export async function runReview(options) {
|
|
|
71
81
|
if (budget.reviewExhausted()) {
|
|
72
82
|
return skipCell(cell, `spend limit of $${options.maxCostUsd} reached`, emit);
|
|
73
83
|
}
|
|
74
|
-
|
|
75
|
-
budget.add(result.usage);
|
|
76
|
-
return result;
|
|
84
|
+
return runJob(cell, plan, execute);
|
|
77
85
|
});
|
|
78
86
|
// Memory and the previous review filter first, so Verify and Judge are not
|
|
79
87
|
// paid for findings that will not be reported, and a person's dismissal
|
package/dist/pipeline/task.d.ts
CHANGED
package/dist/pipeline/task.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { reportedFindingSchema } from "../domain.js";
|
|
2
2
|
import { errorMessage } from "../errors.js";
|
|
3
|
+
import { SpendLimitReached } from "./budget.js";
|
|
3
4
|
import { addUsage, emptyUsage } from "./usage.js";
|
|
4
5
|
export async function executeTask(runtime, spec, runSignal, callbacks) {
|
|
5
6
|
const timeout = AbortSignal.timeout(spec.timeoutMs);
|
|
@@ -30,9 +31,20 @@ export async function executeTask(runtime, spec, runSignal, callbacks) {
|
|
|
30
31
|
if (signal.aborted) {
|
|
31
32
|
const timedOut = timeout.aborted && !runSignal.aborted;
|
|
32
33
|
result.status = timedOut ? "timed_out" : "cancelled";
|
|
33
|
-
result.error = timedOut
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
result.error = timedOut
|
|
35
|
+
? `timed out after ${spec.timeoutMs}ms`
|
|
36
|
+
: runSignal.reason instanceof SpendLimitReached
|
|
37
|
+
? runSignal.reason.message
|
|
38
|
+
: "run cancelled";
|
|
39
|
+
const ending = iterator
|
|
40
|
+
? await collectAfterAbort(iterator, pending, result, callbacks)
|
|
41
|
+
: undefined;
|
|
42
|
+
// The task's last usage report crossed the limit, and it finished
|
|
43
|
+
// anyway: its review is complete.
|
|
44
|
+
if (ending === "done" && runSignal.reason instanceof SpendLimitReached) {
|
|
45
|
+
result.status = "completed";
|
|
46
|
+
delete result.error;
|
|
47
|
+
}
|
|
36
48
|
void iterator?.return?.();
|
|
37
49
|
}
|
|
38
50
|
else {
|
|
@@ -54,18 +66,19 @@ async function collectAfterAbort(iterator, pending, result, callbacks) {
|
|
|
54
66
|
for (;;) {
|
|
55
67
|
const event = await untilAborted(next, grace);
|
|
56
68
|
if (event.done)
|
|
57
|
-
return;
|
|
69
|
+
return undefined;
|
|
58
70
|
const { value } = event;
|
|
59
71
|
if (value.type === "usage" || value.type === "finding")
|
|
60
72
|
handle(value, result, callbacks);
|
|
61
73
|
if (value.type === "done" || value.type === "error")
|
|
62
|
-
return;
|
|
74
|
+
return value.type;
|
|
63
75
|
next = iterator.next();
|
|
64
76
|
}
|
|
65
77
|
}
|
|
66
78
|
catch {
|
|
67
79
|
// The runtime did not finish within the grace period, or failed: keep
|
|
68
80
|
// what arrived.
|
|
81
|
+
return undefined;
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
84
|
function handle(event, result, callbacks) {
|
|
@@ -88,9 +101,12 @@ function handle(event, result, callbacks) {
|
|
|
88
101
|
}
|
|
89
102
|
return false;
|
|
90
103
|
}
|
|
91
|
-
case "usage":
|
|
92
|
-
|
|
104
|
+
case "usage": {
|
|
105
|
+
const { type: _type, taskId: _taskId, ...usage } = event;
|
|
106
|
+
result.usage = addUsage(result.usage, usage);
|
|
107
|
+
callbacks.onUsage?.(usage);
|
|
93
108
|
return false;
|
|
109
|
+
}
|
|
94
110
|
case "done":
|
|
95
111
|
return true;
|
|
96
112
|
case "error":
|