@curie-agent/daemon 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/channel-manager.d.ts.map +1 -1
- package/dist/src/channel-manager.js +1 -0
- package/dist/src/channel-manager.js.map +1 -1
- package/dist/src/jsonrpc-handler.d.ts +18 -1
- package/dist/src/jsonrpc-handler.d.ts.map +1 -1
- package/dist/src/jsonrpc-handler.js +183 -240
- package/dist/src/jsonrpc-handler.js.map +1 -1
- package/dist/src/ws-handler.d.ts.map +1 -1
- package/dist/src/ws-handler.js +1 -0
- package/dist/src/ws-handler.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/web/dist/assets/index-CyCuINoz.js +234 -0
- package/web/dist/assets/index-ulOOFddN.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-DV8ePK4N.css +0 -1
- package/web/dist/assets/index-De5KT5pu.js +0 -165
|
@@ -4,6 +4,7 @@ import { readFileSync, existsSync, readdirSync, statSync, writeFileSync, mkdirSy
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { Method, renderSlashCommandHelp, findSlashCommand } from '@curie-agent/protocol';
|
|
6
6
|
import { TurnLoop, parseReminderTime, listSnapshots, revertTo, createIdentityFilesAuto, PURE_TOOLS } from '@curie-agent/core';
|
|
7
|
+
import { compactMessages, reconstructMessagesFromEvents, resolveBudget, estimateRequestTokens, breakdownChars, fillPct, formatTokens, DEFAULT_CALIBRATION, DEFAULT_SETTINGS, } from '@curie-agent/core';
|
|
7
8
|
import { listSkills, discoverAllSkills } from '@curie-agent/tools';
|
|
8
9
|
import { executeCd } from './slash-cd.js';
|
|
9
10
|
/** Real package version — previously hardcoded and years out of date. */
|
|
@@ -305,10 +306,11 @@ export class JsonRpcHandler {
|
|
|
305
306
|
}
|
|
306
307
|
case Method.CONFIG_GET: {
|
|
307
308
|
const key = this.getStringParam(params, 'key');
|
|
308
|
-
if (!key)
|
|
309
|
-
return this.paramError('key');
|
|
310
309
|
const settings = this.settingsManager.get();
|
|
311
|
-
|
|
310
|
+
// '*' or omitted → the whole tree, in one consistent snapshot. Reading
|
|
311
|
+
// ~23 top-level keys one at a time can tear if another client writes
|
|
312
|
+
// mid-fetch, yielding a snapshot that never existed on disk.
|
|
313
|
+
result = !key || key === '*' ? settings : this.getNestedValue(settings, key);
|
|
312
314
|
break;
|
|
313
315
|
}
|
|
314
316
|
case Method.CONFIG_SET: {
|
|
@@ -340,14 +342,7 @@ export class JsonRpcHandler {
|
|
|
340
342
|
else {
|
|
341
343
|
this.settingsManager.update({ [key]: value });
|
|
342
344
|
}
|
|
343
|
-
this.
|
|
344
|
-
this.sharedEventBus?.emit({
|
|
345
|
-
type: 'config-changed',
|
|
346
|
-
id: Math.random().toString(36).substring(7),
|
|
347
|
-
timestamp: Date.now(),
|
|
348
|
-
key,
|
|
349
|
-
value,
|
|
350
|
-
});
|
|
345
|
+
this.emitConfigChanged(key, value);
|
|
351
346
|
if (key.startsWith('heartbeat') && this.daemonApp) {
|
|
352
347
|
const updatedSettings = this.settingsManager.get();
|
|
353
348
|
if (updatedSettings.heartbeat?.schedule === 'on') {
|
|
@@ -363,6 +358,17 @@ export class JsonRpcHandler {
|
|
|
363
358
|
this.daemonApp.taskManager.cancelAllHeartbeats();
|
|
364
359
|
}
|
|
365
360
|
}
|
|
361
|
+
// Keep the derived top-level `model` in sync with the active provider.
|
|
362
|
+
// SettingsManager.load() recomputes it from providers[current_provider].model,
|
|
363
|
+
// so a stale top-level value silently reverts on the next daemon start.
|
|
364
|
+
if (key === 'current_provider' || /^providers\.[^.]+\.model$/.test(key)) {
|
|
365
|
+
const synced = this.settingsManager.get();
|
|
366
|
+
const active = synced.providers[synced.current_provider];
|
|
367
|
+
if (active && synced.model !== active.model) {
|
|
368
|
+
this.settingsManager.update({ model: active.model });
|
|
369
|
+
this.emitConfigChanged('model', active.model);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
366
372
|
result = { status: 'ok', key, value };
|
|
367
373
|
break;
|
|
368
374
|
}
|
|
@@ -905,6 +911,7 @@ export class JsonRpcHandler {
|
|
|
905
911
|
'tool-result', 'approval-decision', 'usage',
|
|
906
912
|
'error', 'session-start', 'session-stop', 'hook', 'status',
|
|
907
913
|
'session-resumed', 'context-warning', 'thinking-delta',
|
|
914
|
+
'compaction', 'context-report',
|
|
908
915
|
// Subagent events
|
|
909
916
|
'agent-start', 'agent-text-delta', 'agent-thinking-delta',
|
|
910
917
|
'agent-tool-call', 'agent-tool-result', 'agent-usage',
|
|
@@ -918,7 +925,9 @@ export class JsonRpcHandler {
|
|
|
918
925
|
}
|
|
919
926
|
try {
|
|
920
927
|
const result = await loop.run(text);
|
|
921
|
-
|
|
928
|
+
// No post-run threshold check: TurnLoop enforces the budget before every
|
|
929
|
+
// provider call, on every code path (channels, tasks, heartbeat,
|
|
930
|
+
// subagents) — not just between prompts on this one.
|
|
922
931
|
return { status: 'completed', sessionId: result.sessionId, events: result.events.length };
|
|
923
932
|
}
|
|
924
933
|
catch (err) {
|
|
@@ -936,6 +945,21 @@ export class JsonRpcHandler {
|
|
|
936
945
|
paramError(key) {
|
|
937
946
|
return { jsonrpc: '2.0', id: 0, error: { code: -32602, message: `Missing required parameter: ${key}` } };
|
|
938
947
|
}
|
|
948
|
+
/**
|
|
949
|
+
* Broadcast a settings change so every connected client re-reads config.
|
|
950
|
+
* 'config-changed' is already in ws-handler's newEventTypes, so this reaches
|
|
951
|
+
* the browser. Every settings write must emit — a write that doesn't leaves
|
|
952
|
+
* the web dashboard showing a stale value until the user reloads.
|
|
953
|
+
*/
|
|
954
|
+
emitConfigChanged(key, value) {
|
|
955
|
+
this.sharedEventBus?.emit({
|
|
956
|
+
type: 'config-changed',
|
|
957
|
+
id: Math.random().toString(36).substring(7),
|
|
958
|
+
timestamp: Date.now(),
|
|
959
|
+
key,
|
|
960
|
+
value,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
939
963
|
/** Get a nested value from an object using dot notation (e.g. "providers.anthropic.model"). */
|
|
940
964
|
getNestedValue(obj, path) {
|
|
941
965
|
const parts = path.split('.');
|
|
@@ -1008,6 +1032,7 @@ export class JsonRpcHandler {
|
|
|
1008
1032
|
const pricing = pConfig?.model_cost
|
|
1009
1033
|
? `\`${pConfig.model_cost}\` (per million)`
|
|
1010
1034
|
: 'Not configured';
|
|
1035
|
+
const statusBudget = resolveBudget(settings);
|
|
1011
1036
|
const statusText = `### Curie Agent Status
|
|
1012
1037
|
* **Version:** \`${VERSION}\`
|
|
1013
1038
|
* **Active Model:** \`${settings.model}\`
|
|
@@ -1015,11 +1040,11 @@ export class JsonRpcHandler {
|
|
|
1015
1040
|
* **Approval Mode:** \`${settings.mode || 'auto'}\`
|
|
1016
1041
|
* **Reasoning Effort:** \`${settings.effort || 'auto'}\`
|
|
1017
1042
|
* **Workspace CWD:** \`${this.getSessionCwd(sessionId) || process.cwd()}\`
|
|
1018
|
-
* **Tools per Turn
|
|
1019
|
-
* **Web Search per Turn
|
|
1020
|
-
* **Model Context Window:** \`${(
|
|
1043
|
+
* **Tools per Turn / Run:** \`${String(settings.tools_per_call || DEFAULT_SETTINGS.tools_per_call)}\` / \`${String(settings.tools_per_run || DEFAULT_SETTINGS.tools_per_run)}\`
|
|
1044
|
+
* **Web Search per Turn / Run:** \`${String(settings.websearch_per_call || DEFAULT_SETTINGS.websearch_per_call)}\` / \`${String(settings.websearch_per_run || DEFAULT_SETTINGS.websearch_per_run)}\`
|
|
1045
|
+
* **Model Context Window:** \`${statusBudget.windowTokens.toLocaleString()} tokens\` (\`${statusBudget.usableTokens.toLocaleString()}\` usable, \`${statusBudget.reservedOutput.toLocaleString()}\` reserved for output)
|
|
1021
1046
|
* **Model Pricing:** ${pricing}
|
|
1022
|
-
* **Auto-Compaction:** \`${settings.auto_compact?.enabled
|
|
1047
|
+
* **Auto-Compaction:** \`${settings.auto_compact?.enabled ?? DEFAULT_SETTINGS.auto_compact.enabled}\` (warn \`${String(settings.auto_compact?.warn_threshold ?? DEFAULT_SETTINGS.auto_compact.warn_threshold)}%\`, suggest \`${String(settings.auto_compact?.threshold ?? DEFAULT_SETTINGS.auto_compact.threshold)}%\`, compact \`${String(settings.auto_compact?.forced_threshold ?? DEFAULT_SETTINGS.auto_compact.forced_threshold)}%\`)`;
|
|
1023
1048
|
emitDelta(statusText);
|
|
1024
1049
|
break;
|
|
1025
1050
|
}
|
|
@@ -1126,13 +1151,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1126
1151
|
else {
|
|
1127
1152
|
settings.theme = theme;
|
|
1128
1153
|
this.settingsManager.update(settings);
|
|
1129
|
-
this.
|
|
1130
|
-
type: 'config-changed',
|
|
1131
|
-
id: Math.random().toString(36).substring(7),
|
|
1132
|
-
timestamp: Date.now(),
|
|
1133
|
-
key: 'theme',
|
|
1134
|
-
value: theme,
|
|
1135
|
-
});
|
|
1154
|
+
this.emitConfigChanged('theme', theme);
|
|
1136
1155
|
emitDelta(`Successfully switched theme to: **${theme}**`);
|
|
1137
1156
|
}
|
|
1138
1157
|
break;
|
|
@@ -1149,6 +1168,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1149
1168
|
else {
|
|
1150
1169
|
settings.mode = args.toLowerCase();
|
|
1151
1170
|
this.settingsManager.update(settings);
|
|
1171
|
+
this.emitConfigChanged('mode', settings.mode);
|
|
1152
1172
|
emitDelta(`Successfully switched approval mode to: **${args.toLowerCase()}**`);
|
|
1153
1173
|
}
|
|
1154
1174
|
break;
|
|
@@ -1165,6 +1185,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1165
1185
|
else {
|
|
1166
1186
|
settings.effort = args.toLowerCase();
|
|
1167
1187
|
this.settingsManager.update(settings);
|
|
1188
|
+
this.emitConfigChanged('effort', settings.effort);
|
|
1168
1189
|
emitDelta(`Successfully switched reasoning effort to: **${args.toLowerCase()}**`);
|
|
1169
1190
|
}
|
|
1170
1191
|
break;
|
|
@@ -1192,8 +1213,11 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1192
1213
|
settings.websearch_per_call = wsVal;
|
|
1193
1214
|
}
|
|
1194
1215
|
}
|
|
1195
|
-
this.settingsManager.
|
|
1196
|
-
|
|
1216
|
+
this.settingsManager.update({
|
|
1217
|
+
tools_per_call: settings.tools_per_call,
|
|
1218
|
+
websearch_per_call: settings.websearch_per_call,
|
|
1219
|
+
});
|
|
1220
|
+
emitDelta(`Tool limits updated! Tools per turn: **${String(settings.tools_per_call)}**, WebSearch per turn: **${String(settings.websearch_per_call ?? 5)}**`);
|
|
1197
1221
|
}
|
|
1198
1222
|
}
|
|
1199
1223
|
break;
|
|
@@ -1212,9 +1236,8 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1212
1236
|
emitDelta(`Invalid value: "${args}". Must be a positive integer.`);
|
|
1213
1237
|
}
|
|
1214
1238
|
else {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
emitDelta(`WebSearch/WebFetch limit per turn set to: **${val}**`);
|
|
1239
|
+
this.settingsManager.update({ websearch_per_call: val });
|
|
1240
|
+
emitDelta(`WebSearch/WebFetch limit per turn set to: **${String(val)}**`);
|
|
1218
1241
|
}
|
|
1219
1242
|
}
|
|
1220
1243
|
break;
|
|
@@ -1343,122 +1366,121 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1343
1366
|
const sub = parts[0]?.toLowerCase();
|
|
1344
1367
|
const arg1 = parts[1]?.toLowerCase();
|
|
1345
1368
|
const arg2 = parts[2]?.toLowerCase();
|
|
1369
|
+
const d = DEFAULT_SETTINGS.auto_compact;
|
|
1370
|
+
/** Persist through update() — get() returns a deep clone, so mutating it is a no-op. */
|
|
1371
|
+
const saveAutoCompact = (patch) => {
|
|
1372
|
+
this.settingsManager.update({ auto_compact: { ...settings.auto_compact, ...patch } });
|
|
1373
|
+
};
|
|
1346
1374
|
if (sub === 'auto') {
|
|
1347
|
-
const
|
|
1348
|
-
if (!s.auto_compact) {
|
|
1349
|
-
s.auto_compact = { enabled: 'on', threshold: 80, warn_threshold: 15, forced_threshold: 85 };
|
|
1350
|
-
}
|
|
1375
|
+
const ac = settings.auto_compact ?? d;
|
|
1351
1376
|
if (!arg1) {
|
|
1352
|
-
emitDelta(`### Auto-Compaction Settings
|
|
1353
|
-
* **Auto-compact**: \`${s.auto_compact.enabled}\`
|
|
1354
|
-
* **Threshold**: \`${s.auto_compact.threshold ?? 80}%\`
|
|
1355
|
-
* **Warning Threshold**: \`${s.auto_compact.warn_threshold ?? 15}%\`
|
|
1356
|
-
* **Pricing Warn**: \`${s.pricing_tier_warn ?? 'off'}\`
|
|
1377
|
+
emitDelta(`### Auto-Compaction Settings
|
|
1357
1378
|
|
|
1358
|
-
**
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1379
|
+
* **Auto-compact**: \`${ac.enabled}\`
|
|
1380
|
+
* **Warn at**: \`${String(ac.warn_threshold ?? d.warn_threshold)}%\`
|
|
1381
|
+
* **Suggest at**: \`${String(ac.threshold ?? d.threshold)}%\`
|
|
1382
|
+
* **Compact at**: \`${String(ac.forced_threshold ?? d.forced_threshold)}%\`
|
|
1383
|
+
* **Summarizer model**: \`${ac.model || '(active model)'}\`
|
|
1384
|
+
* **Pricing warn**: \`${settings.pricing_tier_warn ?? 'off'}\`
|
|
1385
|
+
|
|
1386
|
+
**Usage**: \`/context auto [on|off|threshold N|warn N|forced N|pricing on/off]\``);
|
|
1364
1387
|
}
|
|
1365
|
-
else if (arg1 === 'off') {
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
emitDelta(`Autocompaction disabled.`);
|
|
1388
|
+
else if (arg1 === 'on' || arg1 === 'off') {
|
|
1389
|
+
saveAutoCompact({ enabled: arg1 });
|
|
1390
|
+
emitDelta(`Auto-compaction ${arg1 === 'on' ? 'enabled' : 'disabled'}.`);
|
|
1369
1391
|
}
|
|
1370
|
-
else if (arg1 === 'threshold' && arg2) {
|
|
1392
|
+
else if ((arg1 === 'threshold' || arg1 === 'warn' || arg1 === 'forced') && arg2) {
|
|
1371
1393
|
const pct = parseInt(arg2, 10);
|
|
1372
|
-
if (isNaN(pct) || pct <
|
|
1373
|
-
emitDelta(`Invalid
|
|
1394
|
+
if (isNaN(pct) || pct < 5 || pct > 99) {
|
|
1395
|
+
emitDelta(`Invalid value. Use a percentage between 5 and 99.`);
|
|
1374
1396
|
}
|
|
1375
1397
|
else {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
emitDelta(
|
|
1398
|
+
const key = arg1 === 'threshold' ? 'threshold' : arg1 === 'warn' ? 'warn_threshold' : 'forced_threshold';
|
|
1399
|
+
saveAutoCompact({ [key]: pct });
|
|
1400
|
+
emitDelta(`${arg1 === 'threshold' ? 'Suggest' : arg1 === 'warn' ? 'Warn' : 'Forced compaction'} threshold set to ${String(pct)}%.`);
|
|
1379
1401
|
}
|
|
1380
1402
|
}
|
|
1381
|
-
else if (arg1 === '
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
emitDelta(`Invalid warning threshold. Use a value between 5 and 95.`);
|
|
1385
|
-
}
|
|
1386
|
-
else {
|
|
1387
|
-
s.auto_compact.warn_threshold = pct;
|
|
1388
|
-
this.settingsManager.save();
|
|
1389
|
-
emitDelta(`Warning threshold set to ${pct}%.`);
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
else if (arg1 === 'pricing') {
|
|
1393
|
-
if (arg2 === 'on') {
|
|
1394
|
-
s.pricing_tier_warn = 'on';
|
|
1395
|
-
this.settingsManager.save();
|
|
1396
|
-
emitDelta(`Pricing tier warnings enabled.`);
|
|
1397
|
-
}
|
|
1398
|
-
else if (arg2 === 'off') {
|
|
1399
|
-
s.pricing_tier_warn = 'off';
|
|
1400
|
-
this.settingsManager.save();
|
|
1401
|
-
emitDelta(`Pricing tier warnings disabled.`);
|
|
1402
|
-
}
|
|
1403
|
-
else {
|
|
1404
|
-
emitDelta(`Usage: \`/context auto pricing on/off\``);
|
|
1405
|
-
}
|
|
1403
|
+
else if (arg1 === 'pricing' && (arg2 === 'on' || arg2 === 'off')) {
|
|
1404
|
+
this.settingsManager.update({ pricing_tier_warn: arg2 });
|
|
1405
|
+
emitDelta(`Pricing tier warnings ${arg2 === 'on' ? 'enabled' : 'disabled'}.`);
|
|
1406
1406
|
}
|
|
1407
1407
|
else {
|
|
1408
|
-
emitDelta(`Usage: \`/context auto [on|off|threshold N|warn N|pricing on/off]\``);
|
|
1408
|
+
emitDelta(`Usage: \`/context auto [on|off|threshold N|warn N|forced N|pricing on/off]\``);
|
|
1409
1409
|
}
|
|
1410
1410
|
}
|
|
1411
1411
|
else if (sub === 'compact') {
|
|
1412
|
-
emitDelta(
|
|
1412
|
+
emitDelta(`Compacting conversation history…`);
|
|
1413
1413
|
try {
|
|
1414
|
-
const
|
|
1415
|
-
emitDelta(`\n\n
|
|
1414
|
+
const r = await this.runAutomaticCompaction(sessionId);
|
|
1415
|
+
emitDelta(`\n\n**Compacted.** ${String(r.summarizedMessageCount)} message(s) summarized, ` +
|
|
1416
|
+
`~${formatTokens(r.estimatedTokensBefore)} → ~${formatTokens(r.estimatedTokensAfter)} tokens. ` +
|
|
1417
|
+
`The full transcript is preserved on disk.\n\n**Summary:**\n\n${r.summary}`);
|
|
1416
1418
|
}
|
|
1417
1419
|
catch (err) {
|
|
1418
|
-
emitDelta(`\n\n
|
|
1420
|
+
emitDelta(`\n\n**Compaction failed**: ${err instanceof Error ? err.message : String(err)}`);
|
|
1419
1421
|
}
|
|
1420
1422
|
}
|
|
1423
|
+
else if (sub === 'messages') {
|
|
1424
|
+
const messages = reconstructMessagesFromEvents(this.sessionStore.loadEvents(sessionId) || []);
|
|
1425
|
+
if (messages.length === 0) {
|
|
1426
|
+
emitDelta(`No messages in this session yet.`);
|
|
1427
|
+
break;
|
|
1428
|
+
}
|
|
1429
|
+
const lines = [`### Messages (${String(messages.length)})`, '', '| # | Role | Tokens | Detail |', '|---|---|---|---|'];
|
|
1430
|
+
messages.forEach((m, i) => {
|
|
1431
|
+
const tokens = estimateRequestTokens({ messages: [m] });
|
|
1432
|
+
const detail = m.role === 'tool'
|
|
1433
|
+
? (m.toolName ?? 'tool')
|
|
1434
|
+
: m.role === 'user'
|
|
1435
|
+
? `${String(m.content).slice(0, 60).replace(/[\n|]/g, ' ')}…`
|
|
1436
|
+
: m.content.map((b) => b.type === 'tool-use' ? `→ ${b.name}` : b.type).join(', ');
|
|
1437
|
+
lines.push(`| ${String(i + 1)} | ${m.role} | ${formatTokens(tokens)} | ${detail} |`);
|
|
1438
|
+
});
|
|
1439
|
+
emitDelta(lines.join('\n'));
|
|
1440
|
+
}
|
|
1421
1441
|
else {
|
|
1442
|
+
// Emit measurements, not markup. The TUI and the web dashboard each
|
|
1443
|
+
// render this natively — the hand-written HTML this replaces showed
|
|
1444
|
+
// a themed gauge in the browser and nothing at all in the terminal.
|
|
1445
|
+
const budget = resolveBudget(settings);
|
|
1422
1446
|
const activeLoop = this.turnLoops.get(sessionId);
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
const
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1447
|
+
const model = settings.providers[settings.current_provider]?.model || settings.model;
|
|
1448
|
+
const messages = activeLoop
|
|
1449
|
+
? activeLoop.getMessages()
|
|
1450
|
+
: reconstructMessagesFromEvents(this.sessionStore.loadEvents(sessionId) || []);
|
|
1451
|
+
const chars = breakdownChars({
|
|
1452
|
+
system: this.systemPrompt,
|
|
1453
|
+
messages,
|
|
1454
|
+
toolDefinitions: this.tools.map((t) => t.definition),
|
|
1455
|
+
});
|
|
1456
|
+
const toTokens = (c) => Math.ceil(c / DEFAULT_CALIBRATION);
|
|
1457
|
+
const breakdown = [
|
|
1458
|
+
{ label: 'System prompt', tokens: toTokens(chars.system) },
|
|
1459
|
+
{ label: 'Tool definitions', tokens: toTokens(chars.toolDefinitions) },
|
|
1460
|
+
{ label: 'Conversation', tokens: toTokens(chars.conversation) },
|
|
1461
|
+
{ label: 'Tool results', tokens: toTokens(chars.toolResults) },
|
|
1462
|
+
];
|
|
1463
|
+
const usedTokens = breakdown.reduce((sum, b) => sum + b.tokens, 0);
|
|
1464
|
+
const report = {
|
|
1465
|
+
type: 'context-report',
|
|
1466
|
+
id: crypto.randomUUID(),
|
|
1467
|
+
model,
|
|
1468
|
+
windowTokens: budget.windowTokens,
|
|
1469
|
+
usedTokens,
|
|
1470
|
+
reservedOutput: budget.reservedOutput,
|
|
1471
|
+
breakdown,
|
|
1472
|
+
timestamp: Date.now(),
|
|
1439
1473
|
};
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
emitDelta(`<div style="background:var(--s3);border-radius:8px;padding:12px;margin:8px 0">` +
|
|
1451
|
-
`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">` +
|
|
1452
|
-
`<span style="font-family:monospace;font-size:12px;color:var(--text)">Context Window (${model})</span>` +
|
|
1453
|
-
`<span style="font-family:monospace;font-size:13px;font-weight:bold;color:${barColor}">${pct}%</span>` +
|
|
1454
|
-
`</div>` +
|
|
1455
|
-
`<div style="background:var(--s2);border-radius:4px;height:8px;overflow:hidden">` +
|
|
1456
|
-
`<div style="width:${pct}%;height:100%;background:${barGradient};border-radius:4px"></div>` +
|
|
1457
|
-
`</div>` +
|
|
1458
|
-
`<div style="display:flex;justify-content:space-between;margin-top:8px;font-family:monospace;font-size:11px;color:var(--muted)">` +
|
|
1459
|
-
`<span>In: ${fmt(input)}</span><span>Out: ${fmt(output)}</span><span>Max: ${fmt(windowSize)}</span>` +
|
|
1460
|
-
`</div></div>`);
|
|
1461
|
-
}
|
|
1474
|
+
this.sharedEventBus?.emit({ ...report, sessionId });
|
|
1475
|
+
// Plain-text fallback so the command still says something useful on a
|
|
1476
|
+
// surface that has not yet learned the event.
|
|
1477
|
+
const pct = fillPct(usedTokens, budget);
|
|
1478
|
+
const rows = breakdown
|
|
1479
|
+
.map((b) => `* ${b.label}: ${formatTokens(b.tokens)} (${String(usedTokens > 0 ? Math.round((b.tokens / usedTokens) * 100) : 0)}%)`)
|
|
1480
|
+
.join('\n');
|
|
1481
|
+
emitDelta(`### Context Window — \`${model}\`\n\n` +
|
|
1482
|
+
`**${String(pct)}%** — ${formatTokens(usedTokens)} of ${formatTokens(budget.usableTokens)} usable ` +
|
|
1483
|
+
`(${formatTokens(budget.windowTokens)} window, ${formatTokens(budget.reservedOutput)} reserved for output)\n\n${rows}`);
|
|
1462
1484
|
}
|
|
1463
1485
|
break;
|
|
1464
1486
|
}
|
|
@@ -1562,7 +1584,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1562
1584
|
}
|
|
1563
1585
|
case 'enable': {
|
|
1564
1586
|
settings.heartbeat.schedule = 'on';
|
|
1565
|
-
this.settingsManager.
|
|
1587
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1566
1588
|
if (this.daemonApp) {
|
|
1567
1589
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1568
1590
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -1577,7 +1599,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1577
1599
|
}
|
|
1578
1600
|
case 'disable': {
|
|
1579
1601
|
settings.heartbeat.schedule = 'off';
|
|
1580
|
-
this.settingsManager.
|
|
1602
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1581
1603
|
if (this.daemonApp) {
|
|
1582
1604
|
this.daemonApp.taskManager.cancelAllHeartbeats();
|
|
1583
1605
|
}
|
|
@@ -1605,7 +1627,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1605
1627
|
}
|
|
1606
1628
|
else {
|
|
1607
1629
|
settings.heartbeat.intraday = tokens.join(',');
|
|
1608
|
-
this.settingsManager.
|
|
1630
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1609
1631
|
if (this.daemonApp && settings.heartbeat.schedule === 'on') {
|
|
1610
1632
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1611
1633
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -1633,7 +1655,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1633
1655
|
}
|
|
1634
1656
|
else {
|
|
1635
1657
|
settings.heartbeat.daily = rest;
|
|
1636
|
-
this.settingsManager.
|
|
1658
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1637
1659
|
if (this.daemonApp && settings.heartbeat.schedule === 'on') {
|
|
1638
1660
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1639
1661
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -1662,7 +1684,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1662
1684
|
}
|
|
1663
1685
|
else {
|
|
1664
1686
|
settings.heartbeat.weekly = rest;
|
|
1665
|
-
this.settingsManager.
|
|
1687
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1666
1688
|
if (this.daemonApp && settings.heartbeat.schedule === 'on') {
|
|
1667
1689
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1668
1690
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -1690,7 +1712,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1690
1712
|
}
|
|
1691
1713
|
else {
|
|
1692
1714
|
settings.heartbeat.monthly = rest;
|
|
1693
|
-
this.settingsManager.
|
|
1715
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1694
1716
|
if (this.daemonApp && settings.heartbeat.schedule === 'on') {
|
|
1695
1717
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1696
1718
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -1718,7 +1740,7 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
1718
1740
|
}
|
|
1719
1741
|
else {
|
|
1720
1742
|
settings.heartbeat.dreaming = rest;
|
|
1721
|
-
this.settingsManager.
|
|
1743
|
+
this.settingsManager.update({ heartbeat: settings.heartbeat });
|
|
1722
1744
|
if (this.daemonApp && settings.heartbeat.schedule === 'on') {
|
|
1723
1745
|
this.daemonApp.taskManager.rescheduleFromSettings({
|
|
1724
1746
|
HEARTBEAT_INTRADAY: settings.heartbeat.intraday,
|
|
@@ -2214,129 +2236,50 @@ Usage: \`/model window <tokens>\` (e.g., \`/model window 120000\`).`);
|
|
|
2214
2236
|
}
|
|
2215
2237
|
return true;
|
|
2216
2238
|
}
|
|
2217
|
-
|
|
2239
|
+
/**
|
|
2240
|
+
* Compact a session's history.
|
|
2241
|
+
*
|
|
2242
|
+
* The summarization itself lives in `@curie-agent/core` so the TurnLoop can
|
|
2243
|
+
* run it mid-run — this is the out-of-band entry point for `/context compact`
|
|
2244
|
+
* on an idle session.
|
|
2245
|
+
*
|
|
2246
|
+
* Nothing is deleted. A `compaction` marker is appended and message
|
|
2247
|
+
* reconstruction replays forward from it, so the full transcript survives for
|
|
2248
|
+
* the UI, resume and audit while the model carries only the summary.
|
|
2249
|
+
*/
|
|
2250
|
+
async runAutomaticCompaction(sessionId) {
|
|
2218
2251
|
if (!this.createProvider) {
|
|
2219
2252
|
throw new Error('No provider configured');
|
|
2220
2253
|
}
|
|
2221
2254
|
const settings = this.settingsManager.get();
|
|
2222
2255
|
const provider = this.createProvider(settings);
|
|
2223
|
-
const
|
|
2256
|
+
const activeModel = settings.providers[settings.current_provider]?.model || settings.model;
|
|
2224
2257
|
const events = this.sessionStore.loadEvents(sessionId) || [];
|
|
2225
2258
|
if (events.length === 0) {
|
|
2226
2259
|
throw new Error('No events to compact');
|
|
2227
2260
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
for (const e of events) {
|
|
2231
|
-
if (e.type === 'user-prompt' && e.text) {
|
|
2232
|
-
transcriptParts.push(`User: ${e.text}`);
|
|
2233
|
-
}
|
|
2234
|
-
else if (e.type === 'assistant-delta' && e.text) {
|
|
2235
|
-
const lastIdx = transcriptParts.length - 1;
|
|
2236
|
-
if (lastIdx >= 0 && transcriptParts[lastIdx]?.startsWith('Assistant:')) {
|
|
2237
|
-
transcriptParts[lastIdx] += e.text;
|
|
2238
|
-
}
|
|
2239
|
-
else {
|
|
2240
|
-
transcriptParts.push(`Assistant: ${e.text}`);
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
const transcript = transcriptParts.join('\n\n');
|
|
2245
|
-
if (!transcript.trim()) {
|
|
2261
|
+
const messages = reconstructMessagesFromEvents(events);
|
|
2262
|
+
if (messages.length === 0) {
|
|
2246
2263
|
throw new Error('No conversational history found to compact');
|
|
2247
2264
|
}
|
|
2248
|
-
const
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
const
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
text: `This is a continuation of a compacted conversation. Here is the high-fidelity summary of our session so far:\n\n${cleanSummary}\n\nLet's continue!`,
|
|
2267
|
-
cwd: process.cwd(),
|
|
2268
|
-
timestamp: Date.now() + 1,
|
|
2269
|
-
},
|
|
2270
|
-
{
|
|
2271
|
-
type: 'assistant-delta',
|
|
2272
|
-
id: crypto.randomUUID(),
|
|
2273
|
-
text: `Got it! I have fully restored our conversation summary and details. Let let me know what you would like to do next!`,
|
|
2274
|
-
timestamp: Date.now() + 2,
|
|
2275
|
-
},
|
|
2276
|
-
{
|
|
2277
|
-
type: 'assistant-stop',
|
|
2278
|
-
id: crypto.randomUUID(),
|
|
2279
|
-
timestamp: Date.now() + 3,
|
|
2280
|
-
}
|
|
2281
|
-
];
|
|
2282
|
-
const data = newEvents.map((e) => JSON.stringify(e)).join('\n') + '\n';
|
|
2283
|
-
writeFileSync(eventsPath, data, 'utf-8');
|
|
2284
|
-
return cleanSummary;
|
|
2285
|
-
}
|
|
2286
|
-
async checkContextThresholds(sessionId) {
|
|
2287
|
-
const settings = this.settingsManager.get();
|
|
2288
|
-
const history = this.sessionStore.loadEvents(sessionId) || [];
|
|
2289
|
-
const usageEvents = history.filter((e) => e.type === 'usage');
|
|
2290
|
-
const latestUsage = usageEvents[usageEvents.length - 1];
|
|
2291
|
-
const input = latestUsage?.inputTokens ?? 0;
|
|
2292
|
-
if (input === 0)
|
|
2293
|
-
return; // No token data yet
|
|
2294
|
-
const windowSize = settings.providers?.[settings.current_provider]?.model_context_window ?? 200000;
|
|
2295
|
-
const pct = Math.min(100, Math.round((input / windowSize) * 100));
|
|
2296
|
-
const autoCompact = settings.auto_compact || { enabled: 'on', threshold: 80, warn_threshold: 60, forced_threshold: 85 };
|
|
2297
|
-
const warnThresh = autoCompact.warn_threshold ?? 60;
|
|
2298
|
-
const compactThresh = autoCompact.threshold ?? 80;
|
|
2299
|
-
const forcedThresh = autoCompact.forced_threshold ?? 85;
|
|
2300
|
-
const enabled = autoCompact.enabled ?? 'on';
|
|
2301
|
-
if (pct >= forcedThresh && enabled === 'on') {
|
|
2302
|
-
try {
|
|
2303
|
-
const summary = await this.runAutomaticCompaction(sessionId, 'detailed');
|
|
2304
|
-
const successMessage = `⚡ **Auto-Compaction Executed Successfully!**\n\nContext usage was at **${pct}%** (forced threshold: **${forcedThresh}%**).\nWe have summarized the conversation, reducing the history size down to just ~500 tokens. The agent will continue seamlessly!\n\n**Restored Context Summary:**\n\n${summary}`;
|
|
2305
|
-
const warningEvent = {
|
|
2306
|
-
type: 'context-warning',
|
|
2307
|
-
id: crypto.randomUUID(),
|
|
2308
|
-
message: successMessage,
|
|
2309
|
-
timestamp: Date.now(),
|
|
2310
|
-
};
|
|
2311
|
-
this.sharedEventBus?.emit({ ...warningEvent, sessionId });
|
|
2312
|
-
this.sessionStore.appendEvent(sessionId, { ...warningEvent, sessionId });
|
|
2313
|
-
}
|
|
2314
|
-
catch (err) {
|
|
2315
|
-
console.error('[compaction] Auto-compaction failed:', err);
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
else if (pct >= compactThresh) {
|
|
2319
|
-
const suggestMessage = `⚠️ **Context Fill High (${pct}%)**\n\nYour context fill is at **${pct}%** (Threshold: **${compactThresh}%**). Suggesting conversation compaction.\n\nType \`/context compact\` to run compaction, summarize history, and free memory immediately!`;
|
|
2320
|
-
const warningEvent = {
|
|
2321
|
-
type: 'context-warning',
|
|
2322
|
-
id: crypto.randomUUID(),
|
|
2323
|
-
message: suggestMessage,
|
|
2324
|
-
timestamp: Date.now(),
|
|
2325
|
-
};
|
|
2326
|
-
this.sharedEventBus?.emit({ ...warningEvent, sessionId });
|
|
2327
|
-
this.sessionStore.appendEvent(sessionId, { ...warningEvent, sessionId });
|
|
2328
|
-
}
|
|
2329
|
-
else if (pct >= warnThresh) {
|
|
2330
|
-
const warnMessage = `⚠️ **Context Warning (${pct}%)**\n\nContext window is **${pct}%** full (Warning threshold: **${warnThresh}%**).`;
|
|
2331
|
-
const warningEvent = {
|
|
2332
|
-
type: 'context-warning',
|
|
2333
|
-
id: crypto.randomUUID(),
|
|
2334
|
-
message: warnMessage,
|
|
2335
|
-
timestamp: Date.now(),
|
|
2336
|
-
};
|
|
2337
|
-
this.sharedEventBus?.emit({ ...warningEvent, sessionId });
|
|
2338
|
-
this.sessionStore.appendEvent(sessionId, { ...warningEvent, sessionId });
|
|
2339
|
-
}
|
|
2265
|
+
const result = await compactMessages({
|
|
2266
|
+
messages,
|
|
2267
|
+
provider: provider,
|
|
2268
|
+
model: settings.auto_compact?.model || activeModel,
|
|
2269
|
+
budget: resolveBudget(settings),
|
|
2270
|
+
});
|
|
2271
|
+
const marker = {
|
|
2272
|
+
type: 'compaction',
|
|
2273
|
+
id: crypto.randomUUID(),
|
|
2274
|
+
summary: result.summary,
|
|
2275
|
+
summarizedMessageCount: result.summarizedMessageCount,
|
|
2276
|
+
tokensBefore: result.estimatedTokensBefore,
|
|
2277
|
+
tokensAfter: result.estimatedTokensAfter,
|
|
2278
|
+
timestamp: Date.now(),
|
|
2279
|
+
};
|
|
2280
|
+
this.sessionStore.appendEvent(sessionId, { ...marker, sessionId });
|
|
2281
|
+
this.sharedEventBus?.emit({ ...marker, sessionId });
|
|
2282
|
+
return result;
|
|
2340
2283
|
}
|
|
2341
2284
|
}
|
|
2342
2285
|
//# sourceMappingURL=jsonrpc-handler.js.map
|