clementine-agent 1.18.59 → 1.18.60
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.
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import pino from 'pino';
|
|
20
|
-
import { BASE_DIR, VAULT_DIR, CRON_PROGRESS_DIR, } from '../config.js';
|
|
20
|
+
import { BASE_DIR, VAULT_DIR, CRON_PROGRESS_DIR, BUDGET, } from '../config.js';
|
|
21
21
|
import { runAgent } from './run-agent.js';
|
|
22
22
|
import { buildExtraMcpForRunAgent } from './run-agent-mcp.js';
|
|
23
23
|
import { buildAutonomousMemoryContext } from './run-agent-context.js';
|
|
@@ -281,7 +281,12 @@ export async function runAgentCron(opts) {
|
|
|
281
281
|
profile: opts.profile,
|
|
282
282
|
});
|
|
283
283
|
// ── Run via canonical runAgent ────────────────────────────────────
|
|
284
|
-
|
|
284
|
+
// Per-tier cap from config (BUDGET.cronT1 / BUDGET.cronT2). Sourced
|
|
285
|
+
// from env / clementine.json / dashboard writes. 0 means uncapped —
|
|
286
|
+
// we pass undefined so runAgent omits the SDK option entirely.
|
|
287
|
+
// Caller can still override via opts.maxBudgetUsd.
|
|
288
|
+
const configuredCap = tier >= 2 ? BUDGET.cronT2 : BUDGET.cronT1;
|
|
289
|
+
const maxBudget = opts.maxBudgetUsd ?? (configuredCap > 0 ? configuredCap : undefined);
|
|
285
290
|
const effort = tier >= 2 ? 'high' : 'medium';
|
|
286
291
|
logger.info({
|
|
287
292
|
job: opts.jobName,
|
|
@@ -302,7 +307,7 @@ export async function runAgentCron(opts) {
|
|
|
302
307
|
memoryStore: opts.memoryStore,
|
|
303
308
|
model: opts.model,
|
|
304
309
|
effort,
|
|
305
|
-
maxBudgetUsd: maxBudget,
|
|
310
|
+
...(maxBudget !== undefined ? { maxBudgetUsd: maxBudget } : {}),
|
|
306
311
|
maxTurns: opts.maxTurns,
|
|
307
312
|
abortSignal: opts.abortSignal,
|
|
308
313
|
extraMcpServers: mcp.servers,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* through the canonical runAgent() instead of buildOptions+query.
|
|
15
15
|
*/
|
|
16
16
|
import pino from 'pino';
|
|
17
|
-
import { OWNER_NAME, MODELS, } from '../config.js';
|
|
17
|
+
import { OWNER_NAME, MODELS, BUDGET, } from '../config.js';
|
|
18
18
|
const OWNER = OWNER_NAME || 'the user';
|
|
19
19
|
function formatDate(d) {
|
|
20
20
|
return d.toLocaleDateString('en-US', {
|
|
@@ -65,6 +65,10 @@ export async function runAgentHeartbeat(opts) {
|
|
|
65
65
|
profile: opts.profile?.slug,
|
|
66
66
|
promptChars: prompt.length,
|
|
67
67
|
}, 'runAgentHeartbeat: dispatching to runAgent (no tools)');
|
|
68
|
+
// Heartbeat cap from config (BUDGET.heartbeat). Sourced from env /
|
|
69
|
+
// clementine.json / dashboard writes. 0 = uncapped — runAgent
|
|
70
|
+
// omits the SDK option in that case.
|
|
71
|
+
const heartbeatBudget = opts.maxBudgetUsd ?? (BUDGET.heartbeat > 0 ? BUDGET.heartbeat : undefined);
|
|
68
72
|
const sessionKey = `heartbeat:${opts.profile?.slug ?? 'clementine'}`;
|
|
69
73
|
const result = await runAgent(prompt, {
|
|
70
74
|
sessionKey,
|
|
@@ -73,7 +77,7 @@ export async function runAgentHeartbeat(opts) {
|
|
|
73
77
|
memoryStore: opts.memoryStore,
|
|
74
78
|
model: opts.model ?? MODELS.haiku,
|
|
75
79
|
effort: 'low',
|
|
76
|
-
maxBudgetUsd:
|
|
80
|
+
...(heartbeatBudget !== undefined ? { maxBudgetUsd: heartbeatBudget } : {}),
|
|
77
81
|
maxTurns: 1,
|
|
78
82
|
// No tools — heartbeats are decision-only. Empty list bypasses the
|
|
79
83
|
// CORE_TOOLS_FOR_AGENT_PARENT default and stops the SDK from
|
package/dist/agent/run-agent.js
CHANGED
|
@@ -62,8 +62,15 @@ function buildRunAgentEnv() {
|
|
|
62
62
|
return env;
|
|
63
63
|
}
|
|
64
64
|
const logger = pino({ name: 'clementine.run-agent' });
|
|
65
|
+
// Last-resort fallbacks for callers that pass NO maxBudgetUsd. The
|
|
66
|
+
// production callers (`runAgent` from gateway/router, runAgentCron,
|
|
67
|
+
// runAgentHeartbeat) read `BUDGET.*` from src/config.ts — which is
|
|
68
|
+
// itself sourced from env / clementine.json / dashboard writes — and
|
|
69
|
+
// pass it explicitly. Chat is intentionally omitted: the chat path
|
|
70
|
+
// must always go through `BUDGET.chat` (0 = uncapped), never a silent
|
|
71
|
+
// hardcoded floor. If `source: 'chat'` ever lands here without an
|
|
72
|
+
// explicit budget, we treat it as uncapped.
|
|
65
73
|
const DEFAULT_BUDGETS = {
|
|
66
|
-
chat: 0.50,
|
|
67
74
|
cron: 1.00,
|
|
68
75
|
heartbeat: 0.25,
|
|
69
76
|
'team-task': 1.00,
|
|
@@ -97,7 +104,13 @@ const CORE_TOOLS_FOR_AGENT_PARENT = [
|
|
|
97
104
|
export async function runAgent(prompt, opts) {
|
|
98
105
|
const source = opts.source ?? 'chat';
|
|
99
106
|
const effort = opts.effort ?? DEFAULT_EFFORTS[source] ?? 'medium';
|
|
100
|
-
|
|
107
|
+
// 0 (or undefined) means "no cap" — matches the dashboard's
|
|
108
|
+
// "Remove spend caps" preset contract. We omit `maxBudgetUsd` from
|
|
109
|
+
// sdkOptions entirely in that case so the SDK runs uncapped.
|
|
110
|
+
const requestedBudget = opts.maxBudgetUsd ?? DEFAULT_BUDGETS[source];
|
|
111
|
+
const maxBudgetUsd = typeof requestedBudget === 'number' && requestedBudget > 0
|
|
112
|
+
? requestedBudget
|
|
113
|
+
: undefined;
|
|
101
114
|
const startedAt = Date.now();
|
|
102
115
|
// Build the AgentDefinition map. Caller can override; otherwise we
|
|
103
116
|
// use the standard system subagents + hired-agent profiles.
|
|
@@ -187,8 +200,8 @@ export async function runAgent(prompt, opts) {
|
|
|
187
200
|
allowDangerouslySkipPermissions: true,
|
|
188
201
|
cwd: BASE_DIR,
|
|
189
202
|
env: subprocessEnv,
|
|
190
|
-
maxBudgetUsd,
|
|
191
203
|
effort,
|
|
204
|
+
...(maxBudgetUsd !== undefined ? { maxBudgetUsd } : {}),
|
|
192
205
|
...(opts.maxTurns ? { maxTurns: opts.maxTurns } : {}),
|
|
193
206
|
...(opts.model ? { model: opts.model } : {}),
|
|
194
207
|
...(opts.resumeSessionId ? { resume: opts.resumeSessionId } : {}),
|
|
@@ -201,7 +214,7 @@ export async function runAgent(prompt, opts) {
|
|
|
201
214
|
profile: opts.profile?.slug,
|
|
202
215
|
forceSubagent: opts.forceSubagent,
|
|
203
216
|
effort,
|
|
204
|
-
maxBudgetUsd,
|
|
217
|
+
maxBudgetUsd: maxBudgetUsd ?? 'uncapped',
|
|
205
218
|
agentCount: Object.keys(agents).length,
|
|
206
219
|
allowedToolCount: allowedTools.length,
|
|
207
220
|
}, 'runAgent: starting query');
|
|
@@ -212,76 +225,93 @@ export async function runAgent(prompt, opts) {
|
|
|
212
225
|
let subtype = 'unknown';
|
|
213
226
|
let usage;
|
|
214
227
|
const stream = query({ prompt: effectivePrompt, options: sdkOptions });
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
try {
|
|
229
|
+
for await (const message of stream) {
|
|
230
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
231
|
+
sessionId = message.session_id ?? '';
|
|
232
|
+
logger.debug({ sessionKey: opts.sessionKey, sdkSessionId: sessionId }, 'runAgent: SDK session initialized');
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (message.type === 'assistant') {
|
|
236
|
+
const am = message;
|
|
237
|
+
const blocks = (am.message?.content ?? []);
|
|
238
|
+
for (const block of blocks) {
|
|
239
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
240
|
+
finalText += block.text;
|
|
241
|
+
if (opts.onText) {
|
|
242
|
+
try {
|
|
243
|
+
await opts.onText(block.text);
|
|
244
|
+
}
|
|
245
|
+
catch { /* streaming is best-effort */ }
|
|
230
246
|
}
|
|
231
|
-
catch { /* streaming is best-effort */ }
|
|
232
247
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
else if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
249
|
+
if (opts.onToolActivity) {
|
|
250
|
+
try {
|
|
251
|
+
await opts.onToolActivity({ tool: block.name, input: block.input ?? {} });
|
|
252
|
+
}
|
|
253
|
+
catch { /* best-effort */ }
|
|
238
254
|
}
|
|
239
|
-
catch { /* best-effort */ }
|
|
240
255
|
}
|
|
241
256
|
}
|
|
257
|
+
continue;
|
|
242
258
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (r)
|
|
258
|
-
finalText = r;
|
|
259
|
-
}
|
|
260
|
-
// Mirror cost to usage_log. Same shape as the existing
|
|
261
|
-
// logQueryResult, but standalone so we don't depend on
|
|
262
|
-
// PersonalAssistant's instance state.
|
|
263
|
-
const modelUsage = result.modelUsage;
|
|
264
|
-
if (opts.memoryStore && modelUsage) {
|
|
265
|
-
try {
|
|
266
|
-
opts.memoryStore.logUsage({
|
|
267
|
-
sessionKey: `${source}:${opts.sessionKey}`,
|
|
268
|
-
source: `runagent.${source}`,
|
|
269
|
-
modelUsage,
|
|
270
|
-
numTurns,
|
|
271
|
-
durationMs: Date.now() - startedAt,
|
|
272
|
-
agentSlug: opts.profile?.slug,
|
|
273
|
-
totalCostUsd: totalCostUsd,
|
|
274
|
-
});
|
|
259
|
+
if (message.type === 'result') {
|
|
260
|
+
const result = message;
|
|
261
|
+
sessionId = sessionId || (result.session_id ?? '');
|
|
262
|
+
subtype = result.subtype ?? 'unknown';
|
|
263
|
+
numTurns = result.num_turns ?? numTurns;
|
|
264
|
+
totalCostUsd = result.total_cost_usd ?? 0;
|
|
265
|
+
const u = result.usage;
|
|
266
|
+
if (u)
|
|
267
|
+
usage = u;
|
|
268
|
+
if (subtype === 'success') {
|
|
269
|
+
// success carries `result` field with the final text.
|
|
270
|
+
const r = result.result;
|
|
271
|
+
if (r)
|
|
272
|
+
finalText = r;
|
|
275
273
|
}
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
// Mirror cost to usage_log. Same shape as the existing
|
|
275
|
+
// logQueryResult, but standalone so we don't depend on
|
|
276
|
+
// PersonalAssistant's instance state.
|
|
277
|
+
const modelUsage = result.modelUsage;
|
|
278
|
+
if (opts.memoryStore && modelUsage) {
|
|
279
|
+
try {
|
|
280
|
+
opts.memoryStore.logUsage({
|
|
281
|
+
sessionKey: `${source}:${opts.sessionKey}`,
|
|
282
|
+
source: `runagent.${source}`,
|
|
283
|
+
modelUsage,
|
|
284
|
+
numTurns,
|
|
285
|
+
durationMs: Date.now() - startedAt,
|
|
286
|
+
agentSlug: opts.profile?.slug,
|
|
287
|
+
totalCostUsd: totalCostUsd,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
logger.debug({ err }, 'runAgent: usage logging failed (non-fatal)');
|
|
292
|
+
}
|
|
278
293
|
}
|
|
294
|
+
continue;
|
|
279
295
|
}
|
|
280
|
-
|
|
296
|
+
// Other message types (UserMessage with tool_result, StreamEvent,
|
|
297
|
+
// SDKCompactBoundaryMessage) — observed but not acted on. The SDK
|
|
298
|
+
// handles compaction internally; we just let it run.
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
// Translate the SDK's budget-exhaustion throw into a message that
|
|
303
|
+
// tells the user (a) what cap tripped and (b) how to raise it.
|
|
304
|
+
// The raw SDK string ("Claude Code returned an error result:
|
|
305
|
+
// Reached maximum budget ($0.5)") leaks through the channel layer
|
|
306
|
+
// as a generic "Something went wrong:" with no actionable hint.
|
|
307
|
+
const msg = String(err?.message ?? err);
|
|
308
|
+
if (/Reached maximum budget|error_max_budget_usd/i.test(msg)) {
|
|
309
|
+
const cap = maxBudgetUsd?.toFixed(2) ?? '?';
|
|
310
|
+
const envKey = `BUDGET_${source.toUpperCase().replace(/-/g, '_')}_USD`;
|
|
311
|
+
throw new Error(`Hit the $${cap} ${source} budget cap before finishing. ` +
|
|
312
|
+
`Raise it in the dashboard (Budgets & Costs) or set ${envKey}=0 to remove caps.`);
|
|
281
313
|
}
|
|
282
|
-
|
|
283
|
-
// SDKCompactBoundaryMessage) — observed but not acted on. The SDK
|
|
284
|
-
// handles compaction internally; we just let it run.
|
|
314
|
+
throw err;
|
|
285
315
|
}
|
|
286
316
|
logger.info({
|
|
287
317
|
sessionKey: opts.sessionKey,
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -6949,7 +6949,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6949
6949
|
}
|
|
6950
6950
|
else if (preset === 'uncapped' || preset === 'off' || preset === 'none') {
|
|
6951
6951
|
writes = DASHBOARD_BUDGET_ROWS.map(row => ({ key: row.key, value: '0' }));
|
|
6952
|
-
message = 'Removed spend caps by setting all budget values to 0.
|
|
6952
|
+
message = 'Removed spend caps by setting all budget values to 0. Restart Clementine for the change to take effect on running workers. (1M context mode is separate — use Force 200K or Safe Recovery for 1M errors.)';
|
|
6953
6953
|
}
|
|
6954
6954
|
else {
|
|
6955
6955
|
res.status(400).json({ error: 'preset must be defaults or uncapped' });
|
package/dist/gateway/router.js
CHANGED
|
@@ -10,7 +10,7 @@ import pino from 'pino';
|
|
|
10
10
|
import { oneMillionContextRecoveryMessage, PersonalAssistant, } from '../agent/assistant.js';
|
|
11
11
|
import { runWithTrace, logAuditJsonl } from '../agent/hooks.js';
|
|
12
12
|
import { SelfImproveLoop } from '../agent/self-improve.js';
|
|
13
|
-
import { MODELS, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, } from '../config.js';
|
|
13
|
+
import { MODELS, BUDGET, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, } from '../config.js';
|
|
14
14
|
import { scanner } from '../security/scanner.js';
|
|
15
15
|
import { lanes } from './lanes.js';
|
|
16
16
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
@@ -1820,7 +1820,13 @@ export class Gateway {
|
|
|
1820
1820
|
// Builder cost knobs: Haiku is plenty for JSON drafting,
|
|
1821
1821
|
// tight budget, no tools surfaced in the system prompt.
|
|
1822
1822
|
const builderModel = isBuilderSession ? MODELS.haiku : effectiveModel;
|
|
1823
|
-
|
|
1823
|
+
// Builder stays tight ($0.10 — Haiku JSON drafting only).
|
|
1824
|
+
// Regular chat reads BUDGET.chat from config (env / clementine.json /
|
|
1825
|
+
// dashboard writes). 0 = uncapped — the runAgent layer omits the
|
|
1826
|
+
// SDK option entirely in that case.
|
|
1827
|
+
const chatBudget = isBuilderSession
|
|
1828
|
+
? 0.10
|
|
1829
|
+
: (BUDGET.chat > 0 ? BUDGET.chat : undefined);
|
|
1824
1830
|
const builderAllowedTools = isBuilderSession ? [] : undefined;
|
|
1825
1831
|
logger.info({
|
|
1826
1832
|
sessionKey: effectiveSessionKey,
|
|
@@ -1841,7 +1847,7 @@ export class Gateway {
|
|
|
1841
1847
|
memoryStore: this.assistant.getMemoryStore?.() ?? null,
|
|
1842
1848
|
...(builderModel ? { model: builderModel } : {}),
|
|
1843
1849
|
...(maxTurns ? { maxTurns } : {}),
|
|
1844
|
-
...(
|
|
1850
|
+
...(chatBudget !== undefined ? { maxBudgetUsd: chatBudget } : {}),
|
|
1845
1851
|
...(builderAllowedTools ? { allowedTools: builderAllowedTools } : {}),
|
|
1846
1852
|
...(chatSystemAppend ? { systemPromptAppend: chatSystemAppend } : {}),
|
|
1847
1853
|
...(priorSdkSessionId ? { resumeSessionId: priorSdkSessionId } : {}),
|