sentinelayer-cli 0.14.0 → 0.16.0
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/package.json +1 -1
- package/src/commands/session.js +48 -0
- package/src/session/recap.js +84 -5
- package/src/session/usage.js +53 -9
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -948,6 +948,50 @@ async function hydrateAfterCheckpointMutation(sessionId, { targetPath } = {}) {
|
|
|
948
948
|
}));
|
|
949
949
|
}
|
|
950
950
|
|
|
951
|
+
async function hydrateJoinBriefingContext(sessionId, { targetPath, limit = 100 } = {}) {
|
|
952
|
+
const normalizedLimit = Math.max(1, Math.min(200, parsePositiveInteger(limit, "limit", 100)));
|
|
953
|
+
try {
|
|
954
|
+
const remoteTail = await pollSessionEventsBefore(sessionId, {
|
|
955
|
+
targetPath,
|
|
956
|
+
limit: normalizedLimit,
|
|
957
|
+
timeoutMs: 15_000,
|
|
958
|
+
forceCircuitProbe: true,
|
|
959
|
+
});
|
|
960
|
+
if (!remoteTail?.ok) {
|
|
961
|
+
return {
|
|
962
|
+
ok: false,
|
|
963
|
+
reason: normalizeString(remoteTail?.reason) || "remote_tail_unavailable",
|
|
964
|
+
remoteEvents: 0,
|
|
965
|
+
appended: 0,
|
|
966
|
+
skipped: 0,
|
|
967
|
+
failed: 0,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const appended = await appendMissingRemoteEvents(sessionId, remoteTail.events, {
|
|
971
|
+
targetPath,
|
|
972
|
+
});
|
|
973
|
+
return {
|
|
974
|
+
ok: true,
|
|
975
|
+
reason: "",
|
|
976
|
+
remoteEvents: Array.isArray(remoteTail.events) ? remoteTail.events.length : 0,
|
|
977
|
+
appended: appended.appended,
|
|
978
|
+
skipped: appended.skipped,
|
|
979
|
+
failed: appended.failed,
|
|
980
|
+
cursor: remoteTail.cursor || null,
|
|
981
|
+
beforeSequence: remoteTail.beforeSequence || null,
|
|
982
|
+
};
|
|
983
|
+
} catch (error) {
|
|
984
|
+
return {
|
|
985
|
+
ok: false,
|
|
986
|
+
reason: normalizeString(error?.message) || "join_context_hydrate_failed",
|
|
987
|
+
remoteEvents: 0,
|
|
988
|
+
appended: 0,
|
|
989
|
+
skipped: 0,
|
|
990
|
+
failed: 0,
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
951
995
|
async function appendMissingRemoteEvents(sessionId, remoteEvents = [], { targetPath } = {}) {
|
|
952
996
|
const events = Array.isArray(remoteEvents) ? remoteEvents : [];
|
|
953
997
|
if (events.length === 0) {
|
|
@@ -1577,6 +1621,9 @@ export function registerSessionCommand(program) {
|
|
|
1577
1621
|
skipRemoteProbe: true,
|
|
1578
1622
|
remoteSession,
|
|
1579
1623
|
});
|
|
1624
|
+
const joinHydration = await hydrateJoinBriefingContext(normalizedSessionId, {
|
|
1625
|
+
targetPath,
|
|
1626
|
+
});
|
|
1580
1627
|
|
|
1581
1628
|
const explicitAgent = normalizeString(options.agent);
|
|
1582
1629
|
const agentSeed = explicitAgent || normalizeString(options.name);
|
|
@@ -1630,6 +1677,7 @@ export function registerSessionCommand(program) {
|
|
|
1630
1677
|
materializedLocalSession: localSession.materialized,
|
|
1631
1678
|
refreshedLocalSession: Boolean(localSession.refreshed),
|
|
1632
1679
|
verificationSource: verification.source,
|
|
1680
|
+
joinHydration,
|
|
1633
1681
|
eventCount: Number.isFinite(eventCount) ? eventCount : 0,
|
|
1634
1682
|
agentCount: Number.isFinite(agentCount) ? agentCount : 0,
|
|
1635
1683
|
lastActivityAt: lastActivityIso || null,
|
package/src/session/recap.js
CHANGED
|
@@ -7,6 +7,7 @@ import { dedupeSessionEvents } from "./event-identity.js";
|
|
|
7
7
|
import { resolveSessionPaths } from "./paths.js";
|
|
8
8
|
import { appendToStream, readStream } from "./stream.js";
|
|
9
9
|
import { getSession } from "./store.js";
|
|
10
|
+
import { aggregateSessionUsage } from "./usage.js";
|
|
10
11
|
|
|
11
12
|
const SENTI_AGENT_ID = "senti";
|
|
12
13
|
const SENTI_MODEL = "gpt-5.4-mini";
|
|
@@ -349,6 +350,7 @@ function buildRecapText({
|
|
|
349
350
|
activeLocks = 0,
|
|
350
351
|
pendingTasks = 0,
|
|
351
352
|
taskLedger = emptyTaskLedgerSummary(),
|
|
353
|
+
usageSummary = normalizeUsageSummary(),
|
|
352
354
|
snippets = [],
|
|
353
355
|
} = {}) {
|
|
354
356
|
const agentText =
|
|
@@ -360,8 +362,9 @@ function buildRecapText({
|
|
|
360
362
|
const pendingText =
|
|
361
363
|
pendingTasks > 0 ? `You have ${pendingTasks} pending task${pendingTasks === 1 ? "" : "s"}.` : "";
|
|
362
364
|
const taskText = buildTaskLedgerText(taskLedger);
|
|
365
|
+
const usageText = buildUsageLedgerText(usageSummary);
|
|
363
366
|
const snippetText = snippets.length > 0 ? `Recent: ${snippets.join(" | ")}` : "";
|
|
364
|
-
return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${taskText}. ${snippetText}`.replace(
|
|
367
|
+
return `While you were away: ${agentText}. ${findingText}. ${lockText}. ${pendingText} ${taskText}. ${usageText} ${snippetText}`.replace(
|
|
365
368
|
/\s+/g,
|
|
366
369
|
" "
|
|
367
370
|
).trim();
|
|
@@ -402,6 +405,70 @@ function buildTaskLedgerText(taskLedger = emptyTaskLedgerSummary()) {
|
|
|
402
405
|
.join(". ");
|
|
403
406
|
}
|
|
404
407
|
|
|
408
|
+
function roundCurrency(value) {
|
|
409
|
+
const normalized = Number(value || 0);
|
|
410
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
return Math.round(normalized * 1_000_000) / 1_000_000;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function normalizeUsageSummary(events = []) {
|
|
417
|
+
const aggregate = aggregateSessionUsage(events);
|
|
418
|
+
const totals = {
|
|
419
|
+
totalTokens: Number(aggregate.totals.totalTokens || 0),
|
|
420
|
+
inputTokens: Number(aggregate.totals.inputTokens || 0),
|
|
421
|
+
outputTokens: Number(aggregate.totals.outputTokens || 0),
|
|
422
|
+
costUsd: roundCurrency(aggregate.totals.costUsd),
|
|
423
|
+
interactions: Number(aggregate.totals.interactions || 0),
|
|
424
|
+
};
|
|
425
|
+
const topAgents = [...aggregate.perAgent.values()]
|
|
426
|
+
.filter(
|
|
427
|
+
(agent) =>
|
|
428
|
+
Number(agent.totalTokens || 0) > 0 ||
|
|
429
|
+
Number(agent.costUsd || 0) > 0 ||
|
|
430
|
+
Number(agent.interactions || 0) > 0,
|
|
431
|
+
)
|
|
432
|
+
.sort((left, right) => {
|
|
433
|
+
const costDelta = Number(right.costUsd || 0) - Number(left.costUsd || 0);
|
|
434
|
+
if (costDelta !== 0) return costDelta;
|
|
435
|
+
const tokenDelta = Number(right.totalTokens || 0) - Number(left.totalTokens || 0);
|
|
436
|
+
if (tokenDelta !== 0) return tokenDelta;
|
|
437
|
+
return normalizeString(left.agentId).localeCompare(normalizeString(right.agentId));
|
|
438
|
+
})
|
|
439
|
+
.slice(0, 3)
|
|
440
|
+
.map((agent) => ({
|
|
441
|
+
agentId: normalizeString(agent.agentId) || "unknown",
|
|
442
|
+
model: normalizeString(agent.model) || "unknown",
|
|
443
|
+
totalTokens: Number(agent.totalTokens || 0),
|
|
444
|
+
inputTokens: Number(agent.inputTokens || 0),
|
|
445
|
+
outputTokens: Number(agent.outputTokens || 0),
|
|
446
|
+
costUsd: roundCurrency(agent.costUsd),
|
|
447
|
+
interactions: Number(agent.interactions || 0),
|
|
448
|
+
}));
|
|
449
|
+
return { totals, topAgents };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function buildUsageLedgerText(usageSummary = {}) {
|
|
453
|
+
const totals = usageSummary.totals && typeof usageSummary.totals === "object" ? usageSummary.totals : {};
|
|
454
|
+
const totalTokens = Number(totals.totalTokens || 0);
|
|
455
|
+
const costUsd = roundCurrency(totals.costUsd);
|
|
456
|
+
if (totalTokens <= 0 && costUsd <= 0) {
|
|
457
|
+
return "";
|
|
458
|
+
}
|
|
459
|
+
const topAgents = Array.isArray(usageSummary.topAgents) ? usageSummary.topAgents : [];
|
|
460
|
+
const topText =
|
|
461
|
+
topAgents.length > 0
|
|
462
|
+
? ` Top agents: ${topAgents
|
|
463
|
+
.map(
|
|
464
|
+
(agent) =>
|
|
465
|
+
`${agent.agentId} ${Number(agent.totalTokens || 0).toLocaleString("en-US")} tokens/$${roundCurrency(agent.costUsd).toFixed(4)}`,
|
|
466
|
+
)
|
|
467
|
+
.join("; ")}.`
|
|
468
|
+
: "";
|
|
469
|
+
return `Usage: ${totalTokens.toLocaleString("en-US")} tokens / $${costUsd.toFixed(4)}.${topText}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
405
472
|
// Multi-agent session etiquette + read-path rules surfaced in the
|
|
406
473
|
// context_briefing payload an agent receives on first join. Web
|
|
407
474
|
// renders this as markdown (see sentinelayer-web Session.tsx
|
|
@@ -444,7 +511,14 @@ function buildPeriodicText(recap = {}) {
|
|
|
444
511
|
const lastActor = normalizeString(summary.lastActorId);
|
|
445
512
|
const actorText = lastActor ? `${lastActor} active` : "no active actor";
|
|
446
513
|
const taskText = buildTaskLedgerText(summary.taskLedger);
|
|
447
|
-
|
|
514
|
+
const usageText = buildUsageLedgerText({
|
|
515
|
+
totals: summary.usageTotals,
|
|
516
|
+
topAgents: summary.usageTopAgents,
|
|
517
|
+
});
|
|
518
|
+
return `Session active for ${elapsedMinutes}m. ${activeAgents} agents. ${totalFindings} findings. ${activeLocks} locks. ${taskText}. ${usageText} ${actorText}.`.replace(
|
|
519
|
+
/\s+/g,
|
|
520
|
+
" ",
|
|
521
|
+
).trim();
|
|
448
522
|
}
|
|
449
523
|
|
|
450
524
|
export async function buildSessionRecap(
|
|
@@ -475,9 +549,9 @@ export async function buildSessionRecap(
|
|
|
475
549
|
} catch {
|
|
476
550
|
sessionMetadata = null;
|
|
477
551
|
}
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
);
|
|
552
|
+
const sortedEvents = sortEventsByConversationTime(dedupeSessionEvents(allEvents), normalizedNow);
|
|
553
|
+
const usageSummary = normalizeUsageSummary(sortedEvents);
|
|
554
|
+
const events = sortedEvents.slice(-normalizedMaxEvents);
|
|
481
555
|
const visibleEvents = (Array.isArray(events) ? events : []).filter((event) => {
|
|
482
556
|
const agentId = normalizeString(event.agent?.id || event.agentId);
|
|
483
557
|
if (!agentId) {
|
|
@@ -524,6 +598,7 @@ export async function buildSessionRecap(
|
|
|
524
598
|
activeLocks,
|
|
525
599
|
pendingTasks,
|
|
526
600
|
taskLedger,
|
|
601
|
+
usageSummary,
|
|
527
602
|
snippets,
|
|
528
603
|
});
|
|
529
604
|
|
|
@@ -543,6 +618,8 @@ export async function buildSessionRecap(
|
|
|
543
618
|
activeLocks,
|
|
544
619
|
pendingTasksForAgent: pendingTasks,
|
|
545
620
|
taskLedger,
|
|
621
|
+
usageTotals: usageSummary.totals,
|
|
622
|
+
usageTopAgents: usageSummary.topAgents,
|
|
546
623
|
snippets,
|
|
547
624
|
elapsedMinutes,
|
|
548
625
|
windowElapsedMinutes,
|
|
@@ -589,6 +666,7 @@ export async function emitContextBriefing(
|
|
|
589
666
|
ephemeral: true,
|
|
590
667
|
style: RECAP_STYLE,
|
|
591
668
|
generatedAt: recap.generatedAt,
|
|
669
|
+
summary: recap.summary,
|
|
592
670
|
},
|
|
593
671
|
});
|
|
594
672
|
const persisted = await appendToStream(sessionId, event, {
|
|
@@ -773,6 +851,7 @@ export function emitPeriodicRecap(
|
|
|
773
851
|
ephemeral: true,
|
|
774
852
|
style: RECAP_STYLE,
|
|
775
853
|
generatedAt: nowIso,
|
|
854
|
+
summary: recap.summary,
|
|
776
855
|
},
|
|
777
856
|
});
|
|
778
857
|
const persisted = await appendToStream(state.sessionId, event, {
|
package/src/session/usage.js
CHANGED
|
@@ -55,6 +55,21 @@ function num(value) {
|
|
|
55
55
|
return Number.isFinite(v) && v >= 0 ? v : 0;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function plainObject(value) {
|
|
59
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function firstUsageNumber(payload = {}, keys = []) {
|
|
63
|
+
const usage = plainObject(payload.usage);
|
|
64
|
+
for (const key of keys) {
|
|
65
|
+
const direct = num(payload[key]);
|
|
66
|
+
if (direct > 0) return direct;
|
|
67
|
+
const nested = num(usage[key]);
|
|
68
|
+
if (nested > 0) return nested;
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
function clipText(text, max = 4000) {
|
|
59
74
|
const s = n(text);
|
|
60
75
|
if (s.length <= max) return s;
|
|
@@ -179,10 +194,36 @@ export function aggregateSessionUsage(events = []) {
|
|
|
179
194
|
const payload = event.payload || {};
|
|
180
195
|
const agentId = n(payload.agentId || event.agent?.id);
|
|
181
196
|
if (!agentId) continue;
|
|
197
|
+
const model = n(payload.model || event.agent?.model) || "unknown";
|
|
198
|
+
const inputTokens = firstUsageNumber(payload, [
|
|
199
|
+
"inputTokens",
|
|
200
|
+
"input_tokens",
|
|
201
|
+
"tokensIn",
|
|
202
|
+
"tokens_in",
|
|
203
|
+
]);
|
|
204
|
+
const outputTokens = firstUsageNumber(payload, [
|
|
205
|
+
"outputTokens",
|
|
206
|
+
"output_tokens",
|
|
207
|
+
"tokensOut",
|
|
208
|
+
"tokens_out",
|
|
209
|
+
]);
|
|
210
|
+
const explicitTotalTokens = firstUsageNumber(payload, [
|
|
211
|
+
"totalTokens",
|
|
212
|
+
"total_tokens",
|
|
213
|
+
"tokens",
|
|
214
|
+
]);
|
|
215
|
+
const totalTokens = explicitTotalTokens || inputTokens + outputTokens;
|
|
216
|
+
const costUsd = firstUsageNumber(payload, [
|
|
217
|
+
"costUsd",
|
|
218
|
+
"cost_usd",
|
|
219
|
+
"providerCostUsd",
|
|
220
|
+
"provider_cost_usd",
|
|
221
|
+
"cost",
|
|
222
|
+
]);
|
|
182
223
|
if (!perAgent.has(agentId)) {
|
|
183
224
|
perAgent.set(agentId, {
|
|
184
225
|
agentId,
|
|
185
|
-
model
|
|
226
|
+
model,
|
|
186
227
|
totalTokens: 0,
|
|
187
228
|
inputTokens: 0,
|
|
188
229
|
outputTokens: 0,
|
|
@@ -191,16 +232,19 @@ export function aggregateSessionUsage(events = []) {
|
|
|
191
232
|
});
|
|
192
233
|
}
|
|
193
234
|
const record = perAgent.get(agentId);
|
|
194
|
-
record.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
record.
|
|
235
|
+
if (record.model === "unknown" && model !== "unknown") {
|
|
236
|
+
record.model = model;
|
|
237
|
+
}
|
|
238
|
+
record.totalTokens += totalTokens;
|
|
239
|
+
record.inputTokens += inputTokens;
|
|
240
|
+
record.outputTokens += outputTokens;
|
|
241
|
+
record.costUsd += costUsd;
|
|
198
242
|
record.interactions += 1;
|
|
199
243
|
|
|
200
|
-
totals.totalTokens +=
|
|
201
|
-
totals.inputTokens +=
|
|
202
|
-
totals.outputTokens +=
|
|
203
|
-
totals.costUsd +=
|
|
244
|
+
totals.totalTokens += totalTokens;
|
|
245
|
+
totals.inputTokens += inputTokens;
|
|
246
|
+
totals.outputTokens += outputTokens;
|
|
247
|
+
totals.costUsd += costUsd;
|
|
204
248
|
totals.interactions += 1;
|
|
205
249
|
}
|
|
206
250
|
totals.costUsd = Math.round(totals.costUsd * 1_000_000) / 1_000_000;
|