sentinelayer-cli 0.34.6 → 0.34.7
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 +44 -8
- package/src/scan/generator.js +21 -12
- package/src/session/pricing-ledger.js +208 -1
- package/src/session/recap.js +66 -2
- package/src/session/transcript.js +27 -3
- package/src/session/usage-report.js +23 -4
- package/src/session/usage.js +2 -2
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -109,6 +109,7 @@ import {
|
|
|
109
109
|
} from "../session/rotating-log.js";
|
|
110
110
|
import { buildSessionRecap } from "../session/recap.js";
|
|
111
111
|
import { computeTranscriptStats } from "../session/transcript.js";
|
|
112
|
+
import { buildSessionUsageLedger } from "../session/pricing-ledger.js";
|
|
112
113
|
import {
|
|
113
114
|
buildSessionUsageReport,
|
|
114
115
|
buildSessionUsageReportFromLedgerPayload,
|
|
@@ -150,6 +151,22 @@ function normalizeString(value) {
|
|
|
150
151
|
return String(value || "").trim();
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
// A wake hook hands remote-influenced event fields to a local host command
|
|
155
|
+
// (as SL_WAKE_* env vars). Bound + strip each value so a crafted event cannot
|
|
156
|
+
// inject control chars / newlines into the child environment or blow it up.
|
|
157
|
+
const WAKE_VALUE_MAX_LENGTH = 512;
|
|
158
|
+
function sanitizeWakeValue(value) {
|
|
159
|
+
// Strip control chars (incl. NUL / newline) by codepoint so a crafted remote
|
|
160
|
+
// event cannot inject into the child env; a codepoint check avoids putting a
|
|
161
|
+
// control-char regex literal in source.
|
|
162
|
+
let out = "";
|
|
163
|
+
for (const ch of normalizeString(value)) {
|
|
164
|
+
const code = ch.codePointAt(0);
|
|
165
|
+
out += code < 0x20 || code === 0x7f ? " " : ch;
|
|
166
|
+
}
|
|
167
|
+
return out.slice(0, WAKE_VALUE_MAX_LENGTH);
|
|
168
|
+
}
|
|
169
|
+
|
|
153
170
|
function messagePartsHaveContent(messageParts) {
|
|
154
171
|
const parts = Array.isArray(messageParts) ? messageParts : [messageParts];
|
|
155
172
|
return parts.some((part) => normalizeString(part));
|
|
@@ -2160,14 +2177,28 @@ export function createSessionWakeRunner({
|
|
|
2160
2177
|
return;
|
|
2161
2178
|
}
|
|
2162
2179
|
busy = true;
|
|
2180
|
+
const rawSequence = event?.sequenceId ?? event?.sequence_id ?? "";
|
|
2181
|
+
const sanitizedSequence = sanitizeWakeValue(rawSequence);
|
|
2163
2182
|
const env = {
|
|
2164
2183
|
...process.env,
|
|
2165
|
-
SL_WAKE_SESSION_ID:
|
|
2166
|
-
SL_WAKE_AGENT_ID:
|
|
2167
|
-
SL_WAKE_EVENT_TYPE:
|
|
2168
|
-
SL_WAKE_EVENT_CURSOR:
|
|
2169
|
-
SL_WAKE_EVENT_SEQUENCE:
|
|
2170
|
-
SL_WAKE_ACTOR_ID:
|
|
2184
|
+
SL_WAKE_SESSION_ID: sanitizeWakeValue(sessionId),
|
|
2185
|
+
SL_WAKE_AGENT_ID: sanitizeWakeValue(agentId),
|
|
2186
|
+
SL_WAKE_EVENT_TYPE: sanitizeWakeValue(event?.event),
|
|
2187
|
+
SL_WAKE_EVENT_CURSOR: sanitizeWakeValue(event?.cursor),
|
|
2188
|
+
SL_WAKE_EVENT_SEQUENCE: sanitizedSequence,
|
|
2189
|
+
SL_WAKE_ACTOR_ID: sanitizeWakeValue(event?.agent?.id || event?.agentId),
|
|
2190
|
+
};
|
|
2191
|
+
// Metadata-only wake envelope for the host command's stdin. Deliberately
|
|
2192
|
+
// EXCLUDES the event body/content so session message text / PII never
|
|
2193
|
+
// reaches an external process — the woken agent re-reads the room over its
|
|
2194
|
+
// own authenticated channel using this cursor/sequence.
|
|
2195
|
+
const wakePayload = {
|
|
2196
|
+
event: env.SL_WAKE_EVENT_TYPE,
|
|
2197
|
+
sessionId: env.SL_WAKE_SESSION_ID,
|
|
2198
|
+
agentId: env.SL_WAKE_AGENT_ID,
|
|
2199
|
+
cursor: env.SL_WAKE_EVENT_CURSOR,
|
|
2200
|
+
sequenceId: typeof rawSequence === "number" && Number.isFinite(rawSequence) ? rawSequence : sanitizedSequence || null,
|
|
2201
|
+
actorId: env.SL_WAKE_ACTOR_ID,
|
|
2171
2202
|
};
|
|
2172
2203
|
let child;
|
|
2173
2204
|
try {
|
|
@@ -2185,7 +2216,7 @@ export function createSessionWakeRunner({
|
|
|
2185
2216
|
});
|
|
2186
2217
|
try {
|
|
2187
2218
|
if (child && child.stdin) {
|
|
2188
|
-
child.stdin.write(JSON.stringify(
|
|
2219
|
+
child.stdin.write(JSON.stringify(wakePayload));
|
|
2189
2220
|
child.stdin.end();
|
|
2190
2221
|
}
|
|
2191
2222
|
} catch {
|
|
@@ -4509,7 +4540,7 @@ export function registerSessionCommand(program) {
|
|
|
4509
4540
|
.option("--max-polls <n>", "Stop after N poll cycles (useful for tests and smoke checks)")
|
|
4510
4541
|
.option(
|
|
4511
4542
|
"--wake <command>",
|
|
4512
|
-
"Wake hook: run this shell command on each matched event (notify->resume bridge).
|
|
4543
|
+
"Wake hook: run this shell command on each matched event (notify->resume bridge). Metadata JSON is piped to stdin; sanitized SL_WAKE_* env vars are set.",
|
|
4513
4544
|
)
|
|
4514
4545
|
.option(
|
|
4515
4546
|
"--wake-host <name>",
|
|
@@ -6267,9 +6298,14 @@ export function registerSessionCommand(program) {
|
|
|
6267
6298
|
const hiddenControlEventCount =
|
|
6268
6299
|
dedupedEvents.length - transcriptBaseEvents.length + rawActionEvents.length - actionEvents.length;
|
|
6269
6300
|
const exportEvents = mergeSessionActionEvents(transcriptBaseEvents, actionEvents);
|
|
6301
|
+
const usageLedger = buildSessionUsageLedger(exportEvents, {
|
|
6302
|
+
sessionId: normalizedSessionId,
|
|
6303
|
+
includeEstimatedMessages: true,
|
|
6304
|
+
});
|
|
6270
6305
|
const stats = computeTranscriptStats({
|
|
6271
6306
|
sessionMeta: sessionPayload,
|
|
6272
6307
|
events: exportEvents,
|
|
6308
|
+
usageLedger,
|
|
6273
6309
|
});
|
|
6274
6310
|
const participants = buildSessionParticipants({
|
|
6275
6311
|
statsAgents: stats.agents,
|
package/src/scan/generator.js
CHANGED
|
@@ -227,16 +227,21 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
227
227
|
with: {
|
|
228
228
|
github_token: "${{ github.token }}",
|
|
229
229
|
openai_api_key: "${{ secrets.OPENAI_API_KEY }}",
|
|
230
|
-
google_api_key:
|
|
231
|
-
|
|
230
|
+
google_api_key:
|
|
231
|
+
"${{ secrets.GOOGLE_GEMINI_API_KEY != '' && secrets.GOOGLE_GEMINI_API_KEY || secrets.GOOGLE_API_KEY }}",
|
|
232
|
+
llm_provider:
|
|
233
|
+
"${{ secrets.OPENAI_API_KEY != '' && 'openai' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'google' || 'openai') }}",
|
|
232
234
|
sentinelayer_token: `\${{ secrets.${normalizedSecret} }}`,
|
|
233
|
-
sentinelayer_managed_llm: `\${{ secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
|
|
235
|
+
sentinelayer_managed_llm: `\${{ secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
|
|
234
236
|
scan_mode: profile.scanMode || "deep",
|
|
235
237
|
severity_gate: profile.severityGate || "P1",
|
|
236
|
-
model:
|
|
238
|
+
model:
|
|
239
|
+
"${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-5.3-codex') }}",
|
|
237
240
|
codex_model: "gpt-5.3-codex",
|
|
238
|
-
model_fallback:
|
|
239
|
-
|
|
241
|
+
model_fallback:
|
|
242
|
+
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-4.1-mini' }}",
|
|
243
|
+
use_codex:
|
|
244
|
+
"${{ secrets.OPENAI_API_KEY != '' || (secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '') }}",
|
|
240
245
|
codex_only: "false",
|
|
241
246
|
llm_failure_policy: "block",
|
|
242
247
|
max_daily_scans: "${{ vars.OMAR_MAX_DAILY_SCANS || '200' }}",
|
|
@@ -295,11 +300,15 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
295
300
|
OMAR_SEVERITY_GATE: "${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}",
|
|
296
301
|
OMAR_P2_MAX_ALLOWED: "${{ github.event_name == 'workflow_dispatch' && inputs.p2_max_allowed || '5' }}",
|
|
297
302
|
OMAR_OPENAI_KEY_PRESENT: "${{ secrets.OPENAI_API_KEY != '' }}",
|
|
298
|
-
OMAR_GOOGLE_KEY_PRESENT:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
+
OMAR_GOOGLE_KEY_PRESENT:
|
|
304
|
+
"${{ secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '' }}",
|
|
305
|
+
OMAR_MANAGED_LLM: `\${{ secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
|
|
306
|
+
OMAR_LLM_PROVIDER:
|
|
307
|
+
"${{ secrets.OPENAI_API_KEY != '' && 'openai' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'google' || 'openai') }}",
|
|
308
|
+
OMAR_MODEL:
|
|
309
|
+
"${{ secrets.OPENAI_API_KEY != '' && 'gpt-5.3-codex' || ((secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-5.3-codex') }}",
|
|
310
|
+
OMAR_MODEL_FALLBACK:
|
|
311
|
+
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-4.1-mini' }}",
|
|
303
312
|
},
|
|
304
313
|
run: [
|
|
305
314
|
"set -euo pipefail",
|
|
@@ -344,7 +353,7 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
344
353
|
" 'llm_provider': env('OMAR_LLM_PROVIDER', 'openai'),",
|
|
345
354
|
" 'model': env('OMAR_MODEL', 'gpt-5.3-codex'),",
|
|
346
355
|
" 'model_fallback': env('OMAR_MODEL_FALLBACK', 'gpt-4.1-mini'),",
|
|
347
|
-
" 'llm_route': '
|
|
356
|
+
" 'llm_route': 'openai_api_key' if bool_env('OMAR_OPENAI_KEY_PRESENT') else ('google_api_key' if bool_env('OMAR_GOOGLE_KEY_PRESENT') else 'sentinelayer_managed'),",
|
|
348
357
|
" 'google_key_present': bool_env('OMAR_GOOGLE_KEY_PRESENT'),",
|
|
349
358
|
" 'openai_key_present': bool_env('OMAR_OPENAI_KEY_PRESENT'),",
|
|
350
359
|
" 'managed_llm': bool_env('OMAR_MANAGED_LLM'),",
|
|
@@ -4,10 +4,12 @@ import {
|
|
|
4
4
|
DEFAULT_PRICE_BOOK_VERSION,
|
|
5
5
|
estimateModelCost,
|
|
6
6
|
} from "../cost/tracker.js";
|
|
7
|
+
import { estimateTokens } from "../cost/tokenizer.js";
|
|
7
8
|
|
|
8
9
|
const SESSION_USAGE_EVENT = "session_usage";
|
|
9
10
|
export const BILLING_SESSION_USAGE_SCHEMA = "billing/v1";
|
|
10
11
|
export const LOCAL_SESSION_USAGE_SCHEMA = "session_usage/local-v1";
|
|
12
|
+
export const ESTIMATED_MESSAGE_USAGE_SCHEMA = "session_usage/estimated-message-v1";
|
|
11
13
|
const LEGACY_SESSION_USAGE_SCHEMAS = new Set(["", "session_usage/v0"]);
|
|
12
14
|
const SUPPORTED_SESSION_USAGE_SCHEMAS = new Set([
|
|
13
15
|
BILLING_SESSION_USAGE_SCHEMA,
|
|
@@ -38,6 +40,19 @@ const USAGE_HINT_KEYS = [
|
|
|
38
40
|
"cost_usd",
|
|
39
41
|
"cost",
|
|
40
42
|
];
|
|
43
|
+
const ESTIMATED_MESSAGE_EVENT_KINDS = new Set([
|
|
44
|
+
"agent_response",
|
|
45
|
+
"session_message",
|
|
46
|
+
"session_say",
|
|
47
|
+
]);
|
|
48
|
+
const HUMAN_AGENT_IDS = new Set([
|
|
49
|
+
"cli-user",
|
|
50
|
+
"human",
|
|
51
|
+
"user",
|
|
52
|
+
"you",
|
|
53
|
+
]);
|
|
54
|
+
const DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_WINDOW_MS = 5 * 60 * 1000;
|
|
55
|
+
const DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_SEQUENCE_WINDOW = 3;
|
|
41
56
|
|
|
42
57
|
function n(value) {
|
|
43
58
|
return String(value == null ? "" : value).trim();
|
|
@@ -94,6 +109,12 @@ function roundUsd(value) {
|
|
|
94
109
|
return Math.round(Number(value || 0) * 1_000_000) / 1_000_000;
|
|
95
110
|
}
|
|
96
111
|
|
|
112
|
+
function positiveInteger(value, fallbackValue) {
|
|
113
|
+
const parsed = Number(value);
|
|
114
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallbackValue;
|
|
115
|
+
return Math.floor(parsed);
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
export function createSessionUsageLedgerId({
|
|
98
119
|
sessionId = "",
|
|
99
120
|
agentId = "",
|
|
@@ -116,6 +137,52 @@ function fallbackIdempotencyKey({ sessionId, event, agentId, action, model, tota
|
|
|
116
137
|
return `event:${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
|
|
117
138
|
}
|
|
118
139
|
|
|
140
|
+
function messageEstimateIdempotencyKey({ sessionId, event, agentId, model, text }) {
|
|
141
|
+
const payload = object(event?.payload);
|
|
142
|
+
const explicit =
|
|
143
|
+
n(payload.clientMessageId || payload.client_message_id) ||
|
|
144
|
+
n(event?.eventId || event?.event_id) ||
|
|
145
|
+
n(event?.idempotencyToken || event?.idempotency_token) ||
|
|
146
|
+
n(event?.cursor);
|
|
147
|
+
if (explicit) return `estimated-message:${explicit}`;
|
|
148
|
+
const sequence = n(event?.sequenceId ?? event?.sequence_id);
|
|
149
|
+
if (sequence) return `estimated-message:seq:${sequence}`;
|
|
150
|
+
const timestamp = n(event?.ts || event?.timestamp);
|
|
151
|
+
const source = [sessionId, timestamp, agentId, model, text].join("\x1f");
|
|
152
|
+
return `estimated-message:${createHash("sha256").update(source).digest("hex").slice(0, 32)}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isLikelyHumanAgent({ agentId = "", role = "", model = "" } = {}) {
|
|
156
|
+
const id = n(agentId).toLowerCase();
|
|
157
|
+
const normalizedRole = n(role).toLowerCase();
|
|
158
|
+
const normalizedModel = n(model).toLowerCase();
|
|
159
|
+
if (!id && !normalizedModel) return true;
|
|
160
|
+
if (HUMAN_AGENT_IDS.has(id)) return true;
|
|
161
|
+
if (id.startsWith("human-") || id.startsWith("user-")) return true;
|
|
162
|
+
if (normalizedModel === "human") return true;
|
|
163
|
+
const agentLikeRole = ["coder", "reviewer", "tester", "observer", "orchestrator"].includes(normalizedRole);
|
|
164
|
+
const agentLikeId = /(codex|claude|gpt|gemini|grok|senti|kai-chen|agent|bot|warden|architect|builder)/i.test(id);
|
|
165
|
+
const agentLikeModel = /(gpt|claude|gemini|grok|codex|sonnet|opus|haiku)/i.test(normalizedModel);
|
|
166
|
+
if (normalizedRole === "human") return true;
|
|
167
|
+
if (normalizedRole === "participant" && !agentLikeId && !agentLikeModel) return true;
|
|
168
|
+
if (!agentLikeRole && !agentLikeId && !agentLikeModel && (!normalizedModel || normalizedModel === "unknown")) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function responseText(payload = {}) {
|
|
175
|
+
if (payload.response && typeof payload.response === "object" && !Array.isArray(payload.response)) {
|
|
176
|
+
return n(payload.response.text);
|
|
177
|
+
}
|
|
178
|
+
return n(payload.response);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function messageText(event = {}) {
|
|
182
|
+
const payload = object(event?.payload);
|
|
183
|
+
return n(payload.message) || responseText(payload) || n(payload.text);
|
|
184
|
+
}
|
|
185
|
+
|
|
119
186
|
function providerCostFromPriceBook({ model, inputTokens, outputTokens, explicitProviderCost }) {
|
|
120
187
|
if (explicitProviderCost != null) {
|
|
121
188
|
return { providerCostUsd: explicitProviderCost, unpriced: false };
|
|
@@ -217,9 +284,139 @@ export function buildUsageLedgerEntry(
|
|
|
217
284
|
unpriced,
|
|
218
285
|
timestamp: n(event?.ts || event?.timestamp),
|
|
219
286
|
sequenceId: nonNegativeInt(event?.sequenceId ?? event?.sequence_id),
|
|
287
|
+
estimated: false,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function buildEstimatedMessageLedgerEntry(
|
|
292
|
+
event,
|
|
293
|
+
{
|
|
294
|
+
sessionId = "",
|
|
295
|
+
priceBookVersion = DEFAULT_PRICE_BOOK_VERSION,
|
|
296
|
+
billingTier = "estimated",
|
|
297
|
+
includeHumanMessages = false,
|
|
298
|
+
action = "estimated_agent_message",
|
|
299
|
+
} = {},
|
|
300
|
+
) {
|
|
301
|
+
const kind = n(event?.event || event?.type);
|
|
302
|
+
if (!ESTIMATED_MESSAGE_EVENT_KINDS.has(kind)) return null;
|
|
303
|
+
|
|
304
|
+
const payload = object(event?.payload);
|
|
305
|
+
if (hasUsageHints(payload) || hasUsageHints(payload.usage)) return null;
|
|
306
|
+
|
|
307
|
+
const text = messageText(event);
|
|
308
|
+
if (!text) return null;
|
|
309
|
+
|
|
310
|
+
const agent = object(event?.agent);
|
|
311
|
+
const agentId = n(agent.id || event?.agentId) || "unknown";
|
|
312
|
+
const model = n(agent.model || event?.agentModel || payload.model) || "unknown";
|
|
313
|
+
const role = n(agent.role || payload.role);
|
|
314
|
+
const lowerAgentId = agentId.toLowerCase();
|
|
315
|
+
if (lowerAgentId === "senti" || lowerAgentId === "kai-chen") {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
if (!includeHumanMessages && isLikelyHumanAgent({ agentId, role, model })) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const outputTokens = estimateTokens(text, {
|
|
323
|
+
model,
|
|
324
|
+
provider: n(agent.provider || payload.provider),
|
|
325
|
+
});
|
|
326
|
+
if (outputTokens <= 0) return null;
|
|
327
|
+
|
|
328
|
+
const actionName = n(action) || "estimated_agent_message";
|
|
329
|
+
const idempotencyKey = messageEstimateIdempotencyKey({
|
|
330
|
+
sessionId,
|
|
331
|
+
event,
|
|
332
|
+
agentId,
|
|
333
|
+
model,
|
|
334
|
+
text,
|
|
335
|
+
});
|
|
336
|
+
const ledgerEntryId = createSessionUsageLedgerId({
|
|
337
|
+
sessionId,
|
|
338
|
+
agentId,
|
|
339
|
+
action: actionName,
|
|
340
|
+
idempotencyKey,
|
|
341
|
+
});
|
|
342
|
+
const { providerCostUsd, unpriced } = providerCostFromPriceBook({
|
|
343
|
+
model,
|
|
344
|
+
inputTokens: 0,
|
|
345
|
+
outputTokens,
|
|
346
|
+
explicitProviderCost: null,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
ledgerEntryId,
|
|
351
|
+
idempotencyKey,
|
|
352
|
+
schema: ESTIMATED_MESSAGE_USAGE_SCHEMA,
|
|
353
|
+
sessionId: n(sessionId),
|
|
354
|
+
agentId,
|
|
355
|
+
action: actionName,
|
|
356
|
+
model,
|
|
357
|
+
priceBookVersion: n(priceBookVersion) || DEFAULT_PRICE_BOOK_VERSION,
|
|
358
|
+
billingTier: n(billingTier) || "estimated",
|
|
359
|
+
provider: n(agent.provider || payload.provider),
|
|
360
|
+
inputTokens: 0,
|
|
361
|
+
outputTokens,
|
|
362
|
+
totalTokens: outputTokens,
|
|
363
|
+
providerCostUsd: roundUsd(providerCostUsd),
|
|
364
|
+
customerCostUsd: null,
|
|
365
|
+
unpriced,
|
|
366
|
+
timestamp: n(event?.ts || event?.timestamp),
|
|
367
|
+
sequenceId: nonNegativeInt(event?.sequenceId ?? event?.sequence_id),
|
|
368
|
+
estimated: true,
|
|
220
369
|
};
|
|
221
370
|
}
|
|
222
371
|
|
|
372
|
+
function collectRealUsageAnchors(events = [], options = {}) {
|
|
373
|
+
const anchors = new Map();
|
|
374
|
+
for (const event of events) {
|
|
375
|
+
const entry = buildUsageLedgerEntry(event, options);
|
|
376
|
+
if (!entry || entry.estimated) continue;
|
|
377
|
+
if (!anchors.has(entry.agentId)) anchors.set(entry.agentId, []);
|
|
378
|
+
anchors.get(entry.agentId).push({
|
|
379
|
+
timestampMs: Date.parse(n(entry.timestamp)),
|
|
380
|
+
sequenceId: Number.isFinite(entry.sequenceId) ? entry.sequenceId : null,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return anchors;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function hasNearbyRealUsageAnchor(entry, anchors, options = {}) {
|
|
387
|
+
if (!entry || !entry.estimated || !anchors || anchors.size === 0) return false;
|
|
388
|
+
const agentAnchors = anchors.get(entry.agentId);
|
|
389
|
+
if (!agentAnchors || agentAnchors.length === 0) return false;
|
|
390
|
+
const windowMs = positiveInteger(
|
|
391
|
+
options.estimatedMessageUsageDedupeWindowMs,
|
|
392
|
+
DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_WINDOW_MS,
|
|
393
|
+
);
|
|
394
|
+
const sequenceWindow = positiveInteger(
|
|
395
|
+
options.estimatedMessageUsageDedupeSequenceWindow,
|
|
396
|
+
DEFAULT_ESTIMATED_MESSAGE_USAGE_DEDUPE_SEQUENCE_WINDOW,
|
|
397
|
+
);
|
|
398
|
+
const entryTimestampMs = Date.parse(n(entry.timestamp));
|
|
399
|
+
const hasEntryTimestamp = Number.isFinite(entryTimestampMs);
|
|
400
|
+
const entrySequenceId = Number.isFinite(entry.sequenceId) ? entry.sequenceId : null;
|
|
401
|
+
return agentAnchors.some((anchor) => {
|
|
402
|
+
if (
|
|
403
|
+
entrySequenceId !== null &&
|
|
404
|
+
Number.isFinite(anchor.sequenceId) &&
|
|
405
|
+
Math.abs(entrySequenceId - anchor.sequenceId) <= sequenceWindow
|
|
406
|
+
) {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
if (
|
|
410
|
+
hasEntryTimestamp &&
|
|
411
|
+
Number.isFinite(anchor.timestampMs) &&
|
|
412
|
+
Math.abs(entryTimestampMs - anchor.timestampMs) <= windowMs
|
|
413
|
+
) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
223
420
|
function newRollup(label) {
|
|
224
421
|
return {
|
|
225
422
|
label,
|
|
@@ -231,6 +428,7 @@ function newRollup(label) {
|
|
|
231
428
|
customerCostUsd: 0,
|
|
232
429
|
hasCustomerCost: false,
|
|
233
430
|
unpriced: 0,
|
|
431
|
+
estimatedEntries: 0,
|
|
234
432
|
};
|
|
235
433
|
}
|
|
236
434
|
|
|
@@ -245,6 +443,7 @@ function addToRollup(rollup, entry) {
|
|
|
245
443
|
rollup.hasCustomerCost = true;
|
|
246
444
|
}
|
|
247
445
|
if (entry.unpriced) rollup.unpriced += 1;
|
|
446
|
+
if (entry.estimated) rollup.estimatedEntries += 1;
|
|
248
447
|
}
|
|
249
448
|
|
|
250
449
|
function finalizeRollup(rollup) {
|
|
@@ -257,6 +456,9 @@ export function buildSessionUsageLedger(events = [], options = {}) {
|
|
|
257
456
|
if (!Array.isArray(events)) {
|
|
258
457
|
throw new Error("events must be an array.");
|
|
259
458
|
}
|
|
459
|
+
const realUsageAnchors = options.includeEstimatedMessages
|
|
460
|
+
? collectRealUsageAnchors(events, options)
|
|
461
|
+
: new Map();
|
|
260
462
|
const entries = [];
|
|
261
463
|
const totals = newRollup("session");
|
|
262
464
|
const perAgent = new Map();
|
|
@@ -266,8 +468,13 @@ export function buildSessionUsageLedger(events = [], options = {}) {
|
|
|
266
468
|
let duplicatesSkipped = 0;
|
|
267
469
|
|
|
268
470
|
for (const event of events) {
|
|
269
|
-
const entry =
|
|
471
|
+
const entry =
|
|
472
|
+
buildUsageLedgerEntry(event, options) ||
|
|
473
|
+
(options.includeEstimatedMessages
|
|
474
|
+
? buildEstimatedMessageLedgerEntry(event, options)
|
|
475
|
+
: null);
|
|
270
476
|
if (!entry) continue;
|
|
477
|
+
if (hasNearbyRealUsageAnchor(entry, realUsageAnchors, options)) continue;
|
|
271
478
|
const dedupeKeys = [
|
|
272
479
|
entry.idempotencyKey ? `idem:${entry.idempotencyKey}` : "",
|
|
273
480
|
entry.ledgerEntryId ? `ledger:${entry.ledgerEntryId}` : "",
|
package/src/session/recap.js
CHANGED
|
@@ -22,6 +22,23 @@ const DEFAULT_TASK_SUMMARY_LIMIT = 3;
|
|
|
22
22
|
const DEFAULT_WORK_PLAN_SUMMARY_LIMIT = 5;
|
|
23
23
|
const MAX_WORK_PLAN_BYTES = 128_000;
|
|
24
24
|
const WORK_PLAN_RELATIVE_PATH = "tasks/todo.md";
|
|
25
|
+
const HISTORICAL_WORK_PLAN_MIN_COMPLETED = 25;
|
|
26
|
+
const HISTORICAL_WORK_PLAN_MIN_TOTAL = 50;
|
|
27
|
+
const HISTORICAL_WORK_PLAN_COMPLETED_RATIO = 0.75;
|
|
28
|
+
const GENERIC_WORK_PLAN_SECTIONS = new Set([
|
|
29
|
+
"plan",
|
|
30
|
+
"plans",
|
|
31
|
+
"todo",
|
|
32
|
+
"todos",
|
|
33
|
+
"to do",
|
|
34
|
+
"tasks",
|
|
35
|
+
"work items",
|
|
36
|
+
"workitems",
|
|
37
|
+
"workstream",
|
|
38
|
+
"workstreams",
|
|
39
|
+
"backlog",
|
|
40
|
+
"next",
|
|
41
|
+
]);
|
|
25
42
|
const RECAP_SOURCE_IGNORED_EVENTS = new Set([
|
|
26
43
|
"agent_heartbeat",
|
|
27
44
|
"agent_join",
|
|
@@ -458,6 +475,41 @@ function shortWorkPlanText(value) {
|
|
|
458
475
|
return `${text.slice(0, 97)}...`;
|
|
459
476
|
}
|
|
460
477
|
|
|
478
|
+
function normalizedWorkPlanSection(value = "") {
|
|
479
|
+
return normalizeString(value)
|
|
480
|
+
.toLowerCase()
|
|
481
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
482
|
+
.replace(/\s+/g, " ")
|
|
483
|
+
.trim();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function isGenericWorkPlanSection(value = "") {
|
|
487
|
+
const normalized = normalizedWorkPlanSection(value);
|
|
488
|
+
if (!normalized) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
if (GENERIC_WORK_PLAN_SECTIONS.has(normalized)) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
return /^(active\s+)?(plan|todo|tasks|backlog)(\s+\d+)?$/.test(normalized);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function shouldSuppressHistoricalWorkPlanDetails(summary = emptyWorkPlanSummary()) {
|
|
498
|
+
const total = Number(summary.total || 0);
|
|
499
|
+
const open = Number(summary.open || 0);
|
|
500
|
+
const completed = Number(summary.completed || 0);
|
|
501
|
+
if (summary.truncated || open <= 0 || completed < HISTORICAL_WORK_PLAN_MIN_COMPLETED) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
if (total < HISTORICAL_WORK_PLAN_MIN_TOTAL) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
if (!isGenericWorkPlanSection(summary.currentSection)) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
return completed / Math.max(1, total) >= HISTORICAL_WORK_PLAN_COMPLETED_RATIO;
|
|
511
|
+
}
|
|
512
|
+
|
|
461
513
|
function summarizeWorkPlanMarkdown(
|
|
462
514
|
raw = "",
|
|
463
515
|
{ limit = DEFAULT_WORK_PLAN_SUMMARY_LIMIT, truncated = false, sourceContext = {} } = {}
|
|
@@ -507,6 +559,10 @@ function summarizeWorkPlanMarkdown(
|
|
|
507
559
|
.filter((record) => record.status === "open")
|
|
508
560
|
.slice(-normalizedLimit);
|
|
509
561
|
summary.recent = records.slice(-normalizedLimit);
|
|
562
|
+
if (!summary.detailSuppressed && shouldSuppressHistoricalWorkPlanDetails(summary)) {
|
|
563
|
+
summary.detailSuppressed = true;
|
|
564
|
+
summary.detailSuppressionReason = "historical_generic_plan_section";
|
|
565
|
+
}
|
|
510
566
|
if (summary.detailSuppressed) {
|
|
511
567
|
summary.currentSection = "";
|
|
512
568
|
summary.recentOpen = [];
|
|
@@ -785,7 +841,15 @@ function buildWorkPlanText(workPlan = emptyWorkPlanSummary()) {
|
|
|
785
841
|
const sourceReason = normalizeString(workPlan.sourceReason);
|
|
786
842
|
const sourceReasonText = sourceReason ? ` (${sourceReason})` : "";
|
|
787
843
|
if (workPlan.detailSuppressed || workPlan.truncated) {
|
|
788
|
-
|
|
844
|
+
const suppressionReason = normalizeString(workPlan.detailSuppressionReason);
|
|
845
|
+
const sourceWindowText = workPlan.truncated
|
|
846
|
+
? `recent ${sourceText} window`
|
|
847
|
+
: sourceText;
|
|
848
|
+
const suppressionText =
|
|
849
|
+
suppressionReason === "historical_generic_plan_section"
|
|
850
|
+
? "Current/next items suppressed because this generic plan is mostly historical completed work."
|
|
851
|
+
: "Current/next items suppressed because the plan file is large.";
|
|
852
|
+
return `Plan: ${open} open / ${completed} done in ${sourceWindowText}${sourceReasonText}. ${suppressionText}`;
|
|
789
853
|
}
|
|
790
854
|
const currentSection = normalizeString(workPlan.currentSection);
|
|
791
855
|
const currentText = currentSection ? ` Current: ${currentSection}.` : "";
|
|
@@ -814,7 +878,7 @@ function roundCurrency(value) {
|
|
|
814
878
|
}
|
|
815
879
|
|
|
816
880
|
function normalizeUsageSummary(events = []) {
|
|
817
|
-
const aggregate = aggregateSessionUsage(events);
|
|
881
|
+
const aggregate = aggregateSessionUsage(events, { includeEstimatedMessages: true });
|
|
818
882
|
const totals = {
|
|
819
883
|
totalTokens: Number(aggregate.totals.totalTokens || 0),
|
|
820
884
|
inputTokens: Number(aggregate.totals.inputTokens || 0),
|
|
@@ -425,6 +425,7 @@ export function computeTranscriptStats({
|
|
|
425
425
|
usageEntries: resolvedUsageLedger.entries.length,
|
|
426
426
|
duplicatesSkipped: resolvedUsageLedger.duplicatesSkipped,
|
|
427
427
|
unpriced: resolvedUsageLedger.totals.unpriced,
|
|
428
|
+
estimatedEntries: resolvedUsageLedger.totals.estimatedEntries,
|
|
428
429
|
priceBookVersions: resolvedUsageLedger.priceBookVersions,
|
|
429
430
|
},
|
|
430
431
|
sentiActions,
|
|
@@ -504,6 +505,12 @@ function rollupsByCostAndTokens(map) {
|
|
|
504
505
|
));
|
|
505
506
|
}
|
|
506
507
|
|
|
508
|
+
function estimateNote(count) {
|
|
509
|
+
const normalized = Math.max(0, Math.floor(Number(count || 0)));
|
|
510
|
+
if (normalized <= 0) return "";
|
|
511
|
+
return `${normalized.toLocaleString("en-US")} estimated`;
|
|
512
|
+
}
|
|
513
|
+
|
|
507
514
|
function recentUsageEntries(entries, limit = 10) {
|
|
508
515
|
return entries
|
|
509
516
|
.map((entry, index) => ({ entry, index }))
|
|
@@ -561,7 +568,15 @@ function appendUsageLedgerSection(lines, usageLedger) {
|
|
|
561
568
|
);
|
|
562
569
|
lines.push(`Provider cost: ${usdCell(totals.providerCostUsd)} · ${customerCostText}`);
|
|
563
570
|
lines.push(`Price books: ${usageLedger.priceBookVersions.map(tableText).filter(Boolean).join(", ") || "—"}`);
|
|
564
|
-
|
|
571
|
+
const estimateText = totals.estimatedEntries > 0
|
|
572
|
+
? ` · Estimated entries: ${intCell(totals.estimatedEntries)}`
|
|
573
|
+
: "";
|
|
574
|
+
lines.push(
|
|
575
|
+
`Duplicates skipped: ${intCell(usageLedger.duplicatesSkipped)} · Unpriced entries: ${intCell(totals.unpriced)}${estimateText}`,
|
|
576
|
+
);
|
|
577
|
+
if (totals.estimatedEntries > 0) {
|
|
578
|
+
lines.push("_Estimated entries are output-text estimates from non-human session messages, not billing-grade provider usage._");
|
|
579
|
+
}
|
|
565
580
|
lines.push("");
|
|
566
581
|
|
|
567
582
|
appendUsageRollupTable(lines, {
|
|
@@ -580,8 +595,11 @@ function appendUsageLedgerSection(lines, usageLedger) {
|
|
|
580
595
|
lines.push("| Time | Agent | Action | Model | Tokens | Provider cost | Customer cost | Idempotency key |");
|
|
581
596
|
lines.push("|---|---|---|---|---:|---:|---:|---|");
|
|
582
597
|
for (const entry of recentUsageEntries(usageLedger.entries)) {
|
|
598
|
+
const action = entry.estimated
|
|
599
|
+
? `${entry.action} (${estimateNote(1)})`
|
|
600
|
+
: entry.action;
|
|
583
601
|
lines.push(
|
|
584
|
-
`| ${tableText(timestampOnly(entry.timestamp)) || "—"} | ${codeCell(entry.agentId)} | ${codeCell(
|
|
602
|
+
`| ${tableText(timestampOnly(entry.timestamp)) || "—"} | ${codeCell(entry.agentId)} | ${codeCell(action)} | ${codeCell(entry.model)} | ${intCell(entry.totalTokens)} | ${usdCell(entry.providerCostUsd)} | ${optionalUsdCell(entry.customerCostUsd, entry.customerCostUsd != null)} | ${codeCell(shortenIdempotencyKey(entry.idempotencyKey))} |`,
|
|
585
603
|
);
|
|
586
604
|
}
|
|
587
605
|
lines.push("");
|
|
@@ -608,8 +626,10 @@ export function buildTranscriptMarkdown({
|
|
|
608
626
|
speakerProfiles = new Map(),
|
|
609
627
|
options = {},
|
|
610
628
|
} = {}) {
|
|
629
|
+
const estimateMessageUsage = options.estimateMessageUsage !== false;
|
|
611
630
|
const usageLedger = buildSessionUsageLedger(events, {
|
|
612
631
|
sessionId: normalize(sessionMeta.sessionId),
|
|
632
|
+
includeEstimatedMessages: estimateMessageUsage,
|
|
613
633
|
});
|
|
614
634
|
const includeSystemEvents = options.includeSystemEvents !== false;
|
|
615
635
|
const stats = computeTranscriptStats({ sessionMeta, events, speakerProfiles, usageLedger });
|
|
@@ -628,8 +648,12 @@ export function buildTranscriptMarkdown({
|
|
|
628
648
|
stats.totals.customerCostTotalUsd == null
|
|
629
649
|
? ""
|
|
630
650
|
: ` · Billable: $${stats.totals.customerCostTotalUsd.toFixed(4)}`;
|
|
651
|
+
const estimatedText =
|
|
652
|
+
stats.totals.estimatedEntries > 0
|
|
653
|
+
? ` · Estimated entries: ${stats.totals.estimatedEntries.toLocaleString("en-US")}`
|
|
654
|
+
: "";
|
|
631
655
|
lines.push(
|
|
632
|
-
`Tokens: ${stats.totals.tokenTotal.toLocaleString("en-US")} · Cost: $${stats.totals.costTotalUsd.toFixed(4)}${billableText}`,
|
|
656
|
+
`Tokens: ${stats.totals.tokenTotal.toLocaleString("en-US")} · Cost: $${stats.totals.costTotalUsd.toFixed(4)}${billableText}${estimatedText}`,
|
|
633
657
|
);
|
|
634
658
|
}
|
|
635
659
|
lines.push("");
|
|
@@ -116,6 +116,7 @@ function rollupToJson(rollup) {
|
|
|
116
116
|
providerCostUsd: rollup.providerCostUsd,
|
|
117
117
|
customerCostUsd: rollup.hasCustomerCost ? rollup.customerCostUsd : null,
|
|
118
118
|
unpriced: rollup.unpriced,
|
|
119
|
+
estimatedEntries: rollup.estimatedEntries || 0,
|
|
119
120
|
};
|
|
120
121
|
}
|
|
121
122
|
|
|
@@ -133,6 +134,7 @@ function hostedRollupToJson(rollup, fallbackLabel = "") {
|
|
|
133
134
|
? money(bag.customerCostUsd ?? bag.customer_cost_usd ?? bag.billableCostUsd ?? bag.billable_cost_usd) ?? 0
|
|
134
135
|
: null,
|
|
135
136
|
unpriced: nonNegativeInt(bag.unpriced),
|
|
137
|
+
estimatedEntries: nonNegativeInt(bag.estimatedEntries ?? bag.estimated_entries),
|
|
136
138
|
};
|
|
137
139
|
}
|
|
138
140
|
|
|
@@ -155,6 +157,7 @@ function entryToJson(entry) {
|
|
|
155
157
|
providerCostUsd: entry.providerCostUsd,
|
|
156
158
|
customerCostUsd: entry.customerCostUsd,
|
|
157
159
|
unpriced: entry.unpriced,
|
|
160
|
+
estimated: Boolean(entry.estimated),
|
|
158
161
|
};
|
|
159
162
|
}
|
|
160
163
|
|
|
@@ -181,6 +184,7 @@ function hostedEntryToJson(entry) {
|
|
|
181
184
|
providerCostUsd: money(bag.providerCostUsd ?? bag.provider_cost_usd ?? bag.costUsd ?? bag.cost_usd ?? bag.cost) ?? 0,
|
|
182
185
|
customerCostUsd: money(bag.customerCostUsd ?? bag.customer_cost_usd ?? bag.billableCostUsd ?? bag.billable_cost_usd),
|
|
183
186
|
unpriced: Boolean(bag.unpriced),
|
|
187
|
+
estimated: Boolean(bag.estimated),
|
|
184
188
|
};
|
|
185
189
|
}
|
|
186
190
|
|
|
@@ -214,8 +218,12 @@ export function buildSessionUsageReport({
|
|
|
214
218
|
sessionId = "",
|
|
215
219
|
events = [],
|
|
216
220
|
recentLimit = DEFAULT_RECENT_LIMIT,
|
|
221
|
+
estimateMessageUsage = true,
|
|
217
222
|
} = {}) {
|
|
218
|
-
const ledger = buildSessionUsageLedger(events, {
|
|
223
|
+
const ledger = buildSessionUsageLedger(events, {
|
|
224
|
+
sessionId: normalize(sessionId),
|
|
225
|
+
includeEstimatedMessages: estimateMessageUsage,
|
|
226
|
+
});
|
|
219
227
|
const limit = clampRecentLimit(recentLimit);
|
|
220
228
|
return {
|
|
221
229
|
schema: SESSION_USAGE_REPORT_SCHEMA,
|
|
@@ -230,6 +238,7 @@ export function buildSessionUsageReport({
|
|
|
230
238
|
providerCostUsd: ledger.totals.providerCostUsd,
|
|
231
239
|
customerCostUsd: ledger.totals.hasCustomerCost ? ledger.totals.customerCostUsd : null,
|
|
232
240
|
unpriced: ledger.totals.unpriced,
|
|
241
|
+
estimatedEntries: ledger.totals.estimatedEntries || 0,
|
|
233
242
|
priceBookVersions: ledger.priceBookVersions,
|
|
234
243
|
},
|
|
235
244
|
perAgent: sortRollups(ledger.perAgent).map(rollupToJson),
|
|
@@ -281,6 +290,7 @@ export function buildSessionUsageReportFromLedgerPayload({
|
|
|
281
290
|
providerCostUsd: totals.providerCostUsd,
|
|
282
291
|
customerCostUsd: totals.customerCostUsd,
|
|
283
292
|
unpriced: totals.unpriced,
|
|
293
|
+
estimatedEntries: nonNegativeInt(source.estimatedEntries ?? source.estimated_entries ?? sourceTotals.estimatedEntries ?? sourceTotals.estimated_entries),
|
|
284
294
|
priceBookVersions: resolvedPriceBookVersions,
|
|
285
295
|
},
|
|
286
296
|
perAgent: sourceArray(
|
|
@@ -343,7 +353,12 @@ export function renderSessionUsageMarkdown(report = {}) {
|
|
|
343
353
|
`Provider cost: ${usdCell(totals.providerCostUsd)} - Customer cost: ${optionalUsdCell(totals.customerCostUsd, totals.customerCostUsd != null)}`,
|
|
344
354
|
);
|
|
345
355
|
lines.push(`Price books: ${(totals.priceBookVersions || []).map(tableText).filter(Boolean).join(", ") || "-"}`);
|
|
346
|
-
lines.push(
|
|
356
|
+
lines.push(
|
|
357
|
+
`Duplicates skipped: ${intCell(totals.duplicatesSkipped)} - Unpriced entries: ${intCell(totals.unpriced)} - Estimated entries: ${intCell(totals.estimatedEntries)}`,
|
|
358
|
+
);
|
|
359
|
+
if (totals.estimatedEntries > 0) {
|
|
360
|
+
lines.push("_Estimated entries are output-text estimates from non-human session messages, not billing-grade provider usage._");
|
|
361
|
+
}
|
|
347
362
|
lines.push("");
|
|
348
363
|
|
|
349
364
|
appendRollupTable(lines, {
|
|
@@ -366,8 +381,9 @@ export function renderSessionUsageMarkdown(report = {}) {
|
|
|
366
381
|
lines.push(`| - | - | - | - | 0 | ${usdCell(0)} | - | - | - |`);
|
|
367
382
|
} else {
|
|
368
383
|
for (const entry of entries) {
|
|
384
|
+
const action = entry.estimated ? `${entry.action} (estimated)` : entry.action;
|
|
369
385
|
lines.push(
|
|
370
|
-
`| ${tableText(timestampOnly(entry.timestamp)) || "-"} | ${codeCell(entry.agentId)} | ${codeCell(
|
|
386
|
+
`| ${tableText(timestampOnly(entry.timestamp)) || "-"} | ${codeCell(entry.agentId)} | ${codeCell(action)} | ${codeCell(entry.model)} | ${intCell(entry.totalTokens)} | ${usdCell(entry.providerCostUsd)} | ${optionalUsdCell(entry.customerCostUsd, entry.customerCostUsd != null)} | ${codeCell(entry.ledgerEntryId)} | ${codeCell(entry.idempotencyKeyHash)} |`,
|
|
371
387
|
);
|
|
372
388
|
}
|
|
373
389
|
}
|
|
@@ -387,6 +403,9 @@ export function renderSessionUsageSummary(report = {}) {
|
|
|
387
403
|
lines.push(
|
|
388
404
|
`priceBooks=${(totals.priceBookVersions || []).map(tableText).filter(Boolean).join(",") || "-"} duplicatesSkipped=${intCell(totals.duplicatesSkipped)} unpriced=${intCell(totals.unpriced)}`,
|
|
389
405
|
);
|
|
406
|
+
if (totals.estimatedEntries > 0) {
|
|
407
|
+
lines.push(`estimatedEntries=${intCell(totals.estimatedEntries)} (output-text estimates; not billing-grade)`);
|
|
408
|
+
}
|
|
390
409
|
const agents = Array.isArray(report.perAgent) ? report.perAgent.slice(0, 8) : [];
|
|
391
410
|
if (agents.length) {
|
|
392
411
|
lines.push("");
|
|
@@ -403,7 +422,7 @@ export function renderSessionUsageSummary(report = {}) {
|
|
|
403
422
|
lines.push("Recent:");
|
|
404
423
|
for (const entry of entries) {
|
|
405
424
|
lines.push(
|
|
406
|
-
`- ${timestampOnly(entry.timestamp) || "-"} ${entry.agentId} ${entry.action} ${intCell(entry.totalTokens)} tokens ${usdCell(entry.providerCostUsd)} ledger=${entry.ledgerEntryId}`,
|
|
425
|
+
`- ${timestampOnly(entry.timestamp) || "-"} ${entry.agentId} ${entry.action}${entry.estimated ? " (estimated)" : ""} ${intCell(entry.totalTokens)} tokens ${usdCell(entry.providerCostUsd)} ledger=${entry.ledgerEntryId}`,
|
|
407
426
|
);
|
|
408
427
|
}
|
|
409
428
|
}
|
package/src/session/usage.js
CHANGED
|
@@ -241,8 +241,8 @@ export async function emitLLMInteraction(
|
|
|
241
241
|
* totals: { totalTokens, inputTokens, outputTokens, costUsd, interactions },
|
|
242
242
|
* }}
|
|
243
243
|
*/
|
|
244
|
-
export function aggregateSessionUsage(events = []) {
|
|
245
|
-
const ledger = buildSessionUsageLedger(events);
|
|
244
|
+
export function aggregateSessionUsage(events = [], options = {}) {
|
|
245
|
+
const ledger = buildSessionUsageLedger(events, options);
|
|
246
246
|
const perAgent = new Map();
|
|
247
247
|
const totals = {
|
|
248
248
|
totalTokens: 0,
|