sentinelayer-cli 0.15.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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
- return `Session active for ${elapsedMinutes}m. ${activeAgents} agents. ${totalFindings} findings. ${activeLocks} locks. ${taskText}. ${actorText}.`;
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 events = sortEventsByConversationTime(dedupeSessionEvents(allEvents), normalizedNow).slice(
479
- -normalizedMaxEvents,
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, {
@@ -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: n(payload.model || event.agent?.model) || "unknown",
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.totalTokens += num(payload.totalTokens);
195
- record.inputTokens += num(payload.inputTokens);
196
- record.outputTokens += num(payload.outputTokens);
197
- record.costUsd += num(payload.costUsd);
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 += num(payload.totalTokens);
201
- totals.inputTokens += num(payload.inputTokens);
202
- totals.outputTokens += num(payload.outputTokens);
203
- totals.costUsd += num(payload.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;