sentinelayer-cli 0.30.10 → 0.30.12
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 +2 -2
- package/src/commands/session.js +209 -0
- package/src/session/daemon.js +1 -0
- package/src/session/preview.js +2 -0
- package/src/session/pricing-ledger.js +4 -1
- package/src/session/transcript.js +200 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sentinelayer-cli",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.12",
|
|
4
4
|
"description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"docs:build": "node scripts/docs-build.mjs",
|
|
10
10
|
"test": "npm run test:unit && npm run test:e2e",
|
|
11
11
|
"test:unit": "node --import ./tests/setup-env.mjs --test tests/unit*.test.mjs",
|
|
12
|
-
"test:e2e": "node --import ./tests/setup-env.mjs --test tests/e2e
|
|
12
|
+
"test:e2e": "node --import ./tests/setup-env.mjs --test tests/e2e*.test.mjs",
|
|
13
13
|
"test:coverage": "c8 node --import ./tests/setup-env.mjs --test tests/unit*.test.mjs",
|
|
14
14
|
"verify": "npm run check && npm run docs:build && npm run test:e2e && npm run test:coverage && npm pack --dry-run",
|
|
15
15
|
"release:publish": "node scripts/release-publish.mjs"
|
package/src/commands/session.js
CHANGED
|
@@ -1712,6 +1712,47 @@ function canPublishListenerPresence(agentId) {
|
|
|
1712
1712
|
);
|
|
1713
1713
|
}
|
|
1714
1714
|
|
|
1715
|
+
const SESSION_OBSERVATION_SEVERITIES = new Set(["info", "p3", "p2", "p1", "p0"]);
|
|
1716
|
+
const SESSION_OBSERVATION_KINDS = new Set([
|
|
1717
|
+
"ux",
|
|
1718
|
+
"process",
|
|
1719
|
+
"reliability",
|
|
1720
|
+
"security",
|
|
1721
|
+
"billing",
|
|
1722
|
+
"coordination",
|
|
1723
|
+
"architecture",
|
|
1724
|
+
"testing",
|
|
1725
|
+
"release",
|
|
1726
|
+
"other",
|
|
1727
|
+
]);
|
|
1728
|
+
const SESSION_OBSERVATION_SUMMARY_MAX_LENGTH = 4_000;
|
|
1729
|
+
const SESSION_OBSERVATION_OPTION_MAX_LENGTH = 512;
|
|
1730
|
+
const SESSION_OBSERVATION_PROPOSAL_MAX_LENGTH = 2_000;
|
|
1731
|
+
|
|
1732
|
+
function requireGrantedNonHumanAgentId(rawAgentId, commandName) {
|
|
1733
|
+
const agentId = normalizeAgentId(rawAgentId, "");
|
|
1734
|
+
if (!agentId || !canPublishListenerPresence(agentId)) {
|
|
1735
|
+
throw new Error(`${commandName} requires a granted non-human agent id.`);
|
|
1736
|
+
}
|
|
1737
|
+
return agentId;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function normalizeSessionObservationChoice(rawValue, allowedValues, fallbackValue, fieldName) {
|
|
1741
|
+
const value = normalizeString(rawValue || fallbackValue).toLowerCase();
|
|
1742
|
+
if (!allowedValues.has(value)) {
|
|
1743
|
+
throw new Error(`${fieldName} must be one of: ${[...allowedValues].join(", ")}.`);
|
|
1744
|
+
}
|
|
1745
|
+
return value;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function normalizeSessionObservationText(rawValue, fieldName, maxLength) {
|
|
1749
|
+
const value = normalizeString(rawValue);
|
|
1750
|
+
if (value.length > maxLength) {
|
|
1751
|
+
throw new Error(`${fieldName} must be ${maxLength} characters or fewer.`);
|
|
1752
|
+
}
|
|
1753
|
+
return value;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1715
1756
|
function listenerLifecycleEventName(type = "") {
|
|
1716
1757
|
const normalized = normalizeString(type).toLowerCase();
|
|
1717
1758
|
if (normalized === "started") return "session_listener_started";
|
|
@@ -3660,6 +3701,174 @@ export function registerSessionCommand(program) {
|
|
|
3660
3701
|
console.log(formatEventLine(persisted));
|
|
3661
3702
|
});
|
|
3662
3703
|
|
|
3704
|
+
session
|
|
3705
|
+
.command("observe <sessionId> [summary...]")
|
|
3706
|
+
.description("Record a durable product/process observation in the session")
|
|
3707
|
+
.requiredOption("--agent <id>", "Granted non-human agent id to record the observation as")
|
|
3708
|
+
.option("--model <model>", "Agent model/provider hint", "cli")
|
|
3709
|
+
.option("--display-name <name>", "Human-readable agent display name")
|
|
3710
|
+
.option("--role <role>", "Agent role metadata", "observer")
|
|
3711
|
+
.option(
|
|
3712
|
+
"--kind <kind>",
|
|
3713
|
+
`Observation kind: ${[...SESSION_OBSERVATION_KINDS].join(", ")}`,
|
|
3714
|
+
"process",
|
|
3715
|
+
)
|
|
3716
|
+
.option(
|
|
3717
|
+
"--severity <level>",
|
|
3718
|
+
`Observation severity: ${[...SESSION_OBSERVATION_SEVERITIES].join(", ")}`,
|
|
3719
|
+
"info",
|
|
3720
|
+
)
|
|
3721
|
+
.option("--owner <owner>", "Suggested owner/team for follow-up")
|
|
3722
|
+
.option("--batch <batch>", "Suggested PR batch or backlog lane")
|
|
3723
|
+
.option("--target-sequence <n>", "Anchor the observation to a session sequence id")
|
|
3724
|
+
.option("--target-cursor <cursor>", "Anchor the observation to a session event cursor")
|
|
3725
|
+
.option("--proposal <text>", "Suggested remediation or next step")
|
|
3726
|
+
.option("--message-file <path>", "Read the observation summary from a UTF-8 file")
|
|
3727
|
+
.option("--stdin", "Read the observation summary from stdin")
|
|
3728
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
3729
|
+
.option("--json", "Emit machine-readable output")
|
|
3730
|
+
.action(async (sessionId, summaryParts, options, command) => {
|
|
3731
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
3732
|
+
if (!normalizedSessionId) {
|
|
3733
|
+
throw new Error("session id is required.");
|
|
3734
|
+
}
|
|
3735
|
+
const summary = normalizeSessionObservationText(
|
|
3736
|
+
await resolveSessionSayMessageInput(summaryParts, options),
|
|
3737
|
+
"summary",
|
|
3738
|
+
SESSION_OBSERVATION_SUMMARY_MAX_LENGTH,
|
|
3739
|
+
);
|
|
3740
|
+
if (!summary) {
|
|
3741
|
+
throw new Error("summary is required.");
|
|
3742
|
+
}
|
|
3743
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
3744
|
+
const agentId = requireGrantedNonHumanAgentId(options.agent, "observe");
|
|
3745
|
+
const kind = normalizeSessionObservationChoice(
|
|
3746
|
+
options.kind,
|
|
3747
|
+
SESSION_OBSERVATION_KINDS,
|
|
3748
|
+
"process",
|
|
3749
|
+
"kind",
|
|
3750
|
+
);
|
|
3751
|
+
const severity = normalizeSessionObservationChoice(
|
|
3752
|
+
options.severity,
|
|
3753
|
+
SESSION_OBSERVATION_SEVERITIES,
|
|
3754
|
+
"info",
|
|
3755
|
+
"severity",
|
|
3756
|
+
);
|
|
3757
|
+
const owner = normalizeSessionObservationText(
|
|
3758
|
+
options.owner,
|
|
3759
|
+
"owner",
|
|
3760
|
+
SESSION_OBSERVATION_OPTION_MAX_LENGTH,
|
|
3761
|
+
);
|
|
3762
|
+
const proposedBatch = normalizeSessionObservationText(
|
|
3763
|
+
options.batch,
|
|
3764
|
+
"batch",
|
|
3765
|
+
SESSION_OBSERVATION_OPTION_MAX_LENGTH,
|
|
3766
|
+
);
|
|
3767
|
+
const proposal = normalizeSessionObservationText(
|
|
3768
|
+
options.proposal,
|
|
3769
|
+
"proposal",
|
|
3770
|
+
SESSION_OBSERVATION_PROPOSAL_MAX_LENGTH,
|
|
3771
|
+
);
|
|
3772
|
+
const targetSequenceId = parseOptionalPositiveInteger(options.targetSequence, "target-sequence");
|
|
3773
|
+
const targetCursor = normalizeSessionObservationText(
|
|
3774
|
+
options.targetCursor,
|
|
3775
|
+
"target-cursor",
|
|
3776
|
+
SESSION_OBSERVATION_OPTION_MAX_LENGTH,
|
|
3777
|
+
);
|
|
3778
|
+
const localSession = await ensureLocalSessionForRemoteCommand(normalizedSessionId, {
|
|
3779
|
+
targetPath,
|
|
3780
|
+
});
|
|
3781
|
+
const clientMessageId = `cli-observation-${randomUUID()}`;
|
|
3782
|
+
const eventPayload = {
|
|
3783
|
+
schema: "session_observation/v1",
|
|
3784
|
+
summary,
|
|
3785
|
+
message: summary,
|
|
3786
|
+
kind,
|
|
3787
|
+
severity,
|
|
3788
|
+
channel: "session",
|
|
3789
|
+
source: "session_observe",
|
|
3790
|
+
clientKind: "cli",
|
|
3791
|
+
clientMessageId,
|
|
3792
|
+
};
|
|
3793
|
+
if (owner) eventPayload.owner = owner;
|
|
3794
|
+
if (proposedBatch) eventPayload.proposedBatch = proposedBatch;
|
|
3795
|
+
if (proposal) eventPayload.proposal = proposal;
|
|
3796
|
+
if (targetSequenceId) eventPayload.targetSequenceId = targetSequenceId;
|
|
3797
|
+
if (targetCursor) eventPayload.targetCursor = targetCursor;
|
|
3798
|
+
|
|
3799
|
+
const agent = await resolveSessionAgentEnvelope(normalizedSessionId, agentId, {
|
|
3800
|
+
targetPath,
|
|
3801
|
+
model: options.model,
|
|
3802
|
+
role: normalizeString(options.role) || "observer",
|
|
3803
|
+
displayName: options.displayName,
|
|
3804
|
+
});
|
|
3805
|
+
agent.role = normalizeString(agent.role) || "observer";
|
|
3806
|
+
agent.model = normalizeString(agent.model) || "cli";
|
|
3807
|
+
agent.clientKind = normalizeString(agent.clientKind) || "cli";
|
|
3808
|
+
|
|
3809
|
+
const event = createAgentEvent({
|
|
3810
|
+
event: "session_observation",
|
|
3811
|
+
agent,
|
|
3812
|
+
sessionId: normalizedSessionId,
|
|
3813
|
+
payload: eventPayload,
|
|
3814
|
+
});
|
|
3815
|
+
event.eventId = clientMessageId;
|
|
3816
|
+
event.idempotencyToken = clientMessageId;
|
|
3817
|
+
|
|
3818
|
+
const remoteConfirmationAnchor = await readSessionConfirmationAnchor(normalizedSessionId, {
|
|
3819
|
+
targetPath,
|
|
3820
|
+
});
|
|
3821
|
+
if (!remoteConfirmationAnchor?.ok) {
|
|
3822
|
+
if (remoteConfirmationAnchor?.reason === "api_403") {
|
|
3823
|
+
throw new Error(
|
|
3824
|
+
`Observation failed (api_403). Ensure this user has an active grant for '${agentId}'.`,
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
throw new Error(
|
|
3828
|
+
`Observation confirmation anchor failed (${remoteConfirmationAnchor?.reason || "unknown"}); local cache was not updated.`,
|
|
3829
|
+
);
|
|
3830
|
+
}
|
|
3831
|
+
const remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
|
|
3832
|
+
targetPath,
|
|
3833
|
+
});
|
|
3834
|
+
if (!remoteSync?.synced) {
|
|
3835
|
+
throw new Error(
|
|
3836
|
+
`Observation failed (${remoteSync?.reason || "unknown"}). Ensure this user has an active grant for '${agentId}'.`,
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
const remoteConfirmation = await confirmSessionEventVisible(normalizedSessionId, clientMessageId, {
|
|
3840
|
+
targetPath,
|
|
3841
|
+
anchorCursor: remoteConfirmationAnchor?.cursor,
|
|
3842
|
+
});
|
|
3843
|
+
if (!remoteConfirmation?.confirmed) {
|
|
3844
|
+
throw new Error(
|
|
3845
|
+
`Observation was accepted but not visible in canonical session events (${remoteConfirmation?.reason || "not_visible"}); local cache was not updated.`,
|
|
3846
|
+
);
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
const persisted = await appendToStream(normalizedSessionId, event, {
|
|
3850
|
+
targetPath,
|
|
3851
|
+
syncRemote: false,
|
|
3852
|
+
});
|
|
3853
|
+
const payload = {
|
|
3854
|
+
command: "session observe",
|
|
3855
|
+
targetPath,
|
|
3856
|
+
sessionId: normalizedSessionId,
|
|
3857
|
+
agentId,
|
|
3858
|
+
event: persisted,
|
|
3859
|
+
materializedLocalSession: localSession.materialized,
|
|
3860
|
+
refreshedLocalSession: Boolean(localSession.refreshed),
|
|
3861
|
+
remoteSync,
|
|
3862
|
+
remoteConfirmationAnchor,
|
|
3863
|
+
remoteConfirmation,
|
|
3864
|
+
};
|
|
3865
|
+
if (shouldEmitJson(options, command)) {
|
|
3866
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
console.log(formatEventLine(persisted));
|
|
3870
|
+
});
|
|
3871
|
+
|
|
3663
3872
|
async function runMessageActionCommand({
|
|
3664
3873
|
sessionId,
|
|
3665
3874
|
actionType,
|
package/src/session/daemon.js
CHANGED
package/src/session/preview.js
CHANGED
|
@@ -12,6 +12,7 @@ const PREVIEW_EVENTS = new Set([
|
|
|
12
12
|
"session_message",
|
|
13
13
|
"session_say",
|
|
14
14
|
"agent_response",
|
|
15
|
+
"session_observation",
|
|
15
16
|
"human_relay",
|
|
16
17
|
"daemon_alert",
|
|
17
18
|
"session_admin_kill",
|
|
@@ -45,6 +46,7 @@ export function pickLatestPreview(events = []) {
|
|
|
45
46
|
if (!kind || !PREVIEW_EVENTS.has(kind)) continue;
|
|
46
47
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
47
48
|
const text =
|
|
49
|
+
payload.summary ||
|
|
48
50
|
payload.message ||
|
|
49
51
|
payload.response ||
|
|
50
52
|
payload.alert ||
|
|
@@ -138,7 +138,10 @@ export function buildUsageLedgerEntry(
|
|
|
138
138
|
{ sessionId = "", priceBookVersion = DEFAULT_PRICE_BOOK_VERSION, billingTier = "unknown" } = {},
|
|
139
139
|
) {
|
|
140
140
|
const kind = n(event?.event || event?.type);
|
|
141
|
-
const
|
|
141
|
+
const rawPayload = event?.payload;
|
|
142
|
+
const payload = object(rawPayload);
|
|
143
|
+
const payloadWasObject = rawPayload && typeof rawPayload === "object" && !Array.isArray(rawPayload);
|
|
144
|
+
if (kind === SESSION_USAGE_EVENT && !payloadWasObject) return null;
|
|
142
145
|
const schema = n(payload.schema);
|
|
143
146
|
const usage = object(payload.usage);
|
|
144
147
|
const legacyUsageEvent = kind !== SESSION_USAGE_EVENT && hasUsageHints(usage);
|
|
@@ -47,6 +47,7 @@ const TRANSCRIPT_EVENT_KINDS = new Set([
|
|
|
47
47
|
"session_reply",
|
|
48
48
|
"session_reaction",
|
|
49
49
|
"session_usage",
|
|
50
|
+
"session_observation",
|
|
50
51
|
"human_relay",
|
|
51
52
|
"agent_join",
|
|
52
53
|
"agent_left",
|
|
@@ -209,11 +210,56 @@ function actionBody(event) {
|
|
|
209
210
|
return "";
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
function boundedMarkdownText(value, { maxLength = 2_000 } = {}) {
|
|
214
|
+
const text = normalize(value).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
215
|
+
if (!maxLength || text.length <= maxLength) return text;
|
|
216
|
+
const head = Math.max(120, Math.floor((maxLength - 3) * 0.75));
|
|
217
|
+
const tail = Math.max(40, maxLength - 3 - head);
|
|
218
|
+
return `${text.slice(0, head)}...${text.slice(-tail)}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function observationTargetLabel(payload = {}) {
|
|
222
|
+
const targetSequence = Number(payload.targetSequenceId || payload.target_sequence_id || 0);
|
|
223
|
+
if (Number.isFinite(targetSequence) && targetSequence > 0) {
|
|
224
|
+
return `#${Math.floor(targetSequence)}`;
|
|
225
|
+
}
|
|
226
|
+
const targetCursor = tableText(payload.targetCursor || payload.target_cursor, { maxLength: 96 });
|
|
227
|
+
return targetCursor ? `cursor ${targetCursor}` : "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function observationBody(event) {
|
|
231
|
+
const payload = event && typeof event.payload === "object" ? event.payload : {};
|
|
232
|
+
const severity = tableText(payload.severity || "info", { maxLength: 16 }).toLowerCase();
|
|
233
|
+
const kind = tableText(payload.kind || "process", { maxLength: 32 }).toLowerCase();
|
|
234
|
+
const summary = boundedMarkdownText(payload.summary || payload.message || payload.text, {
|
|
235
|
+
maxLength: 4_000,
|
|
236
|
+
});
|
|
237
|
+
const proposal = boundedMarkdownText(payload.proposal || payload.recommendation, {
|
|
238
|
+
maxLength: 2_000,
|
|
239
|
+
});
|
|
240
|
+
const owner = tableText(payload.owner || payload.assignee, { maxLength: 96 });
|
|
241
|
+
const proposedBatch = tableText(payload.proposedBatch || payload.batch, { maxLength: 96 });
|
|
242
|
+
const target = observationTargetLabel(payload);
|
|
243
|
+
const metadata = [];
|
|
244
|
+
if (owner) metadata.push(`Owner: \`${owner}\``);
|
|
245
|
+
if (proposedBatch) metadata.push(`Batch: \`${proposedBatch}\``);
|
|
246
|
+
if (target) metadata.push(`Target: \`${target}\``);
|
|
247
|
+
|
|
248
|
+
const lines = [`**Observation:** \`${severity || "info"}\` · \`${kind || "process"}\``];
|
|
249
|
+
if (metadata.length) lines.push(metadata.join(" · "));
|
|
250
|
+
if (summary) lines.push("", summary);
|
|
251
|
+
if (proposal) lines.push("", "**Proposal:**", proposal);
|
|
252
|
+
return lines.join("\n");
|
|
253
|
+
}
|
|
254
|
+
|
|
212
255
|
function eventBody(event) {
|
|
213
256
|
const kind = normalize(event?.event || event?.type);
|
|
214
257
|
if (kind === "session_action" || kind === "session_reply" || kind === "session_reaction") {
|
|
215
258
|
return actionBody(event);
|
|
216
259
|
}
|
|
260
|
+
if (kind === "session_observation") {
|
|
261
|
+
return observationBody(event);
|
|
262
|
+
}
|
|
217
263
|
const payload = event && typeof event.payload === "object" ? event.payload : {};
|
|
218
264
|
// session_usage carries the response inside payload.response.text
|
|
219
265
|
const responseText =
|
|
@@ -237,7 +283,12 @@ function eventBody(event) {
|
|
|
237
283
|
* - totals: { tokenTotal, costTotalUsd } summed through the pricing ledger
|
|
238
284
|
* - sentiActions: count of orchestrator events
|
|
239
285
|
*/
|
|
240
|
-
export function computeTranscriptStats({
|
|
286
|
+
export function computeTranscriptStats({
|
|
287
|
+
sessionMeta = {},
|
|
288
|
+
events = [],
|
|
289
|
+
speakerProfiles = new Map(),
|
|
290
|
+
usageLedger = null,
|
|
291
|
+
} = {}) {
|
|
241
292
|
const perAgent = new Map();
|
|
242
293
|
let firstEventTs = null;
|
|
243
294
|
let lastEventTs = null;
|
|
@@ -305,14 +356,14 @@ export function computeTranscriptStats({ sessionMeta = {}, events = [], speakerP
|
|
|
305
356
|
if (epoch > record.lastSeenMs) record.lastSeenMs = epoch;
|
|
306
357
|
}
|
|
307
358
|
|
|
308
|
-
const
|
|
359
|
+
const resolvedUsageLedger = usageLedger || buildSessionUsageLedger(events, {
|
|
309
360
|
sessionId: normalize(sessionMeta.sessionId),
|
|
310
361
|
});
|
|
311
362
|
const fallbackUsageEpoch =
|
|
312
363
|
lastEventTs ??
|
|
313
364
|
firstEventTs ??
|
|
314
365
|
(Number.isFinite(Date.parse(sessionMeta?.createdAt)) ? Date.parse(sessionMeta.createdAt) : 0);
|
|
315
|
-
for (const entry of
|
|
366
|
+
for (const entry of resolvedUsageLedger.entries) {
|
|
316
367
|
const entryEpoch = Number.isFinite(Date.parse(entry.timestamp))
|
|
317
368
|
? Date.parse(entry.timestamp)
|
|
318
369
|
: fallbackUsageEpoch;
|
|
@@ -364,17 +415,17 @@ export function computeTranscriptStats({ sessionMeta = {}, events = [], speakerP
|
|
|
364
415
|
sessionLiveSeconds,
|
|
365
416
|
agents,
|
|
366
417
|
totals: {
|
|
367
|
-
tokenTotal:
|
|
368
|
-
inputTokens:
|
|
369
|
-
outputTokens:
|
|
370
|
-
costTotalUsd:
|
|
371
|
-
customerCostTotalUsd:
|
|
372
|
-
?
|
|
418
|
+
tokenTotal: resolvedUsageLedger.totals.totalTokens,
|
|
419
|
+
inputTokens: resolvedUsageLedger.totals.inputTokens,
|
|
420
|
+
outputTokens: resolvedUsageLedger.totals.outputTokens,
|
|
421
|
+
costTotalUsd: resolvedUsageLedger.totals.providerCostUsd,
|
|
422
|
+
customerCostTotalUsd: resolvedUsageLedger.totals.hasCustomerCost
|
|
423
|
+
? resolvedUsageLedger.totals.customerCostUsd
|
|
373
424
|
: null,
|
|
374
|
-
usageEntries:
|
|
375
|
-
duplicatesSkipped:
|
|
376
|
-
unpriced:
|
|
377
|
-
priceBookVersions:
|
|
425
|
+
usageEntries: resolvedUsageLedger.entries.length,
|
|
426
|
+
duplicatesSkipped: resolvedUsageLedger.duplicatesSkipped,
|
|
427
|
+
unpriced: resolvedUsageLedger.totals.unpriced,
|
|
428
|
+
priceBookVersions: resolvedUsageLedger.priceBookVersions,
|
|
378
429
|
},
|
|
379
430
|
sentiActions,
|
|
380
431
|
};
|
|
@@ -408,6 +459,134 @@ function avatarMd(identity) {
|
|
|
408
459
|
return identity.avatar || letterTile(identity.displayName || identity.agentId);
|
|
409
460
|
}
|
|
410
461
|
|
|
462
|
+
function intCell(value) {
|
|
463
|
+
return Math.max(0, Math.floor(Number(value || 0))).toLocaleString("en-US");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function usdCell(value) {
|
|
467
|
+
return `$${Number(value || 0).toFixed(6)}`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function tableText(value, { maxLength = 160 } = {}) {
|
|
471
|
+
const escaped = normalize(value)
|
|
472
|
+
.replace(/\r?\n/g, " ")
|
|
473
|
+
.replace(/\|/g, "\\|")
|
|
474
|
+
.replace(/`/g, "'");
|
|
475
|
+
if (!maxLength || escaped.length <= maxLength) return escaped;
|
|
476
|
+
const head = Math.max(24, Math.floor((maxLength - 3) * 0.7));
|
|
477
|
+
const tail = Math.max(12, maxLength - 3 - head);
|
|
478
|
+
return `${escaped.slice(0, head)}...${escaped.slice(-tail)}`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function codeCell(value) {
|
|
482
|
+
const text = tableText(value);
|
|
483
|
+
return text ? `\`${text}\`` : "—";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function optionalUsdCell(value, hasValue) {
|
|
487
|
+
return hasValue ? usdCell(value) : "—";
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function shortenIdempotencyKey(value, maxLength = 36) {
|
|
491
|
+
const text = tableText(value, { maxLength: 0 });
|
|
492
|
+
if (!text || text.length <= maxLength) return text;
|
|
493
|
+
const head = Math.max(8, Math.floor((maxLength - 3) * 0.58));
|
|
494
|
+
const tail = Math.max(6, maxLength - 3 - head);
|
|
495
|
+
return `${text.slice(0, head)}...${text.slice(-tail)}`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function rollupsByCostAndTokens(map) {
|
|
499
|
+
return [...map.values()].sort((a, b) => (
|
|
500
|
+
b.providerCostUsd - a.providerCostUsd ||
|
|
501
|
+
b.totalTokens - a.totalTokens ||
|
|
502
|
+
b.entries - a.entries ||
|
|
503
|
+
tableText(a.label).localeCompare(tableText(b.label))
|
|
504
|
+
));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function recentUsageEntries(entries, limit = 10) {
|
|
508
|
+
return entries
|
|
509
|
+
.map((entry, index) => ({ entry, index }))
|
|
510
|
+
.sort((a, b) => {
|
|
511
|
+
const aSequence = Number.isFinite(a.entry.sequenceId) ? a.entry.sequenceId : null;
|
|
512
|
+
const bSequence = Number.isFinite(b.entry.sequenceId) ? b.entry.sequenceId : null;
|
|
513
|
+
if (aSequence != null && bSequence != null && aSequence !== bSequence) {
|
|
514
|
+
return bSequence - aSequence;
|
|
515
|
+
}
|
|
516
|
+
const aTime = Date.parse(a.entry.timestamp);
|
|
517
|
+
const bTime = Date.parse(b.entry.timestamp);
|
|
518
|
+
if (Number.isFinite(aTime) && Number.isFinite(bTime) && aTime !== bTime) {
|
|
519
|
+
return bTime - aTime;
|
|
520
|
+
}
|
|
521
|
+
return b.index - a.index;
|
|
522
|
+
})
|
|
523
|
+
.slice(0, limit)
|
|
524
|
+
.map(({ entry }) => entry);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function appendUsageRollupTable(lines, { title, labelHeader, rollups }) {
|
|
528
|
+
lines.push(`### ${title}`);
|
|
529
|
+
lines.push("");
|
|
530
|
+
lines.push(`| ${labelHeader} | Entries | Input | Output | Total | Provider cost | Customer cost | Unpriced |`);
|
|
531
|
+
lines.push("|---|---:|---:|---:|---:|---:|---:|---:|");
|
|
532
|
+
if (rollups.length === 0) {
|
|
533
|
+
lines.push(`| — | 0 | 0 | 0 | 0 | ${usdCell(0)} | — | 0 |`);
|
|
534
|
+
} else {
|
|
535
|
+
for (const rollup of rollups) {
|
|
536
|
+
lines.push(
|
|
537
|
+
`| ${codeCell(rollup.label)} | ${intCell(rollup.entries)} | ${intCell(rollup.inputTokens)} | ${intCell(rollup.outputTokens)} | ${intCell(rollup.totalTokens)} | ${usdCell(rollup.providerCostUsd)} | ${optionalUsdCell(rollup.customerCostUsd, rollup.hasCustomerCost)} | ${intCell(rollup.unpriced)} |`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
lines.push("");
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function appendUsageLedgerSection(lines, usageLedger) {
|
|
545
|
+
const totals = usageLedger.totals;
|
|
546
|
+
lines.push("## Usage Ledger");
|
|
547
|
+
lines.push("");
|
|
548
|
+
|
|
549
|
+
if (usageLedger.entries.length === 0 && usageLedger.duplicatesSkipped === 0) {
|
|
550
|
+
lines.push("_No usage telemetry recorded._");
|
|
551
|
+
lines.push("");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const customerCostText = totals.hasCustomerCost
|
|
556
|
+
? `Customer cost: ${usdCell(totals.customerCostUsd)}`
|
|
557
|
+
: "Customer cost: —";
|
|
558
|
+
lines.push(`Accepted entries: ${intCell(usageLedger.entries.length)}`);
|
|
559
|
+
lines.push(
|
|
560
|
+
`Tokens: ${intCell(totals.totalTokens)} (input ${intCell(totals.inputTokens)} / output ${intCell(totals.outputTokens)})`,
|
|
561
|
+
);
|
|
562
|
+
lines.push(`Provider cost: ${usdCell(totals.providerCostUsd)} · ${customerCostText}`);
|
|
563
|
+
lines.push(`Price books: ${usageLedger.priceBookVersions.map(tableText).filter(Boolean).join(", ") || "—"}`);
|
|
564
|
+
lines.push(`Duplicates skipped: ${intCell(usageLedger.duplicatesSkipped)} · Unpriced entries: ${intCell(totals.unpriced)}`);
|
|
565
|
+
lines.push("");
|
|
566
|
+
|
|
567
|
+
appendUsageRollupTable(lines, {
|
|
568
|
+
title: "Per Agent",
|
|
569
|
+
labelHeader: "Agent",
|
|
570
|
+
rollups: rollupsByCostAndTokens(usageLedger.perAgent),
|
|
571
|
+
});
|
|
572
|
+
appendUsageRollupTable(lines, {
|
|
573
|
+
title: "Per Action",
|
|
574
|
+
labelHeader: "Action",
|
|
575
|
+
rollups: rollupsByCostAndTokens(usageLedger.perAction),
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
lines.push("### Recent Entries");
|
|
579
|
+
lines.push("");
|
|
580
|
+
lines.push("| Time | Agent | Action | Model | Tokens | Provider cost | Customer cost | Idempotency key |");
|
|
581
|
+
lines.push("|---|---|---|---|---:|---:|---:|---|");
|
|
582
|
+
for (const entry of recentUsageEntries(usageLedger.entries)) {
|
|
583
|
+
lines.push(
|
|
584
|
+
`| ${tableText(timestampOnly(entry.timestamp)) || "—"} | ${codeCell(entry.agentId)} | ${codeCell(entry.action)} | ${codeCell(entry.model)} | ${intCell(entry.totalTokens)} | ${usdCell(entry.providerCostUsd)} | ${optionalUsdCell(entry.customerCostUsd, entry.customerCostUsd != null)} | ${codeCell(shortenIdempotencyKey(entry.idempotencyKey))} |`,
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
lines.push("");
|
|
588
|
+
}
|
|
589
|
+
|
|
411
590
|
/**
|
|
412
591
|
* Build the iMessage-style markdown transcript.
|
|
413
592
|
*
|
|
@@ -429,8 +608,11 @@ export function buildTranscriptMarkdown({
|
|
|
429
608
|
speakerProfiles = new Map(),
|
|
430
609
|
options = {},
|
|
431
610
|
} = {}) {
|
|
611
|
+
const usageLedger = buildSessionUsageLedger(events, {
|
|
612
|
+
sessionId: normalize(sessionMeta.sessionId),
|
|
613
|
+
});
|
|
432
614
|
const includeSystemEvents = options.includeSystemEvents !== false;
|
|
433
|
-
const stats = computeTranscriptStats({ sessionMeta, events, speakerProfiles });
|
|
615
|
+
const stats = computeTranscriptStats({ sessionMeta, events, speakerProfiles, usageLedger });
|
|
434
616
|
|
|
435
617
|
const lines = [];
|
|
436
618
|
const sessionId = normalize(sessionMeta.sessionId) || "unknown";
|
|
@@ -496,15 +678,17 @@ export function buildTranscriptMarkdown({
|
|
|
496
678
|
stats.participantCount = stats.agents.length + silentRegisteredAgents.length;
|
|
497
679
|
lines.push("");
|
|
498
680
|
|
|
681
|
+
appendUsageLedgerSection(lines, usageLedger);
|
|
682
|
+
|
|
499
683
|
// Conversation
|
|
500
684
|
lines.push("## Conversation");
|
|
501
685
|
lines.push("");
|
|
502
686
|
for (const event of events) {
|
|
503
|
-
const kind = normalize(event
|
|
687
|
+
const kind = normalize(event?.event || event?.type);
|
|
504
688
|
if (!kind || !TRANSCRIPT_EVENT_KINDS.has(kind)) continue;
|
|
505
689
|
if (!includeSystemEvents && SYSTEM_EVENT_KINDS.has(kind)) continue;
|
|
506
690
|
|
|
507
|
-
const agentId = normalize(event
|
|
691
|
+
const agentId = normalize(event?.agent?.id || event?.agentId);
|
|
508
692
|
const profile = speakerProfiles.get(agentId) || null;
|
|
509
693
|
const identity = resolveSpeakerIdentity({
|
|
510
694
|
agentId,
|