@cruxy/cli 0.28.2 → 0.29.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.
- package/dist/agent/budget.d.ts +52 -0
- package/dist/{subagent → agent}/budget.js +2 -8
- package/dist/agent/loop.d.ts +15 -0
- package/dist/agent/loop.js +11 -2
- package/dist/agent/session.d.ts +32 -7
- package/dist/agent/session.js +89 -28
- package/dist/cli/commands/run.js +21 -2
- package/dist/config/schema.d.ts +88 -2
- package/dist/config/schema.js +35 -1
- package/dist/errors/constructors.js +1 -1
- package/dist/jobs/manager.js +2 -1
- package/dist/subagent/index.d.ts +0 -1
- package/dist/subagent/index.js +0 -1
- package/dist/subagent/orchestrator.js +1 -1
- package/dist/subagent/types.d.ts +2 -13
- package/package.json +2 -2
- package/dist/subagent/budget.d.ts +0 -34
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Usage } from "@cruxy/sdk";
|
|
2
|
+
import type { LoopBudget } from "./loop.js";
|
|
3
|
+
/**
|
|
4
|
+
* The live budget primitive for the agent loop: iteration + token + optional
|
|
5
|
+
* wall-clock caps, checked by {@link runAgent} before every model turn (see
|
|
6
|
+
* `LoopBudget`). A tripped cap stops the run with a human-readable reason and
|
|
7
|
+
* the coherent partial history — the loop never runs unbounded.
|
|
8
|
+
*
|
|
9
|
+
* It lives beside the loop it guards: the loop defines the `LoopBudget` seam,
|
|
10
|
+
* this is its one concrete implementation, and the subagent orchestrator, the
|
|
11
|
+
* job manager, and {@link Session} (per-turn token guard) all consume it.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Hard caps a run executes under. `maxTokens` is always finite; `maxIterations`
|
|
15
|
+
* is finite for a subagent (bounded by construction) but may be
|
|
16
|
+
* `Number.POSITIVE_INFINITY` for a token-only guard where iteration count is
|
|
17
|
+
* bounded elsewhere (a main turn is already capped by `agent.maxIterations` in
|
|
18
|
+
* the loop). `timeoutMs` is an optional wall-clock backstop on top.
|
|
19
|
+
*/
|
|
20
|
+
export interface BudgetLimits {
|
|
21
|
+
/** Cap on the run's model turns (may be `Infinity` for a token-only guard). */
|
|
22
|
+
maxIterations: number;
|
|
23
|
+
/** Cap on the run's combined input+output tokens. */
|
|
24
|
+
maxTokens: number;
|
|
25
|
+
/** Optional wall-clock cap in milliseconds. */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the effective limits for one spawn: start from the configured
|
|
30
|
+
* ceilings and let overrides only *narrow* them. A request above a ceiling is
|
|
31
|
+
* clamped down, not honored — "budget overrides within limits" by construction.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveBudget(defaults: BudgetLimits, overrides?: Partial<BudgetLimits>): BudgetLimits;
|
|
34
|
+
/**
|
|
35
|
+
* A live budget for one run. The wall clock starts at construction (spawn /
|
|
36
|
+
* turn start); the clock source is injectable so tests never sleep.
|
|
37
|
+
*/
|
|
38
|
+
export declare class Budget implements LoopBudget {
|
|
39
|
+
private readonly limits;
|
|
40
|
+
private readonly now;
|
|
41
|
+
private readonly startedAt;
|
|
42
|
+
constructor(limits: BudgetLimits, now?: () => number);
|
|
43
|
+
/**
|
|
44
|
+
* The reason to stop before the next model turn, or `null` to continue.
|
|
45
|
+
* Checked at iteration boundaries — the in-flight turn always completes, so
|
|
46
|
+
* overshoot is bounded by one turn.
|
|
47
|
+
*/
|
|
48
|
+
exceeded(state: {
|
|
49
|
+
iterations: number;
|
|
50
|
+
usage: Usage;
|
|
51
|
+
}): string | null;
|
|
52
|
+
}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The subagent budget (C.14): iteration + token + optional wall-clock caps,
|
|
3
|
-
* checked by the agent loop before every model turn (see `LoopBudget`). A
|
|
4
|
-
* tripped cap stops the run with a human-readable reason — the subagent
|
|
5
|
-
* returns a partial result, it never runs unbounded.
|
|
6
|
-
*/
|
|
7
1
|
/**
|
|
8
2
|
* Resolve the effective limits for one spawn: start from the configured
|
|
9
3
|
* ceilings and let overrides only *narrow* them. A request above a ceiling is
|
|
@@ -23,8 +17,8 @@ export function resolveBudget(defaults, overrides) {
|
|
|
23
17
|
};
|
|
24
18
|
}
|
|
25
19
|
/**
|
|
26
|
-
* A live budget for one
|
|
27
|
-
*
|
|
20
|
+
* A live budget for one run. The wall clock starts at construction (spawn /
|
|
21
|
+
* turn start); the clock source is injectable so tests never sleep.
|
|
28
22
|
*/
|
|
29
23
|
export class Budget {
|
|
30
24
|
limits;
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -103,6 +103,21 @@ export interface RunAgentArgs {
|
|
|
103
103
|
* is kill-tree'd rather than orphaned.
|
|
104
104
|
*/
|
|
105
105
|
signal?: AbortSignal;
|
|
106
|
+
/**
|
|
107
|
+
* Mid-loop compaction seam (build item 3). Called at the top of every
|
|
108
|
+
* iteration with the running history; returns the history to continue from —
|
|
109
|
+
* unchanged when under threshold, or with its older prefix summarized away
|
|
110
|
+
* when over. Supplied by {@link Session}, which reuses its own
|
|
111
|
+
* threshold/cut/summarize machinery, so the loop gains no summarization
|
|
112
|
+
* knowledge and no logic is duplicated. Omitted → history is never compacted
|
|
113
|
+
* mid-loop (unchanged behavior; subagents pass nothing).
|
|
114
|
+
*
|
|
115
|
+
* This is what keeps a long autonomous turn — where `send` compacts only once
|
|
116
|
+
* up front and never again — within the context window: without it a run that
|
|
117
|
+
* drives dozens of tool calls (each result appended, some tens of KB) grows
|
|
118
|
+
* unbounded until the next `send`, which in one-shot never comes.
|
|
119
|
+
*/
|
|
120
|
+
compact?: (messages: Message[]) => Promise<Message[]>;
|
|
106
121
|
}
|
|
107
122
|
/**
|
|
108
123
|
* The budget seam for {@link runAgent}: implementations track their own caps
|
package/dist/agent/loop.js
CHANGED
|
@@ -40,8 +40,9 @@ async function driveLoop(args, renderer, routed) {
|
|
|
40
40
|
const { provider, registry, config, ctx } = args;
|
|
41
41
|
const { logger } = ctx;
|
|
42
42
|
// Work on a copy so we never mutate the caller's array as a side effect; the
|
|
43
|
-
// extended history is returned for the caller to adopt.
|
|
44
|
-
|
|
43
|
+
// extended history is returned for the caller to adopt. Reassigned wholesale
|
|
44
|
+
// when the mid-loop compaction seam folds away an older prefix.
|
|
45
|
+
let messages = [...args.messages];
|
|
45
46
|
const usage = { input_tokens: 0, output_tokens: 0 };
|
|
46
47
|
const maxIterations = config.agent.maxIterations;
|
|
47
48
|
// The tool catalogue and environment are stable across the loop, so build the
|
|
@@ -88,6 +89,14 @@ async function driveLoop(args, renderer, routed) {
|
|
|
88
89
|
usage,
|
|
89
90
|
};
|
|
90
91
|
}
|
|
92
|
+
// Mid-loop compaction (build item 3), after the abort/budget checks so a
|
|
93
|
+
// cancelled or over-budget run never spends a summary call: fold away the
|
|
94
|
+
// older prefix when the history has grown past threshold *this* iteration.
|
|
95
|
+
// The seam preserves tool_use/tool_result integrity (it only cuts at a
|
|
96
|
+
// completed turn boundary), so adopting its result mid-turn is safe.
|
|
97
|
+
if (args.compact) {
|
|
98
|
+
messages = await args.compact(messages);
|
|
99
|
+
}
|
|
91
100
|
iterations = i + 1;
|
|
92
101
|
const tools = registry.toToolSpecs();
|
|
93
102
|
// ── Consume one model turn ──────────────────────────────────────────────
|
package/dist/agent/session.d.ts
CHANGED
|
@@ -145,23 +145,46 @@ export declare class Session {
|
|
|
145
145
|
/** Drop the conversation history but keep the session (for `/clear`). */
|
|
146
146
|
clear(): void;
|
|
147
147
|
/**
|
|
148
|
-
* Compact only when
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
148
|
+
* Compact `this.messages` only when it has grown past threshold, adopting the
|
|
149
|
+
* result. On success logs a one-line notice and returns the number of older
|
|
150
|
+
* messages folded into the summary; otherwise returns `null` (under threshold,
|
|
151
|
+
* nothing safe to cut, or summary failed).
|
|
152
152
|
*/
|
|
153
153
|
maybeCompact(onRequestUsage?: (req: RequestUsage) => void): Promise<number | null>;
|
|
154
|
+
/**
|
|
155
|
+
* The mid-loop compaction seam (build item 3) handed to {@link runAgent}: same
|
|
156
|
+
* threshold/cut/summarize path as {@link maybeCompact}, but over the loop's own
|
|
157
|
+
* running history rather than `this.messages` (the loop owns and adopts its
|
|
158
|
+
* copy). Reuses the exact machinery so no summarization logic is duplicated and
|
|
159
|
+
* the loop stays ignorant of it. Under threshold it returns the history
|
|
160
|
+
* unchanged — cheap, no model call.
|
|
161
|
+
*/
|
|
162
|
+
private compactLoopHistory;
|
|
154
163
|
/**
|
|
155
164
|
* Force compaction regardless of the threshold (backs `/compact`). Returns the
|
|
156
165
|
* number of older messages summarized, or `null` if there was nothing safe to
|
|
157
166
|
* cut or the summary call failed.
|
|
158
167
|
*/
|
|
159
168
|
compact(): Promise<number | null>;
|
|
169
|
+
/**
|
|
170
|
+
* Compact `messages` when its estimated footprint exceeds
|
|
171
|
+
* `compactThreshold * maxTokens`, else return it untouched. The estimate adds a
|
|
172
|
+
* fixed `reserveTokens` allowance for the system prompt and tool schemas that
|
|
173
|
+
* {@link estimateTokens} never sees (~4.5k+ tokens of real request payload), so
|
|
174
|
+
* the trigger reflects the actual request size rather than only the visible
|
|
175
|
+
* history — otherwise the loop can sit just under the visible threshold while
|
|
176
|
+
* the real request has already overrun the window. Logs a one-line notice on a
|
|
177
|
+
* successful compaction.
|
|
178
|
+
*/
|
|
179
|
+
private compactIfOverThreshold;
|
|
160
180
|
/**
|
|
161
181
|
* Find a clean cut, summarize the older prefix into a synthetic user/assistant
|
|
162
182
|
* pair, and splice it in front of the kept-recent messages. Best-effort: a
|
|
163
|
-
* failed summary call leaves the history untouched and
|
|
164
|
-
* open — losing compaction is degraded, not unsafe).
|
|
183
|
+
* failed summary call leaves the history untouched and reports `compacted:
|
|
184
|
+
* null` (fail open — losing compaction is degraded, not unsafe). Pure with
|
|
185
|
+
* respect to `this.messages`: it returns the new array for the caller to adopt
|
|
186
|
+
* (the loop and the session each own their own history), and only accumulates
|
|
187
|
+
* the summary's usage into the session total.
|
|
165
188
|
*/
|
|
166
189
|
private runCompaction;
|
|
167
190
|
/**
|
|
@@ -171,7 +194,9 @@ export declare class Session {
|
|
|
171
194
|
* matching `tool_result` (the next user message) must never straddle the cut,
|
|
172
195
|
* or the next provider call breaks. A real user *prompt* (`role:"user"` with
|
|
173
196
|
* string content) only occurs at a completed turn boundary, where every prior
|
|
174
|
-
* tool exchange is already resolved — so the kept region must begin there.
|
|
197
|
+
* tool exchange is already resolved — so the kept region must begin there. The
|
|
198
|
+
* synthetic compaction-summary user message is also string content, so a
|
|
199
|
+
* repeat compaction always finds at least the previous summary as a clean cut.
|
|
175
200
|
*
|
|
176
201
|
* Start from `length - keepRecentMessages` and walk *backwards* to the nearest
|
|
177
202
|
* such prompt: this keeps at least the recent floor and lands clean. Returns
|
package/dist/agent/session.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { loadProjectInstructions } from "../config/index.js";
|
|
3
3
|
import { resolveTaskModel } from "../routing/index.js";
|
|
4
4
|
import { UsageCollector, } from "../usage/index.js";
|
|
5
|
+
import { Budget } from "./budget.js";
|
|
5
6
|
import { runAgent, } from "./loop.js";
|
|
6
7
|
import { SUMMARY_SYSTEM, COMPACTION_MARKER } from "./prompts.js";
|
|
7
8
|
/**
|
|
@@ -115,6 +116,20 @@ export class Session {
|
|
|
115
116
|
const onReq = (req) => collector.record(req);
|
|
116
117
|
// Compact *before* the agent call so the turn runs against a bounded history.
|
|
117
118
|
await this.maybeCompact(onReq);
|
|
119
|
+
// Per-turn runaway guard (build item 4): when `agent.maxTokensPerTurn` is
|
|
120
|
+
// set, hand the loop a fresh token-only budget for THIS turn. Reusing the
|
|
121
|
+
// Budget primitive with an infinite iteration cap makes it purely a token
|
|
122
|
+
// stop — the loop's own `agent.maxIterations` already bounds turn count.
|
|
123
|
+
// Unset/0 → no budget object at all, so the loop's budget check is a no-op
|
|
124
|
+
// and behavior is byte-identical to before. Rebuilt each `send`, so the cap
|
|
125
|
+
// is per-turn, never cumulative across the session (cost is C.22's concern).
|
|
126
|
+
const maxTokensPerTurn = this.args.config.agent.maxTokensPerTurn;
|
|
127
|
+
const budget = maxTokensPerTurn > 0
|
|
128
|
+
? new Budget({
|
|
129
|
+
maxIterations: Number.POSITIVE_INFINITY,
|
|
130
|
+
maxTokens: maxTokensPerTurn,
|
|
131
|
+
})
|
|
132
|
+
: undefined;
|
|
118
133
|
// before-run (C.19): a blocking pre-run hook — or an untrusted project's
|
|
119
134
|
// hooks — throws here and aborts the turn before the model is engaged
|
|
120
135
|
// (fail-closed). No-op when hooks are disabled or none are registered.
|
|
@@ -133,12 +148,21 @@ export class Session {
|
|
|
133
148
|
: await runAgent({
|
|
134
149
|
messages: this.messages,
|
|
135
150
|
...this.args, // carries `router` through to the loop
|
|
151
|
+
// The per-turn token guard (build item 4). After the spread because
|
|
152
|
+
// SessionArgs has no `budget` field — it is a per-turn construction,
|
|
153
|
+
// not session state. `undefined` when the cap is off (no-op check).
|
|
154
|
+
budget,
|
|
136
155
|
taskClass: "main-turn",
|
|
137
156
|
// After the spread so a mid-session `/reload` wins over the initial value.
|
|
138
157
|
projectInstructions: this.projectInstructions,
|
|
139
158
|
planMode: false, // the plan directive belongs only to the runner's propose phase
|
|
140
159
|
renderer,
|
|
141
160
|
onRequestUsage: onReq,
|
|
161
|
+
// Mid-loop compaction (build item 3): let a long autonomous turn
|
|
162
|
+
// compact between iterations, not just once up front. Reuses this
|
|
163
|
+
// session's threshold/cut/summarize path over the loop's own history
|
|
164
|
+
// and attributes the summary usage to this run's collector.
|
|
165
|
+
compact: (messages) => this.compactLoopHistory(messages, onReq),
|
|
142
166
|
});
|
|
143
167
|
this.messages = result.messages;
|
|
144
168
|
this.usage.input_tokens += result.usage.input_tokens;
|
|
@@ -169,21 +193,27 @@ export class Session {
|
|
|
169
193
|
this.messages = [];
|
|
170
194
|
}
|
|
171
195
|
/**
|
|
172
|
-
* Compact only when
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
196
|
+
* Compact `this.messages` only when it has grown past threshold, adopting the
|
|
197
|
+
* result. On success logs a one-line notice and returns the number of older
|
|
198
|
+
* messages folded into the summary; otherwise returns `null` (under threshold,
|
|
199
|
+
* nothing safe to cut, or summary failed).
|
|
176
200
|
*/
|
|
177
201
|
async maybeCompact(onRequestUsage) {
|
|
178
|
-
const {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
202
|
+
const { messages, compacted } = await this.compactIfOverThreshold(this.messages, onRequestUsage);
|
|
203
|
+
this.messages = messages;
|
|
204
|
+
return compacted;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The mid-loop compaction seam (build item 3) handed to {@link runAgent}: same
|
|
208
|
+
* threshold/cut/summarize path as {@link maybeCompact}, but over the loop's own
|
|
209
|
+
* running history rather than `this.messages` (the loop owns and adopts its
|
|
210
|
+
* copy). Reuses the exact machinery so no summarization logic is duplicated and
|
|
211
|
+
* the loop stays ignorant of it. Under threshold it returns the history
|
|
212
|
+
* unchanged — cheap, no model call.
|
|
213
|
+
*/
|
|
214
|
+
async compactLoopHistory(messages, onRequestUsage) {
|
|
215
|
+
const result = await this.compactIfOverThreshold(messages, onRequestUsage);
|
|
216
|
+
return result.messages;
|
|
187
217
|
}
|
|
188
218
|
/**
|
|
189
219
|
* Force compaction regardless of the threshold (backs `/compact`). Returns the
|
|
@@ -191,20 +221,47 @@ export class Session {
|
|
|
191
221
|
* cut or the summary call failed.
|
|
192
222
|
*/
|
|
193
223
|
async compact() {
|
|
194
|
-
|
|
224
|
+
const { messages, compacted } = await this.runCompaction(this.messages);
|
|
225
|
+
this.messages = messages;
|
|
226
|
+
return compacted;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Compact `messages` when its estimated footprint exceeds
|
|
230
|
+
* `compactThreshold * maxTokens`, else return it untouched. The estimate adds a
|
|
231
|
+
* fixed `reserveTokens` allowance for the system prompt and tool schemas that
|
|
232
|
+
* {@link estimateTokens} never sees (~4.5k+ tokens of real request payload), so
|
|
233
|
+
* the trigger reflects the actual request size rather than only the visible
|
|
234
|
+
* history — otherwise the loop can sit just under the visible threshold while
|
|
235
|
+
* the real request has already overrun the window. Logs a one-line notice on a
|
|
236
|
+
* successful compaction.
|
|
237
|
+
*/
|
|
238
|
+
async compactIfOverThreshold(messages, onRequestUsage) {
|
|
239
|
+
const { maxTokens, compactThreshold, reserveTokens } = this.args.config.context;
|
|
240
|
+
const estimated = estimateTokens(messages) + reserveTokens;
|
|
241
|
+
if (estimated <= compactThreshold * maxTokens) {
|
|
242
|
+
return { messages, compacted: null };
|
|
243
|
+
}
|
|
244
|
+
const result = await this.runCompaction(messages, onRequestUsage);
|
|
245
|
+
if (result.compacted) {
|
|
246
|
+
this.args.ctx.logger.info(`compacted ${result.compacted} older message${result.compacted === 1 ? "" : "s"} to stay within context`);
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
195
249
|
}
|
|
196
250
|
/**
|
|
197
251
|
* Find a clean cut, summarize the older prefix into a synthetic user/assistant
|
|
198
252
|
* pair, and splice it in front of the kept-recent messages. Best-effort: a
|
|
199
|
-
* failed summary call leaves the history untouched and
|
|
200
|
-
* open — losing compaction is degraded, not unsafe).
|
|
253
|
+
* failed summary call leaves the history untouched and reports `compacted:
|
|
254
|
+
* null` (fail open — losing compaction is degraded, not unsafe). Pure with
|
|
255
|
+
* respect to `this.messages`: it returns the new array for the caller to adopt
|
|
256
|
+
* (the loop and the session each own their own history), and only accumulates
|
|
257
|
+
* the summary's usage into the session total.
|
|
201
258
|
*/
|
|
202
|
-
async runCompaction(onRequestUsage) {
|
|
203
|
-
const cut = this.findCut();
|
|
259
|
+
async runCompaction(messages, onRequestUsage) {
|
|
260
|
+
const cut = this.findCut(messages);
|
|
204
261
|
if (cut === null)
|
|
205
|
-
return null;
|
|
206
|
-
const prefix =
|
|
207
|
-
const kept =
|
|
262
|
+
return { messages, compacted: null };
|
|
263
|
+
const prefix = messages.slice(0, cut);
|
|
264
|
+
const kept = messages.slice(cut);
|
|
208
265
|
let synopsis;
|
|
209
266
|
try {
|
|
210
267
|
const summary = await this.summarize(prefix, onRequestUsage);
|
|
@@ -214,7 +271,7 @@ export class Session {
|
|
|
214
271
|
}
|
|
215
272
|
catch (err) {
|
|
216
273
|
this.args.ctx.logger.warn(`compaction skipped: summary failed (${err.message})`);
|
|
217
|
-
return null;
|
|
274
|
+
return { messages, compacted: null };
|
|
218
275
|
}
|
|
219
276
|
// A user/assistant pair, not a lone message: the kept region begins with a
|
|
220
277
|
// user prompt, so a single synthetic user message would put two user turns
|
|
@@ -229,8 +286,10 @@ export class Session {
|
|
|
229
286
|
content: `${COMPACTION_MARKER} Summary of the conversation so far:\n\n${synopsis}`,
|
|
230
287
|
},
|
|
231
288
|
];
|
|
232
|
-
|
|
233
|
-
|
|
289
|
+
return {
|
|
290
|
+
messages: [...summaryMessages, ...kept],
|
|
291
|
+
compacted: prefix.length,
|
|
292
|
+
};
|
|
234
293
|
}
|
|
235
294
|
/**
|
|
236
295
|
* Choose the boundary between the summarized prefix and the kept-recent tail.
|
|
@@ -239,18 +298,20 @@ export class Session {
|
|
|
239
298
|
* matching `tool_result` (the next user message) must never straddle the cut,
|
|
240
299
|
* or the next provider call breaks. A real user *prompt* (`role:"user"` with
|
|
241
300
|
* string content) only occurs at a completed turn boundary, where every prior
|
|
242
|
-
* tool exchange is already resolved — so the kept region must begin there.
|
|
301
|
+
* tool exchange is already resolved — so the kept region must begin there. The
|
|
302
|
+
* synthetic compaction-summary user message is also string content, so a
|
|
303
|
+
* repeat compaction always finds at least the previous summary as a clean cut.
|
|
243
304
|
*
|
|
244
305
|
* Start from `length - keepRecentMessages` and walk *backwards* to the nearest
|
|
245
306
|
* such prompt: this keeps at least the recent floor and lands clean. Returns
|
|
246
307
|
* the cut index, or `null` if no safe boundary leaves a non-empty prefix
|
|
247
308
|
* (e.g. a single long in-progress turn — nothing safe to compact).
|
|
248
309
|
*/
|
|
249
|
-
findCut() {
|
|
310
|
+
findCut(messages) {
|
|
250
311
|
const { keepRecentMessages } = this.args.config.context;
|
|
251
|
-
const start =
|
|
312
|
+
const start = messages.length - keepRecentMessages;
|
|
252
313
|
for (let i = start; i >= 1; i--) {
|
|
253
|
-
const msg =
|
|
314
|
+
const msg = messages[i];
|
|
254
315
|
if (msg.role === "user" && typeof msg.content === "string")
|
|
255
316
|
return i;
|
|
256
317
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -187,7 +187,24 @@ export function runCommand() {
|
|
|
187
187
|
logger.info(t.muted(`mcp: server "${d.server}" declared in ${d.root} not loaded (primary-root MCP only this release)`));
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
|
-
|
|
190
|
+
// One-shot vs REPL turn ceiling. A one-shot run caps at
|
|
191
|
+
// `agent.maxIterationsOneShot` (default 40): a headless run can't be
|
|
192
|
+
// resumed by a human, so hitting the cap abandons the whole task — it
|
|
193
|
+
// needs headroom above the interactive cap to clear legit long work,
|
|
194
|
+
// while still bounding a runaway (and it now exits non-zero, see below).
|
|
195
|
+
// The REPL keeps `agent.maxIterations` (a soft checkpoint). Keyed on the
|
|
196
|
+
// one-shot MODE, never on TTY, so `cruxy run "task"` behaves the same
|
|
197
|
+
// piped into CI or run in a terminal.
|
|
198
|
+
const sessionConfig = interactive
|
|
199
|
+
? config
|
|
200
|
+
: {
|
|
201
|
+
...config,
|
|
202
|
+
agent: {
|
|
203
|
+
...config.agent,
|
|
204
|
+
maxIterations: config.agent.maxIterationsOneShot,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const session = buildAgentSession(sessionConfig, apiKey, workspace, Boolean(process.stdin.isTTY), planMode, renderer, checkpoints, sandbox, hooksRunner, mcp.tools);
|
|
191
208
|
if (interactive) {
|
|
192
209
|
try {
|
|
193
210
|
await runInteractive(session, undefined, renderer, checkpoints, hookCommands);
|
|
@@ -253,7 +270,9 @@ export function runCommand() {
|
|
|
253
270
|
stop: result.stop,
|
|
254
271
|
iterations: result.iterations,
|
|
255
272
|
reason: result.stopReason,
|
|
256
|
-
|
|
273
|
+
// The effective one-shot cap (agent.maxIterationsOneShot), so the
|
|
274
|
+
// message names the limit the run actually hit.
|
|
275
|
+
maxIterations: sessionConfig.agent.maxIterations,
|
|
257
276
|
});
|
|
258
277
|
}
|
|
259
278
|
});
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -26,18 +26,48 @@ export declare const CruxyBackendConfigSchema: z.ZodObject<{
|
|
|
26
26
|
gatewayUrl?: string | undefined;
|
|
27
27
|
}>;
|
|
28
28
|
export declare const AgentConfigSchema: z.ZodObject<{
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Hard ceiling on agent loop turns per user turn — the interactive default.
|
|
31
|
+
* In the REPL this is a soft checkpoint: hitting it ends the turn and the
|
|
32
|
+
* human continues (or Ctrl-C's a runaway), so it can be generous.
|
|
33
|
+
*/
|
|
30
34
|
maxIterations: z.ZodDefault<z.ZodNumber>;
|
|
35
|
+
/**
|
|
36
|
+
* Turn ceiling for a ONE-SHOT `cruxy run "task"` (non-interactive). Set higher
|
|
37
|
+
* than {@link maxIterations} because a headless run has no human to resume it:
|
|
38
|
+
* hitting the cap abandons the whole task (fail-loud, non-zero exit), so it
|
|
39
|
+
* must clear the legit long tail (real "fix the failing tests" work runs
|
|
40
|
+
* ~10-25 turns) while still bounding a runaway. Not clamped to
|
|
41
|
+
* `maxIterations` — a user may deliberately set it lower or higher.
|
|
42
|
+
*/
|
|
43
|
+
maxIterationsOneShot: z.ZodDefault<z.ZodNumber>;
|
|
44
|
+
/**
|
|
45
|
+
* Optional hard cap on combined input+output tokens for a SINGLE user turn
|
|
46
|
+
* — a last-resort runaway guard, not cost control (cumulative session cost
|
|
47
|
+
* is C.22's concern). OFF by default (0 = off): today's behavior is
|
|
48
|
+
* preserved exactly and no turn is ever token-stopped. When set, a turn that
|
|
49
|
+
* crosses the cap stops at the next turn boundary with a coherent partial
|
|
50
|
+
* history (`stop: "budget"`); in one-shot that fails loud (exit 20), while
|
|
51
|
+
* interactively the human simply continues. Compaction is the everyday
|
|
52
|
+
* pressure valve for context — this only bounds a genuinely runaway turn, so
|
|
53
|
+
* a hostile hard stop mid-work stays opt-in. Reset each `send`, never
|
|
54
|
+
* cumulative across turns.
|
|
55
|
+
*/
|
|
56
|
+
maxTokensPerTurn: z.ZodDefault<z.ZodNumber>;
|
|
31
57
|
/** Skip per-action confirmation prompts. */
|
|
32
58
|
autoApprove: z.ZodDefault<z.ZodBoolean>;
|
|
33
59
|
/** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
|
|
34
60
|
planMode: z.ZodDefault<z.ZodBoolean>;
|
|
35
61
|
}, "strict", z.ZodTypeAny, {
|
|
36
62
|
maxIterations: number;
|
|
63
|
+
maxIterationsOneShot: number;
|
|
64
|
+
maxTokensPerTurn: number;
|
|
37
65
|
autoApprove: boolean;
|
|
38
66
|
planMode: boolean;
|
|
39
67
|
}, {
|
|
40
68
|
maxIterations?: number | undefined;
|
|
69
|
+
maxIterationsOneShot?: number | undefined;
|
|
70
|
+
maxTokensPerTurn?: number | undefined;
|
|
41
71
|
autoApprove?: boolean | undefined;
|
|
42
72
|
planMode?: boolean | undefined;
|
|
43
73
|
}>;
|
|
@@ -88,16 +118,26 @@ export declare const ContextConfigSchema: z.ZodObject<{
|
|
|
88
118
|
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
89
119
|
/** Compact once the history estimate exceeds this fraction of maxTokens. */
|
|
90
120
|
compactThreshold: z.ZodDefault<z.ZodNumber>;
|
|
121
|
+
/**
|
|
122
|
+
* Fixed token allowance for request payload that `estimateTokens` never
|
|
123
|
+
* sees — the system prompt and every tool's JSON schema — added to the
|
|
124
|
+
* measured history before the threshold test so the trigger reflects the
|
|
125
|
+
* real request size, not just the visible messages. Roughly the size of
|
|
126
|
+
* the built system prompt plus the default tool catalogue today.
|
|
127
|
+
*/
|
|
128
|
+
reserveTokens: z.ZodDefault<z.ZodNumber>;
|
|
91
129
|
/** Most-recent messages always kept verbatim (a floor; the cut rounds up to
|
|
92
130
|
* a clean turn boundary). */
|
|
93
131
|
keepRecentMessages: z.ZodDefault<z.ZodNumber>;
|
|
94
132
|
}, "strict", z.ZodTypeAny, {
|
|
95
133
|
maxTokens: number;
|
|
96
134
|
compactThreshold: number;
|
|
135
|
+
reserveTokens: number;
|
|
97
136
|
keepRecentMessages: number;
|
|
98
137
|
}, {
|
|
99
138
|
maxTokens?: number | undefined;
|
|
100
139
|
compactThreshold?: number | undefined;
|
|
140
|
+
reserveTokens?: number | undefined;
|
|
101
141
|
keepRecentMessages?: number | undefined;
|
|
102
142
|
}>;
|
|
103
143
|
/**
|
|
@@ -979,18 +1019,48 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
979
1019
|
gatewayUrl?: string | undefined;
|
|
980
1020
|
}>>;
|
|
981
1021
|
agent: z.ZodDefault<z.ZodObject<{
|
|
982
|
-
/**
|
|
1022
|
+
/**
|
|
1023
|
+
* Hard ceiling on agent loop turns per user turn — the interactive default.
|
|
1024
|
+
* In the REPL this is a soft checkpoint: hitting it ends the turn and the
|
|
1025
|
+
* human continues (or Ctrl-C's a runaway), so it can be generous.
|
|
1026
|
+
*/
|
|
983
1027
|
maxIterations: z.ZodDefault<z.ZodNumber>;
|
|
1028
|
+
/**
|
|
1029
|
+
* Turn ceiling for a ONE-SHOT `cruxy run "task"` (non-interactive). Set higher
|
|
1030
|
+
* than {@link maxIterations} because a headless run has no human to resume it:
|
|
1031
|
+
* hitting the cap abandons the whole task (fail-loud, non-zero exit), so it
|
|
1032
|
+
* must clear the legit long tail (real "fix the failing tests" work runs
|
|
1033
|
+
* ~10-25 turns) while still bounding a runaway. Not clamped to
|
|
1034
|
+
* `maxIterations` — a user may deliberately set it lower or higher.
|
|
1035
|
+
*/
|
|
1036
|
+
maxIterationsOneShot: z.ZodDefault<z.ZodNumber>;
|
|
1037
|
+
/**
|
|
1038
|
+
* Optional hard cap on combined input+output tokens for a SINGLE user turn
|
|
1039
|
+
* — a last-resort runaway guard, not cost control (cumulative session cost
|
|
1040
|
+
* is C.22's concern). OFF by default (0 = off): today's behavior is
|
|
1041
|
+
* preserved exactly and no turn is ever token-stopped. When set, a turn that
|
|
1042
|
+
* crosses the cap stops at the next turn boundary with a coherent partial
|
|
1043
|
+
* history (`stop: "budget"`); in one-shot that fails loud (exit 20), while
|
|
1044
|
+
* interactively the human simply continues. Compaction is the everyday
|
|
1045
|
+
* pressure valve for context — this only bounds a genuinely runaway turn, so
|
|
1046
|
+
* a hostile hard stop mid-work stays opt-in. Reset each `send`, never
|
|
1047
|
+
* cumulative across turns.
|
|
1048
|
+
*/
|
|
1049
|
+
maxTokensPerTurn: z.ZodDefault<z.ZodNumber>;
|
|
984
1050
|
/** Skip per-action confirmation prompts. */
|
|
985
1051
|
autoApprove: z.ZodDefault<z.ZodBoolean>;
|
|
986
1052
|
/** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
|
|
987
1053
|
planMode: z.ZodDefault<z.ZodBoolean>;
|
|
988
1054
|
}, "strict", z.ZodTypeAny, {
|
|
989
1055
|
maxIterations: number;
|
|
1056
|
+
maxIterationsOneShot: number;
|
|
1057
|
+
maxTokensPerTurn: number;
|
|
990
1058
|
autoApprove: boolean;
|
|
991
1059
|
planMode: boolean;
|
|
992
1060
|
}, {
|
|
993
1061
|
maxIterations?: number | undefined;
|
|
1062
|
+
maxIterationsOneShot?: number | undefined;
|
|
1063
|
+
maxTokensPerTurn?: number | undefined;
|
|
994
1064
|
autoApprove?: boolean | undefined;
|
|
995
1065
|
planMode?: boolean | undefined;
|
|
996
1066
|
}>>;
|
|
@@ -1038,16 +1108,26 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1038
1108
|
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
1039
1109
|
/** Compact once the history estimate exceeds this fraction of maxTokens. */
|
|
1040
1110
|
compactThreshold: z.ZodDefault<z.ZodNumber>;
|
|
1111
|
+
/**
|
|
1112
|
+
* Fixed token allowance for request payload that `estimateTokens` never
|
|
1113
|
+
* sees — the system prompt and every tool's JSON schema — added to the
|
|
1114
|
+
* measured history before the threshold test so the trigger reflects the
|
|
1115
|
+
* real request size, not just the visible messages. Roughly the size of
|
|
1116
|
+
* the built system prompt plus the default tool catalogue today.
|
|
1117
|
+
*/
|
|
1118
|
+
reserveTokens: z.ZodDefault<z.ZodNumber>;
|
|
1041
1119
|
/** Most-recent messages always kept verbatim (a floor; the cut rounds up to
|
|
1042
1120
|
* a clean turn boundary). */
|
|
1043
1121
|
keepRecentMessages: z.ZodDefault<z.ZodNumber>;
|
|
1044
1122
|
}, "strict", z.ZodTypeAny, {
|
|
1045
1123
|
maxTokens: number;
|
|
1046
1124
|
compactThreshold: number;
|
|
1125
|
+
reserveTokens: number;
|
|
1047
1126
|
keepRecentMessages: number;
|
|
1048
1127
|
}, {
|
|
1049
1128
|
maxTokens?: number | undefined;
|
|
1050
1129
|
compactThreshold?: number | undefined;
|
|
1130
|
+
reserveTokens?: number | undefined;
|
|
1051
1131
|
keepRecentMessages?: number | undefined;
|
|
1052
1132
|
}>>;
|
|
1053
1133
|
approval: z.ZodDefault<z.ZodObject<{
|
|
@@ -1737,6 +1817,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1737
1817
|
};
|
|
1738
1818
|
agent: {
|
|
1739
1819
|
maxIterations: number;
|
|
1820
|
+
maxIterationsOneShot: number;
|
|
1821
|
+
maxTokensPerTurn: number;
|
|
1740
1822
|
autoApprove: boolean;
|
|
1741
1823
|
planMode: boolean;
|
|
1742
1824
|
};
|
|
@@ -1752,6 +1834,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1752
1834
|
context: {
|
|
1753
1835
|
maxTokens: number;
|
|
1754
1836
|
compactThreshold: number;
|
|
1837
|
+
reserveTokens: number;
|
|
1755
1838
|
keepRecentMessages: number;
|
|
1756
1839
|
};
|
|
1757
1840
|
index: {
|
|
@@ -1889,6 +1972,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1889
1972
|
} | undefined;
|
|
1890
1973
|
agent?: {
|
|
1891
1974
|
maxIterations?: number | undefined;
|
|
1975
|
+
maxIterationsOneShot?: number | undefined;
|
|
1976
|
+
maxTokensPerTurn?: number | undefined;
|
|
1892
1977
|
autoApprove?: boolean | undefined;
|
|
1893
1978
|
planMode?: boolean | undefined;
|
|
1894
1979
|
} | undefined;
|
|
@@ -1904,6 +1989,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1904
1989
|
context?: {
|
|
1905
1990
|
maxTokens?: number | undefined;
|
|
1906
1991
|
compactThreshold?: number | undefined;
|
|
1992
|
+
reserveTokens?: number | undefined;
|
|
1907
1993
|
keepRecentMessages?: number | undefined;
|
|
1908
1994
|
} | undefined;
|
|
1909
1995
|
index?: {
|
package/dist/config/schema.js
CHANGED
|
@@ -26,8 +26,34 @@ export const CruxyBackendConfigSchema = z
|
|
|
26
26
|
.strict();
|
|
27
27
|
export const AgentConfigSchema = z
|
|
28
28
|
.object({
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Hard ceiling on agent loop turns per user turn — the interactive default.
|
|
31
|
+
* In the REPL this is a soft checkpoint: hitting it ends the turn and the
|
|
32
|
+
* human continues (or Ctrl-C's a runaway), so it can be generous.
|
|
33
|
+
*/
|
|
30
34
|
maxIterations: z.number().int().positive().default(25),
|
|
35
|
+
/**
|
|
36
|
+
* Turn ceiling for a ONE-SHOT `cruxy run "task"` (non-interactive). Set higher
|
|
37
|
+
* than {@link maxIterations} because a headless run has no human to resume it:
|
|
38
|
+
* hitting the cap abandons the whole task (fail-loud, non-zero exit), so it
|
|
39
|
+
* must clear the legit long tail (real "fix the failing tests" work runs
|
|
40
|
+
* ~10-25 turns) while still bounding a runaway. Not clamped to
|
|
41
|
+
* `maxIterations` — a user may deliberately set it lower or higher.
|
|
42
|
+
*/
|
|
43
|
+
maxIterationsOneShot: z.number().int().positive().default(40),
|
|
44
|
+
/**
|
|
45
|
+
* Optional hard cap on combined input+output tokens for a SINGLE user turn
|
|
46
|
+
* — a last-resort runaway guard, not cost control (cumulative session cost
|
|
47
|
+
* is C.22's concern). OFF by default (0 = off): today's behavior is
|
|
48
|
+
* preserved exactly and no turn is ever token-stopped. When set, a turn that
|
|
49
|
+
* crosses the cap stops at the next turn boundary with a coherent partial
|
|
50
|
+
* history (`stop: "budget"`); in one-shot that fails loud (exit 20), while
|
|
51
|
+
* interactively the human simply continues. Compaction is the everyday
|
|
52
|
+
* pressure valve for context — this only bounds a genuinely runaway turn, so
|
|
53
|
+
* a hostile hard stop mid-work stays opt-in. Reset each `send`, never
|
|
54
|
+
* cumulative across turns.
|
|
55
|
+
*/
|
|
56
|
+
maxTokensPerTurn: z.number().int().nonnegative().default(0),
|
|
31
57
|
/** Skip per-action confirmation prompts. */
|
|
32
58
|
autoApprove: z.boolean().default(false),
|
|
33
59
|
/** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
|
|
@@ -68,6 +94,14 @@ export const ContextConfigSchema = z
|
|
|
68
94
|
maxTokens: z.number().int().positive().default(100000),
|
|
69
95
|
/** Compact once the history estimate exceeds this fraction of maxTokens. */
|
|
70
96
|
compactThreshold: z.number().min(0).max(1).default(0.75),
|
|
97
|
+
/**
|
|
98
|
+
* Fixed token allowance for request payload that `estimateTokens` never
|
|
99
|
+
* sees — the system prompt and every tool's JSON schema — added to the
|
|
100
|
+
* measured history before the threshold test so the trigger reflects the
|
|
101
|
+
* real request size, not just the visible messages. Roughly the size of
|
|
102
|
+
* the built system prompt plus the default tool catalogue today.
|
|
103
|
+
*/
|
|
104
|
+
reserveTokens: z.number().int().nonnegative().default(4500),
|
|
71
105
|
/** Most-recent messages always kept verbatim (a floor; the cut rounds up to
|
|
72
106
|
* a clean turn boundary). */
|
|
73
107
|
keepRecentMessages: z.number().int().positive().default(6),
|
|
@@ -1235,7 +1235,7 @@ export function agentIncomplete(info) {
|
|
|
1235
1235
|
cause: `reached the turn limit${cap} after ${turns} without completing. The work so far is shown above.`,
|
|
1236
1236
|
nextSteps: [
|
|
1237
1237
|
"review the partial output above, then re-run with a narrower prompt",
|
|
1238
|
-
"raise `agent.maxIterations` if the task legitimately needs more turns",
|
|
1238
|
+
"raise `agent.maxIterationsOneShot` (one-shot) or `agent.maxIterations` if the task legitimately needs more turns",
|
|
1239
1239
|
],
|
|
1240
1240
|
meta,
|
|
1241
1241
|
});
|
package/dist/jobs/manager.js
CHANGED
|
@@ -2,7 +2,8 @@ import { runAgent } from "../agent/loop.js";
|
|
|
2
2
|
import { ApprovalService, classify, serializeGate, } from "../approval/index.js";
|
|
3
3
|
import { CheckpointGate, withCheckpointGate } from "../checkpoint/index.js";
|
|
4
4
|
import { CruxyError, ErrorCode, approvalRequired, jobLimitExceeded, jobNotFound, jobsDisabled, messageOf, } from "../errors/index.js";
|
|
5
|
-
import { Budget, resolveBudget
|
|
5
|
+
import { Budget, resolveBudget } from "../agent/budget.js";
|
|
6
|
+
import { scopeRegistry } from "../subagent/index.js";
|
|
6
7
|
import { Workspace } from "../workspace/index.js";
|
|
7
8
|
import { LogBuffer } from "./log-buffer.js";
|
|
8
9
|
import { JobLogRenderer } from "./log-renderer.js";
|
package/dist/subagent/index.d.ts
CHANGED
package/dist/subagent/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { runAgent } from "../agent/loop.js";
|
|
3
3
|
import { CruxyError, ErrorCode, messageOf, subagentDepthExceeded, subagentScopeOverlap, } from "../errors/index.js";
|
|
4
4
|
import { Workspace } from "../workspace/index.js";
|
|
5
|
-
import { Budget, resolveBudget } from "
|
|
5
|
+
import { Budget, resolveBudget } from "../agent/budget.js";
|
|
6
6
|
import { scopeRegistry, SUBAGENT_WRITE_TOOLS } from "./registry-scope.js";
|
|
7
7
|
import { Semaphore } from "./semaphore.js";
|
|
8
8
|
import { makeSpawnSubagentTool } from "./spawn-tool.js";
|
package/dist/subagent/types.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Usage } from "@cruxy/sdk";
|
|
2
2
|
import type { TaskClass } from "../routing/index.js";
|
|
3
|
+
import type { BudgetLimits } from "../agent/budget.js";
|
|
4
|
+
export type { BudgetLimits };
|
|
3
5
|
/**
|
|
4
6
|
* Types for subagent orchestration (C.14): the main agent delegates a bounded
|
|
5
7
|
* subtask to a child agent that runs the SAME loop with its own fresh history,
|
|
@@ -14,19 +16,6 @@ import type { TaskClass } from "../routing/index.js";
|
|
|
14
16
|
* dressed up as `done`.
|
|
15
17
|
*/
|
|
16
18
|
export type SubagentStatus = "done" | "budget-exceeded" | "failed" | "cancelled";
|
|
17
|
-
/**
|
|
18
|
-
* Hard caps a subagent runs under. `maxIterations` and `maxTokens` are always
|
|
19
|
-
* finite — a subagent is bounded by construction; `timeoutMs` is an optional
|
|
20
|
-
* wall-clock backstop on top.
|
|
21
|
-
*/
|
|
22
|
-
export interface BudgetLimits {
|
|
23
|
-
/** Cap on the subagent's model turns. */
|
|
24
|
-
maxIterations: number;
|
|
25
|
-
/** Cap on the subagent's combined input+output tokens. */
|
|
26
|
-
maxTokens: number;
|
|
27
|
-
/** Optional wall-clock cap in milliseconds. */
|
|
28
|
-
timeoutMs?: number;
|
|
29
|
-
}
|
|
30
19
|
/** A spawn request: the bounded task plus optional scope/budget narrowing. */
|
|
31
20
|
export interface SubagentSpec {
|
|
32
21
|
/** The complete, self-contained subtask the subagent should perform. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cruxy/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.2",
|
|
4
4
|
"description": "an agentic coding CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"undici": "^6.21.0",
|
|
38
38
|
"zod": "^3.23.8",
|
|
39
39
|
"zod-to-json-schema": "^3.23.5",
|
|
40
|
-
"@cruxy/sdk": "0.2.
|
|
40
|
+
"@cruxy/sdk": "0.2.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import type { Usage } from "@cruxy/sdk";
|
|
2
|
-
import type { LoopBudget } from "../agent/loop.js";
|
|
3
|
-
import type { BudgetLimits } from "./types.js";
|
|
4
|
-
/**
|
|
5
|
-
* The subagent budget (C.14): iteration + token + optional wall-clock caps,
|
|
6
|
-
* checked by the agent loop before every model turn (see `LoopBudget`). A
|
|
7
|
-
* tripped cap stops the run with a human-readable reason — the subagent
|
|
8
|
-
* returns a partial result, it never runs unbounded.
|
|
9
|
-
*/
|
|
10
|
-
/**
|
|
11
|
-
* Resolve the effective limits for one spawn: start from the configured
|
|
12
|
-
* ceilings and let overrides only *narrow* them. A request above a ceiling is
|
|
13
|
-
* clamped down, not honored — "budget overrides within limits" by construction.
|
|
14
|
-
*/
|
|
15
|
-
export declare function resolveBudget(defaults: BudgetLimits, overrides?: Partial<BudgetLimits>): BudgetLimits;
|
|
16
|
-
/**
|
|
17
|
-
* A live budget for one subagent run. The wall clock starts at construction
|
|
18
|
-
* (spawn time); the clock source is injectable so tests never sleep.
|
|
19
|
-
*/
|
|
20
|
-
export declare class Budget implements LoopBudget {
|
|
21
|
-
private readonly limits;
|
|
22
|
-
private readonly now;
|
|
23
|
-
private readonly startedAt;
|
|
24
|
-
constructor(limits: BudgetLimits, now?: () => number);
|
|
25
|
-
/**
|
|
26
|
-
* The reason to stop before the next model turn, or `null` to continue.
|
|
27
|
-
* Checked at iteration boundaries — the in-flight turn always completes, so
|
|
28
|
-
* overshoot is bounded by one turn.
|
|
29
|
-
*/
|
|
30
|
-
exceeded(state: {
|
|
31
|
-
iterations: number;
|
|
32
|
-
usage: Usage;
|
|
33
|
-
}): string | null;
|
|
34
|
-
}
|