@agent-finops/core 0.8.0 → 0.9.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/README.md +5 -3
- package/dist/actionPlanner.d.ts +140 -0
- package/dist/actionPlanner.js +938 -0
- package/dist/actionVerification.d.ts +1240 -0
- package/dist/actionVerification.js +1028 -0
- package/dist/activitySnapshot.d.ts +101 -9
- package/dist/activitySnapshot.js +145 -6
- package/dist/activitySnapshotCache.d.ts +8 -1
- package/dist/activitySnapshotCache.js +103 -7
- package/dist/agentEconomicsReceipt.d.ts +58 -58
- package/dist/analyze.js +3 -1
- package/dist/cutList.js +1 -1
- package/dist/glance.d.ts +30 -2
- package/dist/glance.js +265 -84
- package/dist/index.d.ts +11 -2
- package/dist/index.js +10 -1
- package/dist/insights.js +3 -1
- package/dist/localAgentFormats/gemini.js +2 -2
- package/dist/localAgentFormats/registry.js +6 -2
- package/dist/localAgentFormats/runtimeRegistry.js +5 -2
- package/dist/localAgentFormats/types.d.ts +2 -1
- package/dist/localAgentLogs.d.ts +362 -3
- package/dist/localAgentLogs.js +1964 -165
- package/dist/modelPricing.d.ts +1 -1
- package/dist/modelPricing.js +4 -1
- package/dist/planMath.js +12 -7
- package/dist/projectEconomics.d.ts +617 -0
- package/dist/projectEconomics.js +620 -0
- package/dist/projectEconomicsBuilder.d.ts +89 -0
- package/dist/projectEconomicsBuilder.js +473 -0
- package/dist/projectIndexStore.d.ts +545 -0
- package/dist/projectIndexStore.js +606 -0
- package/dist/providerConnectors.d.ts +59 -1
- package/dist/providerConnectors.js +192 -12
- package/dist/qualitativeIndexCache.d.ts +494 -0
- package/dist/qualitativeIndexCache.js +930 -0
- package/dist/resultCard.d.ts +350 -0
- package/dist/resultCard.js +604 -0
- package/dist/runtimeCommands.d.ts +21 -0
- package/dist/runtimeCommands.js +27 -0
- package/dist/scanGuard.d.ts +3 -1
- package/dist/scanGuard.js +164 -4
- package/dist/schema.d.ts +31 -31
- package/dist/sessionVitals.d.ts +145 -0
- package/dist/sessionVitals.js +521 -0
- package/dist/sourceRegistry.js +90 -52
- package/dist/toolInvocations.d.ts +40 -1
- package/dist/toolInvocations.js +101 -20
- package/package.json +1 -1
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { localAgentFormatDescriptor } from "./localAgentFormats/registry.js";
|
|
3
|
+
import { MAX_TOKEN_EXPERIMENT_SESSIONS_PER_PHASE_V0, MAX_WASTE_FINDING_EVIDENCE_REFS_V0, TOKEN_REDUCTION_EXPERIMENT_V0_KIND, TOKEN_REDUCTION_EXPERIMENT_V0_VERSION, WASTE_FINDING_V0_KIND, WASTE_FINDING_V0_VERSION, createActionVerificationReference, createTokenReductionExperimentV0, createWasteFindingV0 } from "./actionVerification.js";
|
|
4
|
+
const MINIMUM_SESSIONS = 3;
|
|
5
|
+
const CONTEXT_RATIO_THRESHOLD = 1.5;
|
|
6
|
+
const FRESH_MS = 72 * 60 * 60 * 1_000;
|
|
7
|
+
const AV_REF = /^avref_[a-f0-9]{64}$/;
|
|
8
|
+
/**
|
|
9
|
+
* Select the experiment every read-only surface should foreground.
|
|
10
|
+
*
|
|
11
|
+
* Active work outranks newer terminal history so CLI, MCP, and Glance cannot
|
|
12
|
+
* silently hand the user different tests. Creation time and stable lineage ID
|
|
13
|
+
* are deterministic tie-breakers only within the same lifecycle priority.
|
|
14
|
+
*/
|
|
15
|
+
export function selectPreferredTokenReductionExperimentV0(experiments) {
|
|
16
|
+
return experiments.reduce((preferred, candidate) => {
|
|
17
|
+
if (!preferred)
|
|
18
|
+
return candidate;
|
|
19
|
+
const candidatePriority = experimentSelectionPriority(candidate);
|
|
20
|
+
const preferredPriority = experimentSelectionPriority(preferred);
|
|
21
|
+
if (candidatePriority !== preferredPriority) {
|
|
22
|
+
return candidatePriority > preferredPriority ? candidate : preferred;
|
|
23
|
+
}
|
|
24
|
+
const candidateTime = Date.parse(candidate.createdAt);
|
|
25
|
+
const preferredTime = Date.parse(preferred.createdAt);
|
|
26
|
+
if (candidateTime !== preferredTime) {
|
|
27
|
+
return candidateTime > preferredTime ? candidate : preferred;
|
|
28
|
+
}
|
|
29
|
+
return candidate.id.localeCompare(preferred.id) > 0 ? candidate : preferred;
|
|
30
|
+
}, undefined);
|
|
31
|
+
}
|
|
32
|
+
function experimentSelectionPriority(experiment) {
|
|
33
|
+
switch (experiment.lifecycle) {
|
|
34
|
+
case "applied":
|
|
35
|
+
case "collecting": return 4;
|
|
36
|
+
case "baseline_ready":
|
|
37
|
+
case "draft": return 3;
|
|
38
|
+
case "complete": return 2;
|
|
39
|
+
case "rolled_back":
|
|
40
|
+
case "invalidated": return 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Return at most one launch-safe candidate. Signal priority is explicit
|
|
45
|
+
* compaction, explicit repeated reads, a calculated context ratio, then
|
|
46
|
+
* measured configured-not-observed inventory. No prompt, path, or item name is
|
|
47
|
+
* carried into the finding.
|
|
48
|
+
*/
|
|
49
|
+
export function selectBestWasteFindingV0(input) {
|
|
50
|
+
const generatedAt = timestamp(input.generatedAt, "generatedAt");
|
|
51
|
+
const groups = comparableGroups(input.sessionVitals, generatedAt, input.contextHealth)
|
|
52
|
+
.filter((group) => group.sessions.length >= MINIMUM_SESSIONS);
|
|
53
|
+
if (groups.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
const currentSession = activePlannerSession(input.sessionVitals.sessions, input.contextHealth, generatedAt);
|
|
56
|
+
const currentGroup = currentSession
|
|
57
|
+
? groupForActiveSession(groups, currentSession)
|
|
58
|
+
: undefined;
|
|
59
|
+
const currentSessionRef = currentSession?.vital.sessionRef;
|
|
60
|
+
const churn = input.contextHealth?.contextChurn;
|
|
61
|
+
if (currentGroup && currentSessionRef && churn?.currentSessionEvidence === "matched") {
|
|
62
|
+
if ((churn.compactionEvents ?? 0) > 0) {
|
|
63
|
+
return findingForGroup(currentGroup, generatedAt, {
|
|
64
|
+
findingType: "compaction_pressure",
|
|
65
|
+
action: "start_fresh",
|
|
66
|
+
sourceId: "context-health-v1",
|
|
67
|
+
metric: {
|
|
68
|
+
name: "compaction_events",
|
|
69
|
+
unit: "events",
|
|
70
|
+
value: churn.compactionEvents,
|
|
71
|
+
sampleCount: 1,
|
|
72
|
+
evidence: "observed"
|
|
73
|
+
},
|
|
74
|
+
signalRef: createActionVerificationReference("context-signal", `compaction:${churn.compactionEvents}`),
|
|
75
|
+
target: { kind: "session", ref: currentSessionRef }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (churn.readCoverage === "explicit_read_tools_only" &&
|
|
79
|
+
(churn.repeatedReadEvents ?? 0) > 0) {
|
|
80
|
+
const repeated = [...churn.repeatedFiles].sort((left, right) => right.readCount - left.readCount || left.file.localeCompare(right.file))[0];
|
|
81
|
+
if (!repeated)
|
|
82
|
+
return null;
|
|
83
|
+
return findingForGroup(currentGroup, generatedAt, {
|
|
84
|
+
findingType: "repeated_context_read",
|
|
85
|
+
action: "reduce_repeated_reads",
|
|
86
|
+
sourceId: "context-health-v1",
|
|
87
|
+
metric: {
|
|
88
|
+
name: "repeated_read_events",
|
|
89
|
+
unit: "events",
|
|
90
|
+
value: churn.repeatedReadEvents,
|
|
91
|
+
sampleCount: 1,
|
|
92
|
+
evidence: "observed"
|
|
93
|
+
},
|
|
94
|
+
signalRef: createActionVerificationReference("context-signal", `repeated-read:${churn.repeatedReadEvents}`),
|
|
95
|
+
target: {
|
|
96
|
+
kind: "repeated_read_file",
|
|
97
|
+
ref: repeatedReadTargetRef(repeated.file)
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const current = input.contextHealth?.currentSession;
|
|
102
|
+
if (current && current.ratioToMedian !== null &&
|
|
103
|
+
current.ratioToMedian >= CONTEXT_RATIO_THRESHOLD &&
|
|
104
|
+
current.comparisonSessions >= 2 &&
|
|
105
|
+
current.comparisonBasis !== "not_available" &&
|
|
106
|
+
current.usageSource !== "not_available") {
|
|
107
|
+
return findingForGroup(currentGroup, generatedAt, {
|
|
108
|
+
findingType: "high_context_relative_to_baseline",
|
|
109
|
+
action: "trim_context",
|
|
110
|
+
sourceId: "context-health-v1",
|
|
111
|
+
metric: {
|
|
112
|
+
name: "input_context_tokens",
|
|
113
|
+
unit: "ratio",
|
|
114
|
+
value: round(current.ratioToMedian),
|
|
115
|
+
sampleCount: current.comparisonSessions,
|
|
116
|
+
evidence: "calculated"
|
|
117
|
+
},
|
|
118
|
+
signalRef: createActionVerificationReference("context-signal", `context-ratio:${round(current.ratioToMedian)}:${current.comparisonSessions}`),
|
|
119
|
+
target: { kind: "session", ref: currentSessionRef }
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const dead = measuredDeadInventory(input.deadContext);
|
|
124
|
+
if (dead) {
|
|
125
|
+
const group = groups.find((candidate) => candidate.agent === dead.host) ??
|
|
126
|
+
(!dead.host ? groups[0] : undefined);
|
|
127
|
+
if (group) {
|
|
128
|
+
const scopedItems = dead.items.filter((item) => !item.host || item.host === group.agent)
|
|
129
|
+
.sort((left, right) => right.alwaysLoadedTokens - left.alwaysLoadedTokens ||
|
|
130
|
+
left.kind.localeCompare(right.kind) || left.name.localeCompare(right.name) ||
|
|
131
|
+
(left.path ?? "").localeCompare(right.path ?? ""));
|
|
132
|
+
const count = scopedItems.length;
|
|
133
|
+
if (count > 0) {
|
|
134
|
+
const item = scopedItems[0];
|
|
135
|
+
return findingForGroup(group, generatedAt, {
|
|
136
|
+
findingType: "configured_not_observed",
|
|
137
|
+
action: "inspect_scope",
|
|
138
|
+
sourceId: "dead-context-v1",
|
|
139
|
+
surface: "local_agent_configuration",
|
|
140
|
+
metric: {
|
|
141
|
+
name: "configured_items",
|
|
142
|
+
unit: "items",
|
|
143
|
+
value: count,
|
|
144
|
+
sampleCount: input.deadContext.sessions,
|
|
145
|
+
evidence: "observed"
|
|
146
|
+
},
|
|
147
|
+
signalRef: createActionVerificationReference("inventory-signal", `${group.agent}:${count}:${input.deadContext.windowDays}`),
|
|
148
|
+
target: {
|
|
149
|
+
kind: "configured_item",
|
|
150
|
+
ref: configuredItemTargetRef(item)
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const group of groups) {
|
|
157
|
+
const ordered = [...group.sessions].sort(comparePlannerSessions);
|
|
158
|
+
const latest = ordered.at(-1);
|
|
159
|
+
if (Date.parse(generatedAt) - Date.parse(latest.endedAt) > FRESH_MS)
|
|
160
|
+
continue;
|
|
161
|
+
const previous = ordered.slice(0, -1)
|
|
162
|
+
.map((session) => totalTokens(session.vital))
|
|
163
|
+
.filter((value) => value !== null);
|
|
164
|
+
const latestTotal = totalTokens(latest.vital);
|
|
165
|
+
const comparisonMedian = median(previous);
|
|
166
|
+
if (latestTotal === null || comparisonMedian === null || comparisonMedian <= 0 ||
|
|
167
|
+
previous.length < 2)
|
|
168
|
+
continue;
|
|
169
|
+
const ratio = latestTotal / comparisonMedian;
|
|
170
|
+
if (ratio < CONTEXT_RATIO_THRESHOLD)
|
|
171
|
+
continue;
|
|
172
|
+
return findingForGroup(group, generatedAt, {
|
|
173
|
+
findingType: "high_context_relative_to_baseline",
|
|
174
|
+
action: "trim_context",
|
|
175
|
+
sourceId: "session-vitals-v0",
|
|
176
|
+
metric: {
|
|
177
|
+
name: "total_tokens",
|
|
178
|
+
unit: "ratio",
|
|
179
|
+
value: round(ratio),
|
|
180
|
+
sampleCount: previous.length,
|
|
181
|
+
evidence: "calculated"
|
|
182
|
+
},
|
|
183
|
+
signalRef: latest.vital.sessionRef,
|
|
184
|
+
target: { kind: "session", ref: latest.vital.sessionRef }
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
/** Resolve an opaque candidate only from fresh local evidence supplied by the caller. */
|
|
190
|
+
export function resolveWasteFindingTargetV0(input) {
|
|
191
|
+
const { finding } = input;
|
|
192
|
+
if (finding.target.kind === "repeated_read_file") {
|
|
193
|
+
const match = input.contextHealth?.contextChurn.repeatedFiles.find((item) => repeatedReadTargetRef(item.file) === finding.target.ref);
|
|
194
|
+
return match
|
|
195
|
+
? {
|
|
196
|
+
status: "resolved",
|
|
197
|
+
kind: "repeated_read_file",
|
|
198
|
+
ref: finding.target.ref,
|
|
199
|
+
file: match.file,
|
|
200
|
+
readCount: match.readCount,
|
|
201
|
+
localOnly: true
|
|
202
|
+
}
|
|
203
|
+
: { status: "not_found", kind: finding.target.kind, ref: finding.target.ref, localOnly: true };
|
|
204
|
+
}
|
|
205
|
+
if (finding.target.kind === "configured_item") {
|
|
206
|
+
const match = input.deadContext?.deadItems.find((item) => configuredItemTargetRef(item) === finding.target.ref);
|
|
207
|
+
return match
|
|
208
|
+
? {
|
|
209
|
+
status: "resolved",
|
|
210
|
+
kind: "configured_item",
|
|
211
|
+
ref: finding.target.ref,
|
|
212
|
+
name: match.name,
|
|
213
|
+
itemKind: match.kind,
|
|
214
|
+
...(match.path ? { path: match.path } : {}),
|
|
215
|
+
localOnly: true
|
|
216
|
+
}
|
|
217
|
+
: { status: "not_found", kind: finding.target.kind, ref: finding.target.ref, localOnly: true };
|
|
218
|
+
}
|
|
219
|
+
const matches = input.sessionVitals?.sessions.filter((session) => session.sessionRef === finding.target.ref &&
|
|
220
|
+
session.agent === finding.scope.agent &&
|
|
221
|
+
(!finding.scope.projectRef || session.projectRef === finding.scope.projectRef) &&
|
|
222
|
+
(!finding.scope.model ||
|
|
223
|
+
session.models.length === 1 &&
|
|
224
|
+
safeOutputIdentifier("model", session.models[0]) === finding.scope.model)) ?? [];
|
|
225
|
+
const session = matches.length === 1 ? matches[0] : undefined;
|
|
226
|
+
const observedFrom = session ? normalizedTimestamp(session.observedFrom) : null;
|
|
227
|
+
const observedTo = session ? normalizedTimestamp(session.observedTo) : null;
|
|
228
|
+
if (!session || !observedFrom || !observedTo ||
|
|
229
|
+
(session.agent !== "claude-code" && session.agent !== "codex") ||
|
|
230
|
+
!["parent", "subagent", "unknown"].includes(session.sessionType)) {
|
|
231
|
+
return { status: "not_found", kind: "session", ref: finding.target.ref, localOnly: true };
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
status: "resolved",
|
|
235
|
+
kind: "session",
|
|
236
|
+
ref: finding.target.ref,
|
|
237
|
+
agent: session.agent,
|
|
238
|
+
sessionType: session.sessionType,
|
|
239
|
+
observedFrom,
|
|
240
|
+
observedTo,
|
|
241
|
+
localOnly: true
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/** Build one immutable pre-change cohort. Fewer than three sessions fails closed. */
|
|
245
|
+
export function buildTokenReductionBaselineV0(input) {
|
|
246
|
+
const createdAt = timestamp(input.createdAt, "createdAt");
|
|
247
|
+
if (input.finding.source.freshness !== "fresh" ||
|
|
248
|
+
input.finding.source.validationCoverage !== "live_verified")
|
|
249
|
+
return null;
|
|
250
|
+
const groups = comparableGroups(input.sessionVitals, createdAt, input.contextHealth)
|
|
251
|
+
.filter((group) => group.sessions.length >= MINIMUM_SESSIONS &&
|
|
252
|
+
group.agent === input.finding.scope.agent &&
|
|
253
|
+
group.provider === input.finding.scope.provider &&
|
|
254
|
+
group.model === input.finding.scope.model &&
|
|
255
|
+
group.projectRef === input.finding.scope.projectRef)
|
|
256
|
+
.sort((left, right) => {
|
|
257
|
+
const overlap = findingOverlap(input.finding, right) - findingOverlap(input.finding, left);
|
|
258
|
+
return overlap || compareGroups(left, right);
|
|
259
|
+
});
|
|
260
|
+
const group = groups[0];
|
|
261
|
+
if (!group)
|
|
262
|
+
return null;
|
|
263
|
+
const boundedBaseline = group.sessions.slice(-MAX_TOKEN_EXPERIMENT_SESSIONS_PER_PHASE_V0);
|
|
264
|
+
return createTokenReductionExperimentV0({
|
|
265
|
+
kind: TOKEN_REDUCTION_EXPERIMENT_V0_KIND,
|
|
266
|
+
schemaVersion: TOKEN_REDUCTION_EXPERIMENT_V0_VERSION,
|
|
267
|
+
createdAt,
|
|
268
|
+
finding: input.finding,
|
|
269
|
+
cohort: {
|
|
270
|
+
agent: group.agent,
|
|
271
|
+
provider: group.provider,
|
|
272
|
+
model: group.model,
|
|
273
|
+
projectRef: group.projectRef,
|
|
274
|
+
sessionType: group.sessionType,
|
|
275
|
+
workTypeRef: group.workTypeRef,
|
|
276
|
+
workTypeEvidence: "observed",
|
|
277
|
+
...(group.sourceVersionRef ? { sourceVersionRef: group.sourceVersionRef } : {})
|
|
278
|
+
},
|
|
279
|
+
matchingPolicy: {
|
|
280
|
+
basis: "session_cohort",
|
|
281
|
+
minimumBaselineSessions: MINIMUM_SESSIONS,
|
|
282
|
+
minimumPostSessions: MINIMUM_SESSIONS,
|
|
283
|
+
requireExactSourceVersion: group.sourceVersionRef !== undefined
|
|
284
|
+
},
|
|
285
|
+
qualityGuard: {
|
|
286
|
+
required: true,
|
|
287
|
+
minimumEvidence: "user_declared",
|
|
288
|
+
rollbackOnRegression: true
|
|
289
|
+
},
|
|
290
|
+
baselineSessions: boundedBaseline.map((session) => experimentSession(session, input.qualityBySessionRef)),
|
|
291
|
+
intervention: {
|
|
292
|
+
approval: { status: "pending", evidence: "missing" }
|
|
293
|
+
},
|
|
294
|
+
postSessions: []
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/** Select one finding and prepare its baseline, or return null without guessing. */
|
|
298
|
+
export function planTokenReductionActionV0(input) {
|
|
299
|
+
const finding = selectBestWasteFindingV0(input);
|
|
300
|
+
if (!finding)
|
|
301
|
+
return null;
|
|
302
|
+
const experiment = buildTokenReductionBaselineV0({
|
|
303
|
+
finding,
|
|
304
|
+
sessionVitals: input.sessionVitals,
|
|
305
|
+
createdAt: input.generatedAt,
|
|
306
|
+
contextHealth: input.contextHealth,
|
|
307
|
+
qualityBySessionRef: input.qualityBySessionRef
|
|
308
|
+
});
|
|
309
|
+
return experiment ? { finding, experiment } : null;
|
|
310
|
+
}
|
|
311
|
+
/** Record explicit approval, an applied reversible change, and the actual canary outcome. */
|
|
312
|
+
export function markTokenReductionAppliedV0(experiment, input) {
|
|
313
|
+
if (!AV_REF.test(input.changeRef)) {
|
|
314
|
+
throw new TypeError("A change must be represented by an opaque action-verification reference.");
|
|
315
|
+
}
|
|
316
|
+
if (!AV_REF.test(input.rollbackRef)) {
|
|
317
|
+
throw new TypeError("A rollback must be represented by an opaque action-verification reference.");
|
|
318
|
+
}
|
|
319
|
+
if (!AV_REF.test(input.canaryRef)) {
|
|
320
|
+
throw new TypeError("A canary must be represented by an opaque action-verification reference.");
|
|
321
|
+
}
|
|
322
|
+
const approvedAt = timestamp(input.approvedAt, "approvedAt");
|
|
323
|
+
const appliedAt = timestamp(input.appliedAt, "appliedAt");
|
|
324
|
+
if (experiment.lifecycle !== "baseline_ready") {
|
|
325
|
+
throw new TypeError("A complete comparable baseline is required before application.");
|
|
326
|
+
}
|
|
327
|
+
if (experiment.baselineSessions.some((session) => session.quality.status !== "passed" ||
|
|
328
|
+
qualityEvidenceRank(session.quality.evidence) <
|
|
329
|
+
qualityEvidenceRank(experiment.qualityGuard.minimumEvidence))) {
|
|
330
|
+
throw new TypeError("Baseline quality must be recorded before the intervention boundary.");
|
|
331
|
+
}
|
|
332
|
+
return createTokenReductionExperimentV0({
|
|
333
|
+
...experimentBody(experiment),
|
|
334
|
+
intervention: {
|
|
335
|
+
approval: {
|
|
336
|
+
status: "explicit",
|
|
337
|
+
evidence: "user_declared",
|
|
338
|
+
approvedAt
|
|
339
|
+
},
|
|
340
|
+
appliedAt,
|
|
341
|
+
changeRef: input.changeRef,
|
|
342
|
+
rollbackRef: input.rollbackRef,
|
|
343
|
+
canary: {
|
|
344
|
+
status: input.canaryStatus,
|
|
345
|
+
evidence: "user_declared",
|
|
346
|
+
evidenceRef: input.canaryRef
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/** Record execution of the rollback already frozen at the application boundary. */
|
|
352
|
+
export function markTokenReductionRolledBackV0(experiment, input) {
|
|
353
|
+
if (experiment.lifecycle === "rolled_back" || experiment.lifecycle === "invalidated" ||
|
|
354
|
+
experiment.intervention.rolledBackAt) {
|
|
355
|
+
throw new TypeError("A terminal token test cannot record another rollback boundary.");
|
|
356
|
+
}
|
|
357
|
+
if (!experiment.intervention.appliedAt || !experiment.intervention.rollbackRef) {
|
|
358
|
+
throw new TypeError("Only an applied token test can be rolled back.");
|
|
359
|
+
}
|
|
360
|
+
if (input.rollbackRef !== experiment.intervention.rollbackRef) {
|
|
361
|
+
throw new TypeError("The rollback evidence does not match the frozen rollback reference.");
|
|
362
|
+
}
|
|
363
|
+
const rolledBackAt = timestamp(input.rolledBackAt, "rolledBackAt");
|
|
364
|
+
return createTokenReductionExperimentV0({
|
|
365
|
+
...experimentBody(experiment),
|
|
366
|
+
intervention: {
|
|
367
|
+
...experiment.intervention,
|
|
368
|
+
rolledBackAt
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/** Cancel an un-applied baseline so its scope can be used by a future test. */
|
|
373
|
+
export function invalidateTokenReductionExperimentV0(experiment, input) {
|
|
374
|
+
if (experiment.intervention.appliedAt ||
|
|
375
|
+
(experiment.lifecycle !== "draft" && experiment.lifecycle !== "baseline_ready")) {
|
|
376
|
+
throw new TypeError("Only an un-applied draft or baseline can be cancelled; applied changes require rollback.");
|
|
377
|
+
}
|
|
378
|
+
return createTokenReductionExperimentV0({
|
|
379
|
+
...experimentBody(experiment),
|
|
380
|
+
invalidation: {
|
|
381
|
+
reason: input.reason,
|
|
382
|
+
invalidatedAt: timestamp(input.invalidatedAt, "invalidatedAt")
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Add matched completed session snapshots after the application boundary, then delegate every
|
|
388
|
+
* median, exclusion, quality guard, and result label to the canonical evaluator.
|
|
389
|
+
*/
|
|
390
|
+
export function refreshTokenReductionExperimentV0(experiment, input) {
|
|
391
|
+
if (experiment.lifecycle === "complete" || experiment.lifecycle === "rolled_back" ||
|
|
392
|
+
experiment.lifecycle === "invalidated" ||
|
|
393
|
+
experiment.intervention.canary?.status === "failed") {
|
|
394
|
+
throw new TypeError("A complete, terminal, or failed-canary token test cannot collect new evidence.");
|
|
395
|
+
}
|
|
396
|
+
const observedAt = timestamp(input.observedAt, "observedAt");
|
|
397
|
+
const appliedAt = experiment.intervention.appliedAt;
|
|
398
|
+
if (!appliedAt)
|
|
399
|
+
throw new TypeError("An experiment must be applied before post-change refresh.");
|
|
400
|
+
// The baseline and its quality evidence are immutable after the intervention
|
|
401
|
+
// boundary. One native session reference contributes at most one frozen
|
|
402
|
+
// snapshot in the entire experiment. A resumed cumulative session is never
|
|
403
|
+
// converted into a synthetic delta or a second sample.
|
|
404
|
+
const baselineSessions = experiment.baselineSessions;
|
|
405
|
+
const baselineRefs = new Set(baselineSessions.map((session) => session.sessionRef));
|
|
406
|
+
const observedPostSessions = structurallyEligibleSessions(input.sessionVitals, observedAt, input.contextHealth)
|
|
407
|
+
// Keep the persisted experiment project-scoped. Unrelated sessions are
|
|
408
|
+
// neither evidence nor useful exclusions, and a busy machine must not be
|
|
409
|
+
// able to overflow another project's bounded experiment envelope.
|
|
410
|
+
.filter((session) => sessionMatchesCohort(experiment, session))
|
|
411
|
+
.filter((session) => Date.parse(session.startedAt) >= Date.parse(appliedAt))
|
|
412
|
+
.filter((session) => !baselineRefs.has(session.vital.sessionRef))
|
|
413
|
+
.sort(comparePlannerSessions);
|
|
414
|
+
const postByRef = new Map(experiment.postSessions.map((session) => [session.sessionRef, session]));
|
|
415
|
+
for (const observed of observedPostSessions) {
|
|
416
|
+
const prior = postByRef.get(observed.vital.sessionRef);
|
|
417
|
+
if (prior) {
|
|
418
|
+
const requested = input.qualityBySessionRef?.[prior.sessionRef];
|
|
419
|
+
if (prior.quality.status === "missing" && requested && requested !== "missing") {
|
|
420
|
+
postByRef.set(prior.sessionRef, {
|
|
421
|
+
...prior,
|
|
422
|
+
quality: qualityFor(prior.sessionRef, input.qualityBySessionRef)
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (postByRef.size >= MAX_TOKEN_EXPERIMENT_SESSIONS_PER_PHASE_V0)
|
|
428
|
+
continue;
|
|
429
|
+
const next = experimentSession(observed, input.qualityBySessionRef);
|
|
430
|
+
postByRef.set(next.sessionRef, next);
|
|
431
|
+
}
|
|
432
|
+
const postSessions = [...postByRef.values()];
|
|
433
|
+
return createTokenReductionExperimentV0({
|
|
434
|
+
...experimentBody(experiment),
|
|
435
|
+
baselineSessions,
|
|
436
|
+
postSessions,
|
|
437
|
+
intervention: experiment.intervention
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
/** One safe cross-surface projection for CLI, MCP, and Glance adapters. */
|
|
441
|
+
export function buildActionVerificationProjectionV0(experiment) {
|
|
442
|
+
const evaluation = experiment.evaluation;
|
|
443
|
+
let state;
|
|
444
|
+
let tone;
|
|
445
|
+
let headline;
|
|
446
|
+
let detail;
|
|
447
|
+
if (experiment.lifecycle === "rolled_back") {
|
|
448
|
+
state = "rolled_back";
|
|
449
|
+
tone = "neutral";
|
|
450
|
+
headline = "Token test rolled back";
|
|
451
|
+
detail = "The rollback boundary is recorded; this attempt cannot support a reduction claim.";
|
|
452
|
+
}
|
|
453
|
+
else if (experiment.lifecycle === "invalidated") {
|
|
454
|
+
state = "cancelled";
|
|
455
|
+
tone = "neutral";
|
|
456
|
+
headline = "Token test cancelled";
|
|
457
|
+
detail = "The un-applied baseline remains in local history; start a new test from fresh evidence.";
|
|
458
|
+
}
|
|
459
|
+
else if (evaluation.rollbackRecommended) {
|
|
460
|
+
state = "rollback";
|
|
461
|
+
tone = "negative";
|
|
462
|
+
headline = "Quality or token use regressed";
|
|
463
|
+
detail = "Roll back the one approved change and keep the evidence.";
|
|
464
|
+
}
|
|
465
|
+
else if (experiment.lifecycle === "draft") {
|
|
466
|
+
state = "collect_baseline";
|
|
467
|
+
tone = "neutral";
|
|
468
|
+
headline = "Collect three comparable completed session snapshots";
|
|
469
|
+
detail = "Only explicit Claude turn or Codex task completion markers count; resumed native sessions do not become a second sample.";
|
|
470
|
+
}
|
|
471
|
+
else if (experiment.lifecycle === "baseline_ready") {
|
|
472
|
+
state = "approve_one_change";
|
|
473
|
+
tone = "attention";
|
|
474
|
+
headline = "One reversible token test is ready";
|
|
475
|
+
detail = "Define and approve one exact change, rollback, and canary before any handoff.";
|
|
476
|
+
}
|
|
477
|
+
else if (experiment.lifecycle === "applied" || experiment.lifecycle === "collecting") {
|
|
478
|
+
state = "collect_post_change";
|
|
479
|
+
tone = "neutral";
|
|
480
|
+
headline = "Collect three matched post-change sessions";
|
|
481
|
+
detail = "Record whether quality passed, failed, or is still missing.";
|
|
482
|
+
}
|
|
483
|
+
else if (evaluation.status === "measured_token_reduction") {
|
|
484
|
+
state = "review_measured_result";
|
|
485
|
+
tone = "positive";
|
|
486
|
+
headline = "A measured token reduction is ready to review";
|
|
487
|
+
detail = "This is a matched session result, not verified outcome ROI or cash savings.";
|
|
488
|
+
}
|
|
489
|
+
else if (evaluation.status === "no_measured_change") {
|
|
490
|
+
state = "review_measured_result";
|
|
491
|
+
tone = "neutral";
|
|
492
|
+
headline = "No measured token change";
|
|
493
|
+
detail = "The matched session medians were unchanged; do not claim a reduction.";
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
state = "resolve_evidence";
|
|
497
|
+
tone = "attention";
|
|
498
|
+
headline = "The token test is inconclusive";
|
|
499
|
+
detail = "Resolve missing quality or matching evidence before making a claim.";
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
schemaVersion: 0,
|
|
503
|
+
experimentId: experiment.id,
|
|
504
|
+
findingId: experiment.finding.id,
|
|
505
|
+
candidateKey: experiment.finding.candidateKey,
|
|
506
|
+
state,
|
|
507
|
+
tone,
|
|
508
|
+
headline,
|
|
509
|
+
detail,
|
|
510
|
+
evidenceLabel: evaluation.metricEvidence,
|
|
511
|
+
qualityLabel: evaluation.qualityStatus,
|
|
512
|
+
qualityEvidence: evaluation.qualityEvidence,
|
|
513
|
+
baselineSessions: evaluation.baseline.includedSessions,
|
|
514
|
+
postChangeSessions: evaluation.postChange.includedSessions,
|
|
515
|
+
minimumSessions: experiment.matchingPolicy.minimumPostSessions,
|
|
516
|
+
reductionPercent: evaluation.reductionPercent
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function comparableGroups(vitals, asOf, contextHealth) {
|
|
520
|
+
const sessions = structurallyEligibleSessions(vitals, asOf, contextHealth);
|
|
521
|
+
const groups = new Map();
|
|
522
|
+
for (const session of sessions) {
|
|
523
|
+
const project = session.vital.project;
|
|
524
|
+
const key = [
|
|
525
|
+
session.agent,
|
|
526
|
+
session.provider,
|
|
527
|
+
session.model,
|
|
528
|
+
session.projectRef,
|
|
529
|
+
session.sessionType,
|
|
530
|
+
session.workTypeRef,
|
|
531
|
+
session.sourceVersionRef ?? "source-version-missing"
|
|
532
|
+
].join("\u0000");
|
|
533
|
+
const group = groups.get(key) ?? {
|
|
534
|
+
key,
|
|
535
|
+
agent: session.agent,
|
|
536
|
+
provider: session.provider,
|
|
537
|
+
model: session.model,
|
|
538
|
+
projectRef: session.projectRef,
|
|
539
|
+
...(project ? { project } : {}),
|
|
540
|
+
sessionType: session.sessionType,
|
|
541
|
+
workTypeRef: session.workTypeRef,
|
|
542
|
+
...(session.sourceVersionRef ? { sourceVersionRef: session.sourceVersionRef } : {}),
|
|
543
|
+
sessions: []
|
|
544
|
+
};
|
|
545
|
+
group.sessions.push(session);
|
|
546
|
+
groups.set(key, group);
|
|
547
|
+
}
|
|
548
|
+
return [...groups.values()]
|
|
549
|
+
.map((group) => ({ ...group, sessions: group.sessions.sort(comparePlannerSessions) }))
|
|
550
|
+
.sort(compareGroups);
|
|
551
|
+
}
|
|
552
|
+
function structurallyEligibleSessions(vitals, asOf, contextHealth) {
|
|
553
|
+
const asOfMs = Date.parse(asOf);
|
|
554
|
+
const duplicateRefs = new Set();
|
|
555
|
+
const refCounts = new Map();
|
|
556
|
+
for (const vital of vitals.sessions) {
|
|
557
|
+
refCounts.set(vital.sessionRef, (refCounts.get(vital.sessionRef) ?? 0) + 1);
|
|
558
|
+
}
|
|
559
|
+
for (const [ref, count] of refCounts)
|
|
560
|
+
if (count > 1)
|
|
561
|
+
duplicateRefs.add(ref);
|
|
562
|
+
const activeRef = activeSessionRef(vitals.sessions, contextHealth, asOf);
|
|
563
|
+
return vitals.sessions.flatMap((vital) => {
|
|
564
|
+
if (duplicateRefs.has(vital.sessionRef) || vital.sessionRef === activeRef ||
|
|
565
|
+
!AV_REF.test(vital.sessionRef) || vital.tokenEvidence.status !== "observed" ||
|
|
566
|
+
vital.completion.status !== "completed" ||
|
|
567
|
+
vital.sessionType === "unknown" || !vital.activity ||
|
|
568
|
+
vital.models.length !== 1 ||
|
|
569
|
+
vital.sourceVersions.length > 1 ||
|
|
570
|
+
!vital.projectRef || !AV_REF.test(vital.projectRef))
|
|
571
|
+
return [];
|
|
572
|
+
const startedAt = normalizedTimestamp(vital.observedFrom);
|
|
573
|
+
const endedAt = normalizedTimestamp(vital.observedTo);
|
|
574
|
+
if (!startedAt || !endedAt || Date.parse(startedAt) > Date.parse(endedAt) ||
|
|
575
|
+
Date.parse(endedAt) > asOfMs)
|
|
576
|
+
return [];
|
|
577
|
+
const provider = providerFor(vital.agent);
|
|
578
|
+
const sourceVersionRef = createActionPlanningSourceVersionReferenceV0(vital.agent, vital.sourceVersions[0]);
|
|
579
|
+
if (!provider || !sourceVersionRef || totalTokens(vital) === null)
|
|
580
|
+
return [];
|
|
581
|
+
return [{
|
|
582
|
+
vital,
|
|
583
|
+
agent: vital.agent,
|
|
584
|
+
provider,
|
|
585
|
+
model: safeOutputIdentifier("model", vital.models[0]),
|
|
586
|
+
projectRef: vital.projectRef,
|
|
587
|
+
sessionType: vital.sessionType,
|
|
588
|
+
workTypeRef: createActionVerificationReference("coarse-work-type", `${vital.activity.kind}:${vital.activity.action}`),
|
|
589
|
+
// Missing host versions remain explicitly labeled inside the opaque
|
|
590
|
+
// reference. Every cohort is still bound to the parser contract so a
|
|
591
|
+
// parser update cannot silently compare old and new semantics.
|
|
592
|
+
sourceVersionRef,
|
|
593
|
+
startedAt,
|
|
594
|
+
endedAt
|
|
595
|
+
}];
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function activeSessionRef(sessions, contextHealth, asOf) {
|
|
599
|
+
return activePlannerSession(sessions, contextHealth, asOf)?.vital.sessionRef;
|
|
600
|
+
}
|
|
601
|
+
function activePlannerSession(sessions, contextHealth, asOf) {
|
|
602
|
+
const current = contextHealth?.currentSession;
|
|
603
|
+
if (!current || current.status !== "active" || !safeProject(current.project))
|
|
604
|
+
return undefined;
|
|
605
|
+
const scope = contextHealth?.contextChurn.currentSessionScope;
|
|
606
|
+
const refCounts = new Map();
|
|
607
|
+
for (const session of sessions) {
|
|
608
|
+
refCounts.set(session.sessionRef, (refCounts.get(session.sessionRef) ?? 0) + 1);
|
|
609
|
+
}
|
|
610
|
+
const asOfMs = asOf ? Date.parse(asOf) : Number.POSITIVE_INFINITY;
|
|
611
|
+
return sessions
|
|
612
|
+
.filter((session) => session.agent === current.agent &&
|
|
613
|
+
session.project === current.project &&
|
|
614
|
+
(scope !== "parent" && scope !== "subagent" || session.sessionType === scope) &&
|
|
615
|
+
refCounts.get(session.sessionRef) === 1)
|
|
616
|
+
.flatMap((vital) => {
|
|
617
|
+
if (!AV_REF.test(vital.sessionRef) || vital.sessionType === "unknown" ||
|
|
618
|
+
!vital.activity || vital.models.length !== 1 || vital.sourceVersions.length > 1 ||
|
|
619
|
+
!vital.projectRef || !AV_REF.test(vital.projectRef))
|
|
620
|
+
return [];
|
|
621
|
+
const startedAt = normalizedTimestamp(vital.observedFrom);
|
|
622
|
+
const endedAt = normalizedTimestamp(vital.observedTo);
|
|
623
|
+
if (!startedAt || !endedAt || Date.parse(startedAt) > Date.parse(endedAt) ||
|
|
624
|
+
Date.parse(endedAt) > asOfMs)
|
|
625
|
+
return [];
|
|
626
|
+
const provider = providerFor(vital.agent);
|
|
627
|
+
const sourceVersionRef = createActionPlanningSourceVersionReferenceV0(vital.agent, vital.sourceVersions[0]);
|
|
628
|
+
if (!provider || !sourceVersionRef)
|
|
629
|
+
return [];
|
|
630
|
+
return [{
|
|
631
|
+
vital,
|
|
632
|
+
agent: vital.agent,
|
|
633
|
+
provider,
|
|
634
|
+
model: safeOutputIdentifier("model", vital.models[0]),
|
|
635
|
+
projectRef: vital.projectRef,
|
|
636
|
+
sessionType: vital.sessionType,
|
|
637
|
+
workTypeRef: createActionVerificationReference("coarse-work-type", `${vital.activity.kind}:${vital.activity.action}`),
|
|
638
|
+
sourceVersionRef,
|
|
639
|
+
startedAt,
|
|
640
|
+
endedAt
|
|
641
|
+
}];
|
|
642
|
+
})
|
|
643
|
+
.sort((left, right) => Date.parse(right.endedAt) - Date.parse(left.endedAt) ||
|
|
644
|
+
left.vital.sessionRef.localeCompare(right.vital.sessionRef))[0];
|
|
645
|
+
}
|
|
646
|
+
function groupForActiveSession(groups, active) {
|
|
647
|
+
return groups.find((group) => group.agent === active.agent &&
|
|
648
|
+
group.provider === active.provider &&
|
|
649
|
+
group.model === active.model &&
|
|
650
|
+
group.projectRef === active.projectRef &&
|
|
651
|
+
group.sessionType === active.sessionType &&
|
|
652
|
+
group.workTypeRef === active.workTypeRef &&
|
|
653
|
+
group.sourceVersionRef === active.sourceVersionRef);
|
|
654
|
+
}
|
|
655
|
+
function findingForGroup(group, generatedAt, signal) {
|
|
656
|
+
const first = group.sessions[0];
|
|
657
|
+
const last = group.sessions.at(-1);
|
|
658
|
+
const sourceObservedAt = signal.sourceId === "session-vitals-v0"
|
|
659
|
+
? last.endedAt
|
|
660
|
+
: generatedAt;
|
|
661
|
+
return createWasteFindingV0({
|
|
662
|
+
kind: WASTE_FINDING_V0_KIND,
|
|
663
|
+
schemaVersion: WASTE_FINDING_V0_VERSION,
|
|
664
|
+
generatedAt,
|
|
665
|
+
window: {
|
|
666
|
+
start: first.startedAt,
|
|
667
|
+
// Context Health and inventory signals are observed at generation time,
|
|
668
|
+
// so their evidence window must include that observation rather than
|
|
669
|
+
// ending at the last historical baseline session.
|
|
670
|
+
end: signal.sourceId === "session-vitals-v0" ? last.endedAt : generatedAt
|
|
671
|
+
},
|
|
672
|
+
findingType: signal.findingType,
|
|
673
|
+
objective: {
|
|
674
|
+
metric: "total_tokens_per_matched_session",
|
|
675
|
+
direction: "reduce",
|
|
676
|
+
guard: "user_declared_quality_must_hold"
|
|
677
|
+
},
|
|
678
|
+
caveats: ["signal_not_cause", "no_cash_claim", "missing_outcome_evidence"],
|
|
679
|
+
candidateAction: {
|
|
680
|
+
kind: signal.action,
|
|
681
|
+
provider: group.provider,
|
|
682
|
+
surface: signal.surface ?? "session_workflow",
|
|
683
|
+
reversible: true,
|
|
684
|
+
canaryRequired: true,
|
|
685
|
+
rollbackRequired: true
|
|
686
|
+
},
|
|
687
|
+
target: signal.target,
|
|
688
|
+
scope: {
|
|
689
|
+
agent: group.agent,
|
|
690
|
+
provider: group.provider,
|
|
691
|
+
model: group.model,
|
|
692
|
+
projectRef: group.projectRef
|
|
693
|
+
},
|
|
694
|
+
source: {
|
|
695
|
+
id: signal.sourceId,
|
|
696
|
+
validationCoverage: "live_verified",
|
|
697
|
+
freshness: Date.parse(generatedAt) - Date.parse(sourceObservedAt) <= FRESH_MS
|
|
698
|
+
? "fresh"
|
|
699
|
+
: "stale"
|
|
700
|
+
},
|
|
701
|
+
metric: signal.metric,
|
|
702
|
+
evidenceRefs: boundedEvidenceRefs(group.sessions.map((session) => session.vital.sessionRef), signal.signalRef),
|
|
703
|
+
causalStatus: "unproven",
|
|
704
|
+
actionability: "inspect_only",
|
|
705
|
+
approvalRequired: true
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
function measuredDeadInventory(result) {
|
|
709
|
+
if (!result?.hasData || result.isSample || result.measuredDeadCount <= 0)
|
|
710
|
+
return null;
|
|
711
|
+
const items = result.deadItems.filter((item) => item.weightConfidence === "estimated" &&
|
|
712
|
+
item.alwaysLoadedTokens > 0 &&
|
|
713
|
+
item.kind !== "mcp_server" &&
|
|
714
|
+
item.kind !== "mcp_tool");
|
|
715
|
+
if (items.length === 0)
|
|
716
|
+
return null;
|
|
717
|
+
const byHost = new Map();
|
|
718
|
+
for (const item of items)
|
|
719
|
+
if (item.host) {
|
|
720
|
+
byHost.set(item.host, (byHost.get(item.host) ?? 0) + 1);
|
|
721
|
+
}
|
|
722
|
+
const host = [...byHost.entries()].sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))[0]?.[0];
|
|
723
|
+
return { ...(host ? { host } : {}), items };
|
|
724
|
+
}
|
|
725
|
+
function repeatedReadTargetRef(file) {
|
|
726
|
+
return createActionVerificationReference("repeated-read-file", file);
|
|
727
|
+
}
|
|
728
|
+
function configuredItemTargetRef(item) {
|
|
729
|
+
return createActionVerificationReference("configured-item", JSON.stringify({
|
|
730
|
+
kind: item.kind,
|
|
731
|
+
name: item.name,
|
|
732
|
+
scope: item.scope,
|
|
733
|
+
host: item.host ?? null,
|
|
734
|
+
path: item.path ?? null,
|
|
735
|
+
ownerDirs: [...(item.ownerDirs ?? [])].sort()
|
|
736
|
+
}));
|
|
737
|
+
}
|
|
738
|
+
function experimentSession(session, qualityBySessionRef) {
|
|
739
|
+
const evidence = session.vital.tokenEvidence;
|
|
740
|
+
if (evidence.status !== "observed")
|
|
741
|
+
throw new TypeError("Planner session lost token evidence.");
|
|
742
|
+
const cacheWriteTokens = evidence.componentEvidence.cacheWriteTokens === "observed" &&
|
|
743
|
+
evidence.cacheWrite5mTokens !== undefined &&
|
|
744
|
+
evidence.cacheWrite1hTokens !== undefined
|
|
745
|
+
? evidence.cacheWrite5mTokens + evidence.cacheWrite1hTokens
|
|
746
|
+
: null;
|
|
747
|
+
return {
|
|
748
|
+
sessionRef: session.vital.sessionRef,
|
|
749
|
+
startedAt: session.startedAt,
|
|
750
|
+
endedAt: session.endedAt,
|
|
751
|
+
agent: session.agent,
|
|
752
|
+
provider: session.provider,
|
|
753
|
+
model: session.model,
|
|
754
|
+
projectRef: session.projectRef,
|
|
755
|
+
sessionType: session.sessionType,
|
|
756
|
+
workTypeRef: session.workTypeRef,
|
|
757
|
+
...(session.sourceVersionRef ? { sourceVersionRef: session.sourceVersionRef } : {}),
|
|
758
|
+
sourceValidationCoverage: "live_verified",
|
|
759
|
+
tokens: {
|
|
760
|
+
uncachedInputTokens: evidence.inputTokens,
|
|
761
|
+
cacheReadTokens: evidence.componentEvidence.cacheReadTokens === "observed"
|
|
762
|
+
? evidence.cacheReadTokens ?? null
|
|
763
|
+
: null,
|
|
764
|
+
cacheWriteTokens,
|
|
765
|
+
toolTokens: evidence.componentEvidence.toolTokens === "observed"
|
|
766
|
+
? evidence.toolTokens ?? null
|
|
767
|
+
: null,
|
|
768
|
+
outputTokens: evidence.outputTokens,
|
|
769
|
+
thoughtTokens: evidence.componentEvidence.thoughtTokens === "observed"
|
|
770
|
+
? evidence.thoughtTokens ?? null
|
|
771
|
+
: null,
|
|
772
|
+
calculatedTotalTokens: evidence.componentTotalTokens,
|
|
773
|
+
reportedTotalTokens: evidence.reportedTotalTokens ?? null,
|
|
774
|
+
componentEvidence: {
|
|
775
|
+
uncachedInputTokens: evidence.componentEvidence.inputTokens,
|
|
776
|
+
cacheReadTokens: evidence.componentEvidence.cacheReadTokens,
|
|
777
|
+
cacheWriteTokens: evidence.componentEvidence.cacheWriteTokens,
|
|
778
|
+
toolTokens: evidence.componentEvidence.toolTokens,
|
|
779
|
+
outputTokens: evidence.componentEvidence.outputTokens,
|
|
780
|
+
thoughtTokens: evidence.componentEvidence.thoughtTokens,
|
|
781
|
+
calculatedTotalTokens: evidence.componentEvidence.componentTotalTokens,
|
|
782
|
+
reportedTotalTokens: evidence.componentEvidence.reportedTotalTokens
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
quality: qualityFor(session.vital.sessionRef, qualityBySessionRef)
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
function sessionMatchesCohort(experiment, session) {
|
|
789
|
+
const cohort = experiment.cohort;
|
|
790
|
+
return session.agent === cohort.agent &&
|
|
791
|
+
session.provider === cohort.provider &&
|
|
792
|
+
session.model === cohort.model &&
|
|
793
|
+
session.projectRef === cohort.projectRef &&
|
|
794
|
+
session.sessionType === cohort.sessionType &&
|
|
795
|
+
session.workTypeRef === cohort.workTypeRef &&
|
|
796
|
+
experiment.matchingPolicy.requireExactSourceVersion &&
|
|
797
|
+
cohort.sourceVersionRef !== undefined &&
|
|
798
|
+
session.sourceVersionRef === cohort.sourceVersionRef;
|
|
799
|
+
}
|
|
800
|
+
function qualityFor(sessionRef, qualityBySessionRef) {
|
|
801
|
+
const status = qualityBySessionRef?.[sessionRef] ?? "missing";
|
|
802
|
+
return status === "missing"
|
|
803
|
+
? { status: "missing", evidence: "missing" }
|
|
804
|
+
: { status, evidence: "user_declared" };
|
|
805
|
+
}
|
|
806
|
+
function qualityEvidenceRank(evidence) {
|
|
807
|
+
switch (evidence) {
|
|
808
|
+
case "verified": return 3;
|
|
809
|
+
case "observed": return 2;
|
|
810
|
+
case "user_declared": return 1;
|
|
811
|
+
case "missing": return 0;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function experimentBody(experiment) {
|
|
815
|
+
const { id: _id, revisionId: _revisionId, lifecycle: _lifecycle, evaluation: _evaluation, ...body } = experiment;
|
|
816
|
+
return body;
|
|
817
|
+
}
|
|
818
|
+
function findingOverlap(finding, group) {
|
|
819
|
+
const evidence = new Set(finding.evidenceRefs);
|
|
820
|
+
return group.sessions.filter((session) => evidence.has(session.vital.sessionRef)).length;
|
|
821
|
+
}
|
|
822
|
+
function boundedEvidenceRefs(sessionRefs, signalRef) {
|
|
823
|
+
const otherRefs = [...new Set(sessionRefs)]
|
|
824
|
+
.filter((reference) => reference !== signalRef)
|
|
825
|
+
.sort();
|
|
826
|
+
return [
|
|
827
|
+
...otherRefs.slice(0, MAX_WASTE_FINDING_EVIDENCE_REFS_V0 - 1),
|
|
828
|
+
signalRef
|
|
829
|
+
].sort();
|
|
830
|
+
}
|
|
831
|
+
function totalTokens(vital) {
|
|
832
|
+
if (vital.tokenEvidence.status !== "observed")
|
|
833
|
+
return null;
|
|
834
|
+
if (vital.tokenEvidence.reportedTotalTokens !== undefined &&
|
|
835
|
+
Number.isSafeInteger(vital.tokenEvidence.reportedTotalTokens)) {
|
|
836
|
+
return vital.tokenEvidence.reportedTotalTokens;
|
|
837
|
+
}
|
|
838
|
+
return vital.tokenEvidence.componentEvidence.componentTotalTokens ===
|
|
839
|
+
"calculated_complete" && Number.isSafeInteger(vital.tokenEvidence.componentTotalTokens)
|
|
840
|
+
? vital.tokenEvidence.componentTotalTokens
|
|
841
|
+
: null;
|
|
842
|
+
}
|
|
843
|
+
function providerFor(agent) {
|
|
844
|
+
if (agent === "claude-code")
|
|
845
|
+
return "anthropic";
|
|
846
|
+
if (agent === "codex")
|
|
847
|
+
return "openai";
|
|
848
|
+
return undefined;
|
|
849
|
+
}
|
|
850
|
+
function parserFormatVersionRef(agent) {
|
|
851
|
+
const descriptor = localAgentFormatDescriptor(agent);
|
|
852
|
+
if (!descriptor || descriptor.capabilities.actionPlanning !== true)
|
|
853
|
+
return undefined;
|
|
854
|
+
const semanticDigest = createHash("sha256")
|
|
855
|
+
.update(canonicalDescriptorJson(descriptor))
|
|
856
|
+
.digest("hex");
|
|
857
|
+
return createActionVerificationReference("parser-format-version", `${descriptor.id}:schema-${descriptor.schemaVersion}:semantics-${semanticDigest}`);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Produce the opaque source-semantics identity used by every action cohort.
|
|
861
|
+
* Exported so adapters and contract fixtures cannot reimplement a stale
|
|
862
|
+
* descriptor fingerprint. The host version remains inside the hash.
|
|
863
|
+
*/
|
|
864
|
+
export function createActionPlanningSourceVersionReferenceV0(agent, hostVersion) {
|
|
865
|
+
// Parser semantics alone are not an observed host version. Without the
|
|
866
|
+
// source-native host version there is no exact before/after cohort, so fail
|
|
867
|
+
// closed instead of hashing an "unknown" sentinel into apparent evidence.
|
|
868
|
+
if (!hostVersion)
|
|
869
|
+
return undefined;
|
|
870
|
+
const parserFormatRef = parserFormatVersionRef(agent);
|
|
871
|
+
if (!parserFormatRef)
|
|
872
|
+
return undefined;
|
|
873
|
+
return createActionVerificationReference("host-source-and-parser-version", `${agent}:${hostVersion}:${parserFormatRef}`);
|
|
874
|
+
}
|
|
875
|
+
function canonicalDescriptorJson(value) {
|
|
876
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
877
|
+
return JSON.stringify(value);
|
|
878
|
+
}
|
|
879
|
+
if (typeof value === "number") {
|
|
880
|
+
if (!Number.isFinite(value))
|
|
881
|
+
throw new TypeError("Parser descriptor values must be finite.");
|
|
882
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
883
|
+
}
|
|
884
|
+
if (Array.isArray(value)) {
|
|
885
|
+
return `[${value.map((item) => canonicalDescriptorJson(item)).join(",")}]`;
|
|
886
|
+
}
|
|
887
|
+
if (typeof value === "object" && value) {
|
|
888
|
+
const object = value;
|
|
889
|
+
return `{${Object.keys(object)
|
|
890
|
+
.filter((key) => object[key] !== undefined)
|
|
891
|
+
.sort()
|
|
892
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalDescriptorJson(object[key])}`)
|
|
893
|
+
.join(",")}}`;
|
|
894
|
+
}
|
|
895
|
+
throw new TypeError("Parser descriptor contains an unsupported semantic value.");
|
|
896
|
+
}
|
|
897
|
+
function safeProject(value) {
|
|
898
|
+
return Boolean(value && value !== "(home)" && value.length <= 120 &&
|
|
899
|
+
!/[\\/\u0000-\u001f\u007f]/.test(value));
|
|
900
|
+
}
|
|
901
|
+
function safeOutputIdentifier(namespace, value) {
|
|
902
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value) &&
|
|
903
|
+
!/^(?:sk-|sk_|gh[pousr]_|github_pat_|npm_|AIza|xox[baprs]-|glpat-|AKIA)/i.test(value)
|
|
904
|
+
? value
|
|
905
|
+
: createActionVerificationReference(namespace, value);
|
|
906
|
+
}
|
|
907
|
+
function timestamp(value, label) {
|
|
908
|
+
const milliseconds = Date.parse(value);
|
|
909
|
+
if (!Number.isFinite(milliseconds))
|
|
910
|
+
throw new TypeError(`${label} must be a valid timestamp.`);
|
|
911
|
+
return new Date(milliseconds).toISOString();
|
|
912
|
+
}
|
|
913
|
+
function normalizedTimestamp(value) {
|
|
914
|
+
const milliseconds = Date.parse(value);
|
|
915
|
+
return Number.isFinite(milliseconds) ? new Date(milliseconds).toISOString() : null;
|
|
916
|
+
}
|
|
917
|
+
function comparePlannerSessions(left, right) {
|
|
918
|
+
return Date.parse(left.endedAt) - Date.parse(right.endedAt) ||
|
|
919
|
+
left.vital.sessionRef.localeCompare(right.vital.sessionRef);
|
|
920
|
+
}
|
|
921
|
+
function compareGroups(left, right) {
|
|
922
|
+
const leftLatest = Date.parse(left.sessions.at(-1).endedAt);
|
|
923
|
+
const rightLatest = Date.parse(right.sessions.at(-1).endedAt);
|
|
924
|
+
return rightLatest - leftLatest || left.key.localeCompare(right.key);
|
|
925
|
+
}
|
|
926
|
+
function median(values) {
|
|
927
|
+
if (values.length === 0)
|
|
928
|
+
return null;
|
|
929
|
+
const ordered = [...values].sort((left, right) => left - right);
|
|
930
|
+
const middle = Math.floor(ordered.length / 2);
|
|
931
|
+
return ordered.length % 2
|
|
932
|
+
? ordered[middle]
|
|
933
|
+
: (ordered[middle - 1] + ordered[middle]) / 2;
|
|
934
|
+
}
|
|
935
|
+
function round(value) {
|
|
936
|
+
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
937
|
+
}
|
|
938
|
+
//# sourceMappingURL=actionPlanner.js.map
|