sentinelayer-cli 0.17.0 → 0.17.1
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/README.md +16 -6
- package/package.json +3 -2
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/omargate.js +1 -0
- package/src/commands/session.js +302 -25
- package/src/events/schema.js +21 -0
- package/src/legacy-cli.js +16 -0
- package/src/review/investor-dd-devtestbot.js +83 -8
- package/src/review/investor-dd-file-loop.js +83 -6
- package/src/review/investor-dd-orchestrator.js +42 -1
- package/src/review/investor-dd-progress.js +351 -0
- package/src/review/investor-dd-usage.js +227 -0
- package/src/session/daemon.js +341 -2
- package/src/session/recap.js +288 -69
- package/src/session/sync.js +1 -4
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
export const INVESTOR_DD_PROGRESS_VERSION = "investor_dd_progress_v1";
|
|
2
|
+
|
|
3
|
+
export const INVESTOR_DD_EXPECTED_PERSONAS = Object.freeze([
|
|
4
|
+
"security",
|
|
5
|
+
"backend",
|
|
6
|
+
"code-quality",
|
|
7
|
+
"testing",
|
|
8
|
+
"data-layer",
|
|
9
|
+
"reliability",
|
|
10
|
+
"release",
|
|
11
|
+
"observability",
|
|
12
|
+
"infrastructure",
|
|
13
|
+
"supply-chain",
|
|
14
|
+
"documentation",
|
|
15
|
+
"ai-governance",
|
|
16
|
+
"frontend",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const REQUIRED_FOR_SELLABLE = Object.freeze([
|
|
20
|
+
"persona_roster",
|
|
21
|
+
"persona_agentic_loops",
|
|
22
|
+
"senti_streaming",
|
|
23
|
+
"usage_margin_telemetry",
|
|
24
|
+
"live_reconciliation",
|
|
25
|
+
"devtestbot_runtime",
|
|
26
|
+
"report_email",
|
|
27
|
+
"artifact_bundle",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function uniqueStrings(values) {
|
|
31
|
+
return Array.from(new Set((values || []).map((value) => String(value || "").trim()).filter(Boolean)));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function compactBudgetState(budgetState) {
|
|
35
|
+
if (!budgetState || typeof budgetState !== "object") return null;
|
|
36
|
+
return {
|
|
37
|
+
spentUsd: Number.isFinite(budgetState.spentUsd) ? budgetState.spentUsd : 0,
|
|
38
|
+
maxUsd: Number.isFinite(budgetState.maxUsd) ? budgetState.maxUsd : null,
|
|
39
|
+
toolCalls: Number.isFinite(budgetState.toolCalls) ? budgetState.toolCalls : 0,
|
|
40
|
+
llmCalls: Number.isFinite(budgetState.llmCalls) ? budgetState.llmCalls : 0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function usageLedgerKey(entry) {
|
|
45
|
+
return [
|
|
46
|
+
entry?.ledgerEntry?.ledgerEntryId,
|
|
47
|
+
entry?.action,
|
|
48
|
+
entry?.ledgerEntry?.idempotencyKey,
|
|
49
|
+
entry?.inputTokens,
|
|
50
|
+
entry?.outputTokens,
|
|
51
|
+
].map((value) => String(value || "")).join(":");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectSessionUsageLedgerEntries({ budgetState = null, devTestBotPhase = null, usageLedgerEntries = [] }) {
|
|
55
|
+
const candidates = [
|
|
56
|
+
...(Array.isArray(usageLedgerEntries) ? usageLedgerEntries : []),
|
|
57
|
+
...(Array.isArray(budgetState?.sessionUsageLedgerEntries) ? budgetState.sessionUsageLedgerEntries : []),
|
|
58
|
+
devTestBotPhase?.plan?.usageLedger,
|
|
59
|
+
].filter((entry) => entry?.ok);
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const entries = [];
|
|
62
|
+
for (const entry of candidates) {
|
|
63
|
+
const key = usageLedgerKey(entry);
|
|
64
|
+
if (seen.has(key)) continue;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
entries.push(entry);
|
|
67
|
+
}
|
|
68
|
+
return entries;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function byId(capabilities, id) {
|
|
72
|
+
return capabilities.find((capability) => capability.id === id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function addCapability(capabilities, capability) {
|
|
76
|
+
capabilities.push({
|
|
77
|
+
requiredForSellable: REQUIRED_FOR_SELLABLE.includes(capability.id),
|
|
78
|
+
evidence: [],
|
|
79
|
+
gaps: [],
|
|
80
|
+
...capability,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function countByStatus(capabilities, status) {
|
|
85
|
+
return capabilities.filter((capability) => capability.status === status).length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function summarizeInvestorDdProgress(capabilities) {
|
|
89
|
+
const required = capabilities.filter((capability) => capability.requiredForSellable);
|
|
90
|
+
const blockingGaps = required
|
|
91
|
+
.filter((capability) => capability.status !== "complete")
|
|
92
|
+
.flatMap((capability) => capability.gaps.map((gap) => ({ capabilityId: capability.id, gap })));
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
total: capabilities.length,
|
|
96
|
+
complete: countByStatus(capabilities, "complete"),
|
|
97
|
+
partial: countByStatus(capabilities, "partial"),
|
|
98
|
+
deferred: countByStatus(capabilities, "deferred"),
|
|
99
|
+
notConfigured: countByStatus(capabilities, "not_configured"),
|
|
100
|
+
requiredTotal: required.length,
|
|
101
|
+
requiredComplete: required.filter((capability) => capability.status === "complete").length,
|
|
102
|
+
blockingGapCount: blockingGaps.length,
|
|
103
|
+
blockingGaps,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function buildInvestorDdProgress({
|
|
108
|
+
runId,
|
|
109
|
+
generatedAt = new Date().toISOString(),
|
|
110
|
+
personas = [],
|
|
111
|
+
dryRun = false,
|
|
112
|
+
routing = {},
|
|
113
|
+
byPersona = {},
|
|
114
|
+
findings = [],
|
|
115
|
+
compliance = null,
|
|
116
|
+
reconciliationAvailable = false,
|
|
117
|
+
liveValidator = null,
|
|
118
|
+
devTestBotPhase = null,
|
|
119
|
+
reportEmailConfigured = false,
|
|
120
|
+
reportEmailResult = null,
|
|
121
|
+
notification = null,
|
|
122
|
+
artifactFiles = [],
|
|
123
|
+
budgetState = null,
|
|
124
|
+
usageLedgerEntries = [],
|
|
125
|
+
} = {}) {
|
|
126
|
+
const activePersonas = uniqueStrings(personas);
|
|
127
|
+
const missingPersonas = INVESTOR_DD_EXPECTED_PERSONAS.filter(
|
|
128
|
+
(personaId) => !activePersonas.includes(personaId),
|
|
129
|
+
);
|
|
130
|
+
const extraPersonas = activePersonas.filter(
|
|
131
|
+
(personaId) => !INVESTOR_DD_EXPECTED_PERSONAS.includes(personaId),
|
|
132
|
+
);
|
|
133
|
+
const capabilities = [];
|
|
134
|
+
|
|
135
|
+
addCapability(capabilities, {
|
|
136
|
+
id: "persona_roster",
|
|
137
|
+
label: "13-persona DD roster",
|
|
138
|
+
status: missingPersonas.length === 0 ? "complete" : "partial",
|
|
139
|
+
evidence: [
|
|
140
|
+
`activePersonas=${activePersonas.length}`,
|
|
141
|
+
`expectedPersonas=${INVESTOR_DD_EXPECTED_PERSONAS.length}`,
|
|
142
|
+
],
|
|
143
|
+
gaps: missingPersonas.length
|
|
144
|
+
? [`missing expected personas: ${missingPersonas.join(", ")}`]
|
|
145
|
+
: [],
|
|
146
|
+
metadata: { activePersonas, expectedPersonas: INVESTOR_DD_EXPECTED_PERSONAS, missingPersonas, extraPersonas },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const personaRecords = activePersonas.filter((personaId) => byPersona && byPersona[personaId]);
|
|
150
|
+
const allActivePersonasRecorded =
|
|
151
|
+
activePersonas.length > 0 && personaRecords.length === activePersonas.length;
|
|
152
|
+
addCapability(capabilities, {
|
|
153
|
+
id: "persona_agentic_loops",
|
|
154
|
+
label: "Per-persona file loop execution",
|
|
155
|
+
status: dryRun ? "deferred" : allActivePersonasRecorded ? "complete" : "partial",
|
|
156
|
+
evidence: dryRun
|
|
157
|
+
? ["dryRun=true"]
|
|
158
|
+
: [`personaRecords=${personaRecords.length}/${activePersonas.length}`],
|
|
159
|
+
gaps: dryRun
|
|
160
|
+
? ["dry-run skips persona execution"]
|
|
161
|
+
: allActivePersonasRecorded
|
|
162
|
+
? []
|
|
163
|
+
: ["not every active persona produced a persona artifact"],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const streamPresent = artifactFiles.includes("stream.ndjson");
|
|
167
|
+
addCapability(capabilities, {
|
|
168
|
+
id: "senti_streaming",
|
|
169
|
+
label: "Senti session streaming and progress spine",
|
|
170
|
+
status: streamPresent ? "partial" : "not_configured",
|
|
171
|
+
evidence: streamPresent ? ["stream.ndjson artifact present"] : [],
|
|
172
|
+
gaps: [
|
|
173
|
+
"orchestrator emits local NDJSON but does not record an attached Senti session id",
|
|
174
|
+
"live web/session token counters are not part of the DD summary contract",
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const compactBudget = compactBudgetState(budgetState);
|
|
179
|
+
const sessionUsageLedgerEntries = collectSessionUsageLedgerEntries({
|
|
180
|
+
budgetState,
|
|
181
|
+
devTestBotPhase,
|
|
182
|
+
usageLedgerEntries,
|
|
183
|
+
});
|
|
184
|
+
const hasSessionUsageLedger = sessionUsageLedgerEntries.length > 0;
|
|
185
|
+
addCapability(capabilities, {
|
|
186
|
+
id: "usage_margin_telemetry",
|
|
187
|
+
label: "Billing-grade per-agent usage, token, time, LOC, and margin telemetry",
|
|
188
|
+
status: compactBudget || hasSessionUsageLedger ? "partial" : dryRun ? "deferred" : "not_configured",
|
|
189
|
+
evidence: compactBudget || hasSessionUsageLedger
|
|
190
|
+
? [
|
|
191
|
+
...(compactBudget
|
|
192
|
+
? [
|
|
193
|
+
`localBudgetSpentUsd=${compactBudget.spentUsd}`,
|
|
194
|
+
`localBudgetToolCalls=${compactBudget.toolCalls}`,
|
|
195
|
+
`localBudgetLlmCalls=${compactBudget.llmCalls}`,
|
|
196
|
+
]
|
|
197
|
+
: []),
|
|
198
|
+
`sessionUsageLedger=${hasSessionUsageLedger}`,
|
|
199
|
+
...sessionUsageLedgerEntries.map((entry) =>
|
|
200
|
+
`usageLedgerEntry=${entry.ledgerEntry?.ledgerEntryId || "recorded"}`
|
|
201
|
+
),
|
|
202
|
+
]
|
|
203
|
+
: dryRun
|
|
204
|
+
? ["dryRun=true"]
|
|
205
|
+
: [],
|
|
206
|
+
gaps: [
|
|
207
|
+
hasSessionUsageLedger
|
|
208
|
+
? "only optional DD planner calls are wired to billing-grade session_usage"
|
|
209
|
+
: "budgetState is a local run governor, not the billing-grade session_usage ledger",
|
|
210
|
+
"summary does not include per-agent token totals",
|
|
211
|
+
"summary does not include per-agent runtime, LOC scanned, customer price, or margin",
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
addCapability(capabilities, {
|
|
216
|
+
id: "compliance_pack",
|
|
217
|
+
label: "Compliance pack dispatch",
|
|
218
|
+
requiredForSellable: false,
|
|
219
|
+
status: compliance ? "complete" : dryRun ? "deferred" : "not_configured",
|
|
220
|
+
evidence: compliance
|
|
221
|
+
? [`totalCovered=${compliance.totalCovered || 0}`, `totalGaps=${compliance.totalGaps || 0}`]
|
|
222
|
+
: dryRun
|
|
223
|
+
? ["dryRun=true"]
|
|
224
|
+
: [],
|
|
225
|
+
gaps: compliance ? [] : ["compliance pack did not run for this invocation"],
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
addCapability(capabilities, {
|
|
229
|
+
id: "live_reconciliation",
|
|
230
|
+
label: "Live product validation and reconciliation",
|
|
231
|
+
status: reconciliationAvailable ? "complete" : liveValidator ? "partial" : "not_configured",
|
|
232
|
+
evidence: reconciliationAvailable
|
|
233
|
+
? ["live-observations.json and reconciliation verdicts available"]
|
|
234
|
+
: liveValidator
|
|
235
|
+
? ["liveValidator config supplied"]
|
|
236
|
+
: [],
|
|
237
|
+
gaps: reconciliationAvailable
|
|
238
|
+
? []
|
|
239
|
+
: ["findings were not reconciled against live product observations"],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const devSkipped = Boolean(devTestBotPhase?.skipped);
|
|
243
|
+
addCapability(capabilities, {
|
|
244
|
+
id: "devtestbot_runtime",
|
|
245
|
+
label: "devTestBot/AIdenID runtime phase",
|
|
246
|
+
status: devTestBotPhase
|
|
247
|
+
? devSkipped
|
|
248
|
+
? "partial"
|
|
249
|
+
: "complete"
|
|
250
|
+
: dryRun
|
|
251
|
+
? "deferred"
|
|
252
|
+
: "not_configured",
|
|
253
|
+
evidence: devTestBotPhase
|
|
254
|
+
? [
|
|
255
|
+
`skipped=${devSkipped}`,
|
|
256
|
+
`findingCount=${devTestBotPhase.findingCount || 0}`,
|
|
257
|
+
`artifactRoot=${devTestBotPhase.artifactRoot || ""}`,
|
|
258
|
+
]
|
|
259
|
+
: dryRun
|
|
260
|
+
? ["dryRun=true"]
|
|
261
|
+
: [],
|
|
262
|
+
gaps: devTestBotPhase && !devSkipped
|
|
263
|
+
? []
|
|
264
|
+
: ["devTestBot runtime evidence is missing or skipped for this run"],
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
addCapability(capabilities, {
|
|
268
|
+
id: "report_email",
|
|
269
|
+
label: "Investor report email delivery",
|
|
270
|
+
status: reportEmailResult?.queued
|
|
271
|
+
? "complete"
|
|
272
|
+
: reportEmailConfigured
|
|
273
|
+
? "partial"
|
|
274
|
+
: dryRun
|
|
275
|
+
? "deferred"
|
|
276
|
+
: "not_configured",
|
|
277
|
+
evidence: reportEmailResult
|
|
278
|
+
? [
|
|
279
|
+
`queued=${Boolean(reportEmailResult.queued)}`,
|
|
280
|
+
`skipped=${Boolean(reportEmailResult.skipped)}`,
|
|
281
|
+
`code=${reportEmailResult.code || ""}`,
|
|
282
|
+
]
|
|
283
|
+
: dryRun
|
|
284
|
+
? ["dryRun=true"]
|
|
285
|
+
: [],
|
|
286
|
+
gaps: reportEmailResult?.queued
|
|
287
|
+
? []
|
|
288
|
+
: ["no queued DD report email is recorded for this run"],
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const artifactRequired = dryRun
|
|
292
|
+
? ["plan.json", "stream.ndjson", "summary.json", "report.md", "report.html"]
|
|
293
|
+
: ["plan.json", "stream.ndjson", "summary.json", "report.md", "report.html", "findings.json"];
|
|
294
|
+
const missingArtifacts = artifactRequired.filter((file) => !artifactFiles.includes(file));
|
|
295
|
+
addCapability(capabilities, {
|
|
296
|
+
id: "artifact_bundle",
|
|
297
|
+
label: "Portable artifact bundle",
|
|
298
|
+
status: missingArtifacts.length === 0 ? "complete" : "partial",
|
|
299
|
+
evidence: [`artifactFiles=${artifactFiles.length}`, "manifest.json is written after progress.json"],
|
|
300
|
+
gaps: missingArtifacts.map((file) => `missing artifact: ${file}`),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
addCapability(capabilities, {
|
|
304
|
+
id: "notification_delivery",
|
|
305
|
+
label: "Dashboard/email notification delivery",
|
|
306
|
+
requiredForSellable: false,
|
|
307
|
+
status: notification ? "partial" : "not_configured",
|
|
308
|
+
evidence: notification ? ["notification config supplied"] : [],
|
|
309
|
+
gaps: notification
|
|
310
|
+
? ["notification result is not captured in summary.json/progress.json"]
|
|
311
|
+
: ["notification clients were not configured for this invocation"],
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const progressSummary = summarizeInvestorDdProgress(capabilities);
|
|
315
|
+
const sellableReady = progressSummary.requiredComplete === progressSummary.requiredTotal;
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
version: INVESTOR_DD_PROGRESS_VERSION,
|
|
319
|
+
generatedAt,
|
|
320
|
+
runId: runId || "",
|
|
321
|
+
overallStatus: sellableReady ? "complete" : "partial",
|
|
322
|
+
sellableReady,
|
|
323
|
+
truthfulClaim: sellableReady ? "sellable_ready" : "not_sellable_ready",
|
|
324
|
+
summary: progressSummary,
|
|
325
|
+
requiredCapabilityIds: REQUIRED_FOR_SELLABLE,
|
|
326
|
+
activePersonaCount: activePersonas.length,
|
|
327
|
+
plannedPersonaCount: INVESTOR_DD_EXPECTED_PERSONAS.length,
|
|
328
|
+
missingPersonas,
|
|
329
|
+
capabilities,
|
|
330
|
+
routingSummary: {
|
|
331
|
+
routedPersonas: Object.keys(routing || {}).length,
|
|
332
|
+
routedFiles: Object.values(routing || {}).reduce(
|
|
333
|
+
(count, files) => count + (Array.isArray(files) ? files.length : 0),
|
|
334
|
+
0,
|
|
335
|
+
),
|
|
336
|
+
},
|
|
337
|
+
findingCount: Array.isArray(findings) ? findings.length : 0,
|
|
338
|
+
nextRecommendedSlices: byId(capabilities, "persona_roster")?.status === "complete"
|
|
339
|
+
? [
|
|
340
|
+
"wire Senti session id and live usage counters into Investor-DD runs",
|
|
341
|
+
"add per-agent token/time/LOC/customer-price/margin telemetry",
|
|
342
|
+
"require live reconciliation and report-email proof for sellable DD closeout",
|
|
343
|
+
]
|
|
344
|
+
: [
|
|
345
|
+
"add the missing frontend/Jules persona to the default Investor-DD roster",
|
|
346
|
+
"wire Senti session id and live usage counters into Investor-DD runs",
|
|
347
|
+
"add per-agent token/time/LOC/customer-price/margin telemetry",
|
|
348
|
+
"require live reconciliation and report-email proof for sellable DD closeout",
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
2
|
+
import { sanitizeBillingMetadata } from "../billing/ledger-entry.js";
|
|
3
|
+
|
|
4
|
+
export const INVESTOR_DD_USAGE_ACTIONS = Object.freeze({
|
|
5
|
+
devTestBotPlanner: "investor_dd_devtestbot_planner",
|
|
6
|
+
filePlanner: "investor_dd_file_planner",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export class InvestorDdUsageLedgerError extends Error {
|
|
10
|
+
constructor(message, result = null) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "InvestorDdUsageLedgerError";
|
|
13
|
+
this.code = "INVESTOR_DD_USAGE_LEDGER_FAILED";
|
|
14
|
+
this.result = result;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isInvestorDdUsageLedgerError(error) {
|
|
19
|
+
return error instanceof InvestorDdUsageLedgerError
|
|
20
|
+
|| String(error?.code || "") === "INVESTOR_DD_USAGE_LEDGER_FAILED";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeString(value) {
|
|
24
|
+
return String(value || "").trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function plainObject(value) {
|
|
28
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeBool(value) {
|
|
32
|
+
return value === true || value === "true" || value === "1";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function modelFrom(response, fallback) {
|
|
36
|
+
return normalizeString(response?.usage?.model)
|
|
37
|
+
|| normalizeString(response?.model)
|
|
38
|
+
|| normalizeString(fallback)
|
|
39
|
+
|| "gpt-5.3-codex";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function providerFrom(response, fallback) {
|
|
43
|
+
return normalizeString(response?.usage?.provider)
|
|
44
|
+
|| normalizeString(response?.provider)
|
|
45
|
+
|| normalizeString(fallback);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeUsageContext(usageContext = {}, defaults = {}) {
|
|
49
|
+
const context = plainObject(usageContext);
|
|
50
|
+
const fallback = plainObject(defaults);
|
|
51
|
+
return {
|
|
52
|
+
sessionId: normalizeString(context.sessionId || fallback.sessionId),
|
|
53
|
+
agentId: normalizeString(context.agentId || fallback.agentId),
|
|
54
|
+
model: normalizeString(context.model || fallback.model),
|
|
55
|
+
provider: normalizeString(context.provider || fallback.provider),
|
|
56
|
+
targetPath: normalizeString(context.targetPath || fallback.targetPath),
|
|
57
|
+
billingTier: normalizeString(context.billingTier || fallback.billingTier) || "internal",
|
|
58
|
+
sourceCommand: normalizeString(context.sourceCommand || fallback.sourceCommand) || "omargate investor-dd",
|
|
59
|
+
syncRemote: context.syncRemote !== undefined ? context.syncRemote !== false : fallback.syncRemote !== false,
|
|
60
|
+
required: normalizeBool(context.required ?? fallback.required),
|
|
61
|
+
recorder: typeof context.recorder === "function"
|
|
62
|
+
? context.recorder
|
|
63
|
+
: typeof fallback.recorder === "function"
|
|
64
|
+
? fallback.recorder
|
|
65
|
+
: recordCliLlmSessionUsage,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function firstProviderUsageNumber(usage, keys) {
|
|
70
|
+
const source = plainObject(usage);
|
|
71
|
+
for (const key of keys) {
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
|
|
73
|
+
const parsed = Number(source[key]);
|
|
74
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
75
|
+
return { found: true, value: Math.floor(parsed) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { found: false, value: 0 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function usageFailure(context, reason, message) {
|
|
82
|
+
const result = {
|
|
83
|
+
ok: false,
|
|
84
|
+
reason,
|
|
85
|
+
required: context.required,
|
|
86
|
+
};
|
|
87
|
+
if (context.required) {
|
|
88
|
+
throw new InvestorDdUsageLedgerError(message, result);
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function assertInvestorDdUsageContextReady({
|
|
94
|
+
usageContext = {},
|
|
95
|
+
defaults = {},
|
|
96
|
+
action,
|
|
97
|
+
agentId = "",
|
|
98
|
+
model = "",
|
|
99
|
+
targetPath = "",
|
|
100
|
+
} = {}) {
|
|
101
|
+
const context = normalizeUsageContext(usageContext, {
|
|
102
|
+
...defaults,
|
|
103
|
+
agentId,
|
|
104
|
+
model,
|
|
105
|
+
targetPath,
|
|
106
|
+
});
|
|
107
|
+
if (!context.required) {
|
|
108
|
+
return { ok: true, required: false };
|
|
109
|
+
}
|
|
110
|
+
const normalizedAction = normalizeString(action);
|
|
111
|
+
const normalizedAgentId = normalizeString(agentId || context.agentId);
|
|
112
|
+
const normalizedModel = normalizeString(model || context.model);
|
|
113
|
+
if (!context.sessionId || !normalizedAgentId || !normalizedAction || !normalizedModel) {
|
|
114
|
+
throw new InvestorDdUsageLedgerError(
|
|
115
|
+
"Investor-DD required usage ledger context is incomplete before planner spend.",
|
|
116
|
+
{
|
|
117
|
+
ok: false,
|
|
118
|
+
reason: "missing_session_usage_context",
|
|
119
|
+
required: true,
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, required: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function recordInvestorDdLlmUsage({
|
|
127
|
+
usageContext = {},
|
|
128
|
+
defaults = {},
|
|
129
|
+
action,
|
|
130
|
+
agentId = "",
|
|
131
|
+
phase = "",
|
|
132
|
+
response = "",
|
|
133
|
+
model = "",
|
|
134
|
+
provider = "",
|
|
135
|
+
startedAtIso = "",
|
|
136
|
+
targetPath = "",
|
|
137
|
+
metadata = {},
|
|
138
|
+
} = {}) {
|
|
139
|
+
const context = normalizeUsageContext(usageContext, {
|
|
140
|
+
...defaults,
|
|
141
|
+
agentId,
|
|
142
|
+
model,
|
|
143
|
+
provider,
|
|
144
|
+
targetPath,
|
|
145
|
+
});
|
|
146
|
+
const normalizedAction = normalizeString(action);
|
|
147
|
+
const normalizedAgentId = normalizeString(agentId || context.agentId);
|
|
148
|
+
const normalizedModel = modelFrom(response, model || context.model);
|
|
149
|
+
const normalizedProvider = providerFrom(response, provider || context.provider);
|
|
150
|
+
const createdAt = normalizeString(startedAtIso) || new Date().toISOString();
|
|
151
|
+
|
|
152
|
+
if (!context.sessionId || !normalizedAgentId || !normalizedAction || !normalizedModel) {
|
|
153
|
+
const result = {
|
|
154
|
+
ok: false,
|
|
155
|
+
reason: "missing_session_usage_context",
|
|
156
|
+
required: context.required,
|
|
157
|
+
};
|
|
158
|
+
if (context.required) {
|
|
159
|
+
throw new InvestorDdUsageLedgerError("Investor-DD session usage context is required.", result);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const usage = plainObject(response?.usage);
|
|
165
|
+
const input = firstProviderUsageNumber(usage, ["inputTokens", "input_tokens", "tokens_in"]);
|
|
166
|
+
const output = firstProviderUsageNumber(usage, ["outputTokens", "output_tokens", "tokens_out"]);
|
|
167
|
+
if (!input.found && !output.found) {
|
|
168
|
+
return usageFailure(
|
|
169
|
+
context,
|
|
170
|
+
"missing_provider_usage",
|
|
171
|
+
"Investor-DD planner response did not include provider token usage.",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const inputTokens = usageNumber(input.value, 0);
|
|
175
|
+
const outputTokens = usageNumber(output.value, 0);
|
|
176
|
+
if (inputTokens + outputTokens <= 0) {
|
|
177
|
+
return usageFailure(
|
|
178
|
+
context,
|
|
179
|
+
"zero_provider_tokens",
|
|
180
|
+
"Investor-DD planner response reported zero provider tokens.",
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const result = await context.recorder({
|
|
186
|
+
sessionId: context.sessionId,
|
|
187
|
+
agentId: normalizedAgentId,
|
|
188
|
+
action: normalizedAction,
|
|
189
|
+
model: normalizedModel,
|
|
190
|
+
inputTokens,
|
|
191
|
+
outputTokens,
|
|
192
|
+
startedAtIso: createdAt,
|
|
193
|
+
targetPath: context.targetPath,
|
|
194
|
+
billingTier: context.billingTier,
|
|
195
|
+
sourceCommand: context.sourceCommand,
|
|
196
|
+
provider: normalizedProvider,
|
|
197
|
+
syncRemote: context.syncRemote,
|
|
198
|
+
metadata: sanitizeBillingMetadata({
|
|
199
|
+
phase,
|
|
200
|
+
...plainObject(metadata),
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
if ((!result || result.ok === false) && context.required) {
|
|
204
|
+
throw new InvestorDdUsageLedgerError(
|
|
205
|
+
`Investor-DD session usage ledger failed: ${result?.reason || "unknown"}`,
|
|
206
|
+
result || null,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
...(result || { ok: false, reason: "missing_result" }),
|
|
211
|
+
inputTokens,
|
|
212
|
+
outputTokens,
|
|
213
|
+
action: normalizedAction,
|
|
214
|
+
};
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error instanceof InvestorDdUsageLedgerError) throw error;
|
|
217
|
+
const result = {
|
|
218
|
+
ok: false,
|
|
219
|
+
reason: error instanceof Error ? error.message : String(error || "session_usage_failed"),
|
|
220
|
+
required: context.required,
|
|
221
|
+
};
|
|
222
|
+
if (context.required) {
|
|
223
|
+
throw new InvestorDdUsageLedgerError(result.reason, result);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
}
|