pi-daddy 0.41.0 → 0.43.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/CHANGELOG.md +37 -0
- package/contracts/ledger-record/v1/fixtures/capability-decision.json +3 -1
- package/contracts/ledger-record/v1/fixtures/child-lifecycle.json +25 -0
- package/contracts/ledger-record/v1/fixtures/episode-cost-gate.json +14 -0
- package/contracts/ledger-record/v1/fixtures/episode-outcome.json +12 -0
- package/contracts/ledger-record/v1/fixtures/ledger-record.jsonl +6 -3
- package/contracts/ledger-record/v1/fixtures/session-config.json +14 -0
- package/contracts/ledger-record/v1/governance-event.schema.json +356 -0
- package/dist/advisors/advisor.d.ts +24 -1
- package/dist/advisors/advisor.d.ts.map +1 -1
- package/dist/advisors/advisor.js +19 -1
- package/dist/advisors/advisor.js.map +1 -1
- package/dist/advisors/decider.d.ts +11 -0
- package/dist/advisors/decider.d.ts.map +1 -1
- package/dist/advisors/decider.js.map +1 -1
- package/dist/advisors/jev.d.ts.map +1 -1
- package/dist/advisors/jev.js +32 -1
- package/dist/advisors/jev.js.map +1 -1
- package/dist/cli.d.ts +7 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +87 -1
- package/dist/cli.js.map +1 -1
- package/dist/executors/activity-session.d.ts +13 -0
- package/dist/executors/activity-session.d.ts.map +1 -1
- package/dist/executors/activity-session.js +44 -2
- package/dist/executors/activity-session.js.map +1 -1
- package/dist/governance/episode-cost-gate.d.ts +31 -0
- package/dist/governance/episode-cost-gate.d.ts.map +1 -0
- package/dist/governance/episode-cost-gate.js +112 -0
- package/dist/governance/episode-cost-gate.js.map +1 -0
- package/dist/governance/ledger-events.d.ts +108 -1
- package/dist/governance/ledger-events.d.ts.map +1 -1
- package/dist/governance/ledger-events.js +64 -0
- package/dist/governance/ledger-events.js.map +1 -1
- package/dist/governance/ledger-v3-validation.d.ts.map +1 -1
- package/dist/governance/ledger-v3-validation.js +124 -0
- package/dist/governance/ledger-v3-validation.js.map +1 -1
- package/dist/governance/ledger.d.ts +8 -2
- package/dist/governance/ledger.d.ts.map +1 -1
- package/dist/governance/ledger.js +18 -2
- package/dist/governance/ledger.js.map +1 -1
- package/dist/kernel/context-handoff.d.ts.map +1 -1
- package/dist/kernel/context-handoff.js +4 -1
- package/dist/kernel/context-handoff.js.map +1 -1
- package/dist/kernel/definitions.d.ts +5 -0
- package/dist/kernel/definitions.d.ts.map +1 -1
- package/dist/kernel/definitions.js +30 -1
- package/dist/kernel/definitions.js.map +1 -1
- package/dist/kernel/delegate-types.d.ts +4 -0
- package/dist/kernel/delegate-types.d.ts.map +1 -1
- package/dist/kernel/delegate-types.js.map +1 -1
- package/dist/kernel/delegate.d.ts.map +1 -1
- package/dist/kernel/delegate.js +20 -5
- package/dist/kernel/delegate.js.map +1 -1
- package/dist/kernel/env-names.d.ts +5 -0
- package/dist/kernel/env-names.d.ts.map +1 -1
- package/dist/kernel/env-names.js +9 -0
- package/dist/kernel/env-names.js.map +1 -1
- package/dist/kernel/grant-env.d.ts +2 -0
- package/dist/kernel/grant-env.d.ts.map +1 -1
- package/dist/kernel/grant-env.js +1 -0
- package/dist/kernel/grant-env.js.map +1 -1
- package/dist/kernel/propagation.d.ts +2 -2
- package/dist/kernel/propagation.d.ts.map +1 -1
- package/dist/kernel/propagation.js +7 -2
- package/dist/kernel/propagation.js.map +1 -1
- package/dist/products/dashboard-cli.d.ts +4 -0
- package/dist/products/dashboard-cli.d.ts.map +1 -1
- package/dist/products/dashboard-cli.js +32 -4
- package/dist/products/dashboard-cli.js.map +1 -1
- package/dist/products/dashboard-handshake.d.ts +8 -0
- package/dist/products/dashboard-handshake.d.ts.map +1 -1
- package/dist/products/dashboard-handshake.js +3 -0
- package/dist/products/dashboard-handshake.js.map +1 -1
- package/dist/products/dashboard-herdr.d.ts +4 -0
- package/dist/products/dashboard-herdr.d.ts.map +1 -1
- package/dist/products/dashboard-herdr.js +10 -2
- package/dist/products/dashboard-herdr.js.map +1 -1
- package/dist/products/dashboard-projection.d.ts.map +1 -1
- package/dist/products/dashboard-projection.js +13 -6
- package/dist/products/dashboard-projection.js.map +1 -1
- package/dist/products/dashboard-render.d.ts +10 -0
- package/dist/products/dashboard-render.d.ts.map +1 -1
- package/dist/products/dashboard-render.js +9 -0
- package/dist/products/dashboard-render.js.map +1 -1
- package/dist/products/dashboard-session-client.d.ts +17 -0
- package/dist/products/dashboard-session-client.d.ts.map +1 -0
- package/dist/products/dashboard-session-client.js +28 -0
- package/dist/products/dashboard-session-client.js.map +1 -0
- package/dist/products/episode-outcomes.d.ts +32 -0
- package/dist/products/episode-outcomes.d.ts.map +1 -0
- package/dist/products/episode-outcomes.js +280 -0
- package/dist/products/episode-outcomes.js.map +1 -0
- package/dist/products/episode-report.d.ts +59 -0
- package/dist/products/episode-report.d.ts.map +1 -0
- package/dist/products/episode-report.js +311 -0
- package/dist/products/episode-report.js.map +1 -0
- package/extensions/chain-ledger.ts +2 -0
- package/extensions/chain-plan.ts +4 -2
- package/extensions/context-shape.ts +19 -4
- package/extensions/dashboard-session-server.ts +86 -0
- package/extensions/definition-runtime.ts +93 -0
- package/extensions/delegate-chain.ts +31 -2
- package/extensions/delegation-ledger.ts +2 -0
- package/extensions/delegation.ts +24 -0
- package/extensions/effort-advice.ts +12 -0
- package/extensions/execute-child.ts +148 -9
- package/extensions/grants-command.ts +32 -2
- package/extensions/grants.ts +23 -1
- package/extensions/pruning-advice.ts +3 -1
- package/extensions/run-delegation.ts +54 -6
- package/extensions/session-model-prompt.ts +179 -0
- package/extensions/session.ts +104 -3
- package/package.json +1 -1
- package/src/advisors/advisor.ts +45 -2
- package/src/advisors/decider.ts +8 -0
- package/src/advisors/jev.ts +34 -1
- package/src/cli.ts +90 -2
- package/src/executors/activity-session.ts +56 -2
- package/src/governance/episode-cost-gate.ts +128 -0
- package/src/governance/ledger-events.ts +173 -1
- package/src/governance/ledger-v3-validation.ts +146 -0
- package/src/governance/ledger.ts +29 -1
- package/src/kernel/context-handoff.ts +5 -1
- package/src/kernel/definitions.ts +32 -1
- package/src/kernel/delegate-types.ts +4 -0
- package/src/kernel/delegate.ts +22 -5
- package/src/kernel/env-names.ts +10 -0
- package/src/kernel/grant-env.ts +3 -0
- package/src/kernel/propagation.ts +7 -0
- package/src/products/dashboard-cli.ts +32 -4
- package/src/products/dashboard-handshake.ts +5 -0
- package/src/products/dashboard-herdr.ts +15 -1
- package/src/products/dashboard-projection.ts +13 -6
- package/src/products/dashboard-render.ts +11 -0
- package/src/products/dashboard-session-client.ts +35 -0
- package/src/products/episode-outcomes.ts +317 -0
- package/src/products/episode-report.ts +372 -0
|
@@ -18,6 +18,16 @@ import { join } from "node:path";
|
|
|
18
18
|
export interface ChildUsageObservation {
|
|
19
19
|
usage?: ChildUsageTotals;
|
|
20
20
|
unavailable?: "session-missing" | "session-invalid" | "usage-missing";
|
|
21
|
+
resolvedModel?: { provider: string; modelId: string };
|
|
22
|
+
effectiveThinkingLevel?: string;
|
|
23
|
+
tokenDetail?: {
|
|
24
|
+
inputTokens: number | null;
|
|
25
|
+
outputTokens: number | null;
|
|
26
|
+
cacheReadTokens: number | null;
|
|
27
|
+
cacheWriteTokens: number | null;
|
|
28
|
+
reasoningTokens: number | null;
|
|
29
|
+
};
|
|
30
|
+
compactionCount?: number;
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
export interface ActivitySession {
|
|
@@ -127,6 +137,9 @@ async function readChildUsage(path: string | Promise<string | undefined>): Promi
|
|
|
127
137
|
let found = false;
|
|
128
138
|
let reasoning = 0;
|
|
129
139
|
let sawReasoning = false;
|
|
140
|
+
let resolvedModel: ChildUsageObservation["resolvedModel"];
|
|
141
|
+
let effectiveThinkingLevel: string | undefined;
|
|
142
|
+
let compactionCount = 0;
|
|
130
143
|
try {
|
|
131
144
|
const lines = createInterface({ input: createReadStream(resolved, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
132
145
|
for await (const line of lines) {
|
|
@@ -136,16 +149,37 @@ async function readChildUsage(path: string | Promise<string | undefined>): Promi
|
|
|
136
149
|
} catch {
|
|
137
150
|
return { unavailable: "session-invalid" };
|
|
138
151
|
}
|
|
139
|
-
const
|
|
152
|
+
const parsed = object(entry);
|
|
153
|
+
if (
|
|
154
|
+
parsed?.type === "model_change" &&
|
|
155
|
+
typeof parsed.provider === "string" &&
|
|
156
|
+
typeof parsed.modelId === "string"
|
|
157
|
+
) {
|
|
158
|
+
resolvedModel = { provider: parsed.provider, modelId: parsed.modelId };
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (parsed?.type === "thinking_level_change" && typeof parsed.thinkingLevel === "string") {
|
|
162
|
+
effectiveThinkingLevel = parsed.thinkingLevel;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (parsed?.type === "compaction") {
|
|
166
|
+
compactionCount += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const message = object(parsed?.message);
|
|
140
170
|
if (!message) continue;
|
|
141
171
|
if (message.role === "user") {
|
|
142
172
|
Object.assign(totals, emptyUsage());
|
|
143
173
|
found = false;
|
|
144
174
|
reasoning = 0;
|
|
145
175
|
sawReasoning = false;
|
|
176
|
+
compactionCount = 0;
|
|
146
177
|
continue;
|
|
147
178
|
}
|
|
148
179
|
if (message.role !== "assistant" && message.role !== "toolResult") continue;
|
|
180
|
+
if (message.role === "assistant" && typeof message.provider === "string" && typeof message.model === "string") {
|
|
181
|
+
resolvedModel = { provider: message.provider, modelId: message.model };
|
|
182
|
+
}
|
|
149
183
|
const usage = object(message.usage);
|
|
150
184
|
if (message.role === "toolResult" && !usage) continue;
|
|
151
185
|
const cost = object(usage?.cost);
|
|
@@ -161,7 +195,26 @@ async function readChildUsage(path: string | Promise<string | undefined>): Promi
|
|
|
161
195
|
}
|
|
162
196
|
found = true;
|
|
163
197
|
}
|
|
164
|
-
|
|
198
|
+
if (!found)
|
|
199
|
+
return {
|
|
200
|
+
unavailable: "usage-missing",
|
|
201
|
+
...(resolvedModel ? { resolvedModel } : {}),
|
|
202
|
+
...(effectiveThinkingLevel ? { effectiveThinkingLevel } : {}),
|
|
203
|
+
compactionCount,
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
usage: { ...totals, ...(sawReasoning ? { reasoning } : {}) },
|
|
207
|
+
...(resolvedModel ? { resolvedModel } : {}),
|
|
208
|
+
...(effectiveThinkingLevel ? { effectiveThinkingLevel } : {}),
|
|
209
|
+
tokenDetail: {
|
|
210
|
+
inputTokens: positiveOrNull(totals.input),
|
|
211
|
+
outputTokens: positiveOrNull(totals.output),
|
|
212
|
+
cacheReadTokens: positiveOrNull(totals.cacheRead),
|
|
213
|
+
cacheWriteTokens: positiveOrNull(totals.cacheWrite),
|
|
214
|
+
reasoningTokens: sawReasoning ? positiveOrNull(reasoning) : null,
|
|
215
|
+
},
|
|
216
|
+
compactionCount,
|
|
217
|
+
};
|
|
165
218
|
} catch {
|
|
166
219
|
return { unavailable: "session-missing" };
|
|
167
220
|
}
|
|
@@ -182,3 +235,4 @@ const object = (value: unknown): Record<string, unknown> | undefined =>
|
|
|
182
235
|
value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
183
236
|
const nonNegative = (value: unknown): value is number =>
|
|
184
237
|
typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
238
|
+
const positiveOrNull = (value: number): number | null => (value > 0 ? value : null);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Per-episode cost threshold state shared by every child started by one session. */
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readRecords } from "./record.ts";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_EPISODE_COST_CEILING = 5;
|
|
6
|
+
|
|
7
|
+
export type EpisodeCostGateAnswer =
|
|
8
|
+
| { cost: number; ceiling: number; outcome: "continued"; newCeiling: number }
|
|
9
|
+
| { cost: number; ceiling: number; outcome: "stopped" };
|
|
10
|
+
|
|
11
|
+
export interface EpisodeCostGateHooks {
|
|
12
|
+
warn(message: string): void;
|
|
13
|
+
pause?(): Promise<unknown> | unknown;
|
|
14
|
+
ask?(): Promise<number | null>;
|
|
15
|
+
resume?(): Promise<unknown> | unknown;
|
|
16
|
+
stop?(): Promise<unknown> | unknown;
|
|
17
|
+
gate(event: EpisodeCostGateAnswer): Promise<unknown> | unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function episodeCostCeilingFromSettings(value: unknown): number {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return DEFAULT_EPISODE_COST_CEILING;
|
|
22
|
+
const ceiling = (value as Record<string, unknown>).episodeCostCeiling;
|
|
23
|
+
return typeof ceiling === "number" && Number.isFinite(ceiling) && ceiling > 0
|
|
24
|
+
? ceiling
|
|
25
|
+
: DEFAULT_EPISODE_COST_CEILING;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class EpisodeCostGate {
|
|
29
|
+
#ceiling: number;
|
|
30
|
+
#costs = new Map<string, number>();
|
|
31
|
+
#warned = false;
|
|
32
|
+
#usageWarning = false;
|
|
33
|
+
#decision?: Promise<"continue" | "stop">;
|
|
34
|
+
|
|
35
|
+
constructor(ceiling: number) {
|
|
36
|
+
this.#ceiling = ceiling;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get ceiling(): number {
|
|
40
|
+
return this.#ceiling;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get cost(): number {
|
|
44
|
+
return [...this.#costs.values()].reduce((sum, value) => sum + value, 0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setCeiling(ceiling: number): void {
|
|
48
|
+
if (Number.isFinite(ceiling) && ceiling > 0 && !this.#decision) this.#ceiling = ceiling;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
seedCost(executionId: string, cost: number): void {
|
|
52
|
+
if (Number.isFinite(cost) && cost >= 0) this.#costs.set(executionId, cost);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async observe(
|
|
56
|
+
executionId: string,
|
|
57
|
+
cost: number | undefined,
|
|
58
|
+
terminal: boolean,
|
|
59
|
+
hooks: EpisodeCostGateHooks,
|
|
60
|
+
): Promise<"continue" | "stop"> {
|
|
61
|
+
if (cost === undefined) {
|
|
62
|
+
if (terminal && !this.#usageWarning) {
|
|
63
|
+
this.#usageWarning = true;
|
|
64
|
+
hooks.warn("pi-daddy: provider reported no token usage; the episode cost gate cannot fire");
|
|
65
|
+
}
|
|
66
|
+
return "continue";
|
|
67
|
+
}
|
|
68
|
+
this.#costs.set(executionId, cost);
|
|
69
|
+
const total = [...this.#costs.values()].reduce((sum, value) => sum + value, 0);
|
|
70
|
+
if (!this.#warned && total >= this.#ceiling / 2) {
|
|
71
|
+
this.#warned = true;
|
|
72
|
+
hooks.warn(`pi-daddy: episode cost reached 50% of the $${this.#ceiling} ceiling`);
|
|
73
|
+
}
|
|
74
|
+
if (total <= this.#ceiling) return "continue";
|
|
75
|
+
if (this.#decision) {
|
|
76
|
+
const decision = await this.#decision;
|
|
77
|
+
if (decision === "stop") await hooks.stop?.();
|
|
78
|
+
return decision;
|
|
79
|
+
}
|
|
80
|
+
this.#decision = this.#decide(total, hooks);
|
|
81
|
+
return this.#decision;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async #decide(cost: number, hooks: EpisodeCostGateHooks): Promise<"continue" | "stop"> {
|
|
85
|
+
await hooks.pause?.();
|
|
86
|
+
const answer = await hooks.ask?.();
|
|
87
|
+
if (answer !== undefined && answer !== null && Number.isFinite(answer) && answer > cost) {
|
|
88
|
+
const prior = this.#ceiling;
|
|
89
|
+
this.#ceiling = answer;
|
|
90
|
+
try {
|
|
91
|
+
await hooks.gate({ cost, ceiling: prior, outcome: "continued", newCeiling: answer });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
await hooks.stop?.();
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
await hooks.resume?.();
|
|
97
|
+
return "continue";
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await hooks.gate({ cost, ceiling: this.#ceiling, outcome: "stopped" });
|
|
101
|
+
} finally {
|
|
102
|
+
await hooks.stop?.();
|
|
103
|
+
}
|
|
104
|
+
return "stop";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function loadEpisodeCosts(path: string, episodeId: string): Promise<Map<string, number>> {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = readRecords(await readFile(path, "utf8"));
|
|
111
|
+
if (parsed.damage) return new Map();
|
|
112
|
+
const costs = new Map<string, number>();
|
|
113
|
+
for (const record of parsed.records) {
|
|
114
|
+
const body = record.body as Record<string, unknown>;
|
|
115
|
+
if (body?.event !== "child_lifecycle" || body.episodeId !== episodeId) continue;
|
|
116
|
+
if (body.state !== "completed" && body.state !== "failed") continue;
|
|
117
|
+
const usage = body.usage as { cost?: { total?: unknown } } | undefined;
|
|
118
|
+
const cost = usage?.cost?.total;
|
|
119
|
+
if (typeof body.executionId === "string" && typeof cost === "number" && Number.isFinite(cost) && cost >= 0) {
|
|
120
|
+
costs.set(body.executionId, cost);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return costs;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return new Map();
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -63,6 +63,14 @@ export const CHILD_PROCESS_SIGNALS = [
|
|
|
63
63
|
] as const satisfies readonly NodeJS.Signals[];
|
|
64
64
|
export type ChildProcessSignal = (typeof CHILD_PROCESS_SIGNALS)[number];
|
|
65
65
|
|
|
66
|
+
export interface ChildTokenDetail {
|
|
67
|
+
inputTokens: number | null;
|
|
68
|
+
outputTokens: number | null;
|
|
69
|
+
cacheReadTokens: number | null;
|
|
70
|
+
cacheWriteTokens: number | null;
|
|
71
|
+
reasoningTokens: number | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
export interface ChildUsageTotals {
|
|
67
75
|
input: number;
|
|
68
76
|
output: number;
|
|
@@ -140,10 +148,29 @@ export interface ChildLifecycleEvent extends LedgerEventBase {
|
|
|
140
148
|
reason?: string;
|
|
141
149
|
/** The inactivity bound (ms) that governed this child beside the `deadlineAt` ceiling (PR 3e). */
|
|
142
150
|
idleTimeoutMs?: number;
|
|
151
|
+
/** The provider/model selected for the child, confirmed from its persisted assistant message when available. */
|
|
152
|
+
resolvedModel?: { provider: string; modelId: string } | null;
|
|
153
|
+
modelSource?: "explicit" | "session" | "definition" | "global" | "pi";
|
|
154
|
+
/** The persisted effective level and its selection source, when pi reports one. */
|
|
155
|
+
thinkingLevel?: {
|
|
156
|
+
level: string;
|
|
157
|
+
source: "explicit" | "session" | "advisor" | "definition" | "global" | "pi";
|
|
158
|
+
} | null;
|
|
159
|
+
thinkingSource?: "explicit" | "session" | "advisor" | "definition" | "global" | "pi";
|
|
160
|
+
/** Provider-reported token dimensions; zero is normalised to null rather than invented as reported usage. */
|
|
161
|
+
tokenDetail?: ChildTokenDetail;
|
|
143
162
|
/** Aggregate model usage read from the child's pi session file after it stopped. */
|
|
144
163
|
usage?: ChildUsageTotals;
|
|
164
|
+
/** Number of compaction entries in the child's current turn, when the session file was readable. */
|
|
165
|
+
compactionCount?: number;
|
|
145
166
|
/** Why totals could not be read; a fixed code, never transcript content. */
|
|
146
167
|
usageUnavailable?: "session-missing" | "session-invalid" | "usage-missing";
|
|
168
|
+
/** Child-facing identity variables exported at the process boundary. */
|
|
169
|
+
exportedEnvironment?: string[];
|
|
170
|
+
/** SHA-256 of the complete SKILL.md used by this child. */
|
|
171
|
+
definitionHash?: string;
|
|
172
|
+
/** Installed package version exposed for the definition, when available. */
|
|
173
|
+
definitionPackageVersion?: string;
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
export type CapabilityDecisionEvent = GrantRecord & {
|
|
@@ -154,7 +181,120 @@ export type CapabilityDecisionEvent = GrantRecord & {
|
|
|
154
181
|
taskDigest: string;
|
|
155
182
|
};
|
|
156
183
|
|
|
157
|
-
export
|
|
184
|
+
export interface EpisodeOutcomeEvent extends Omit<LedgerEventBase, "correlation"> {
|
|
185
|
+
ledgerVersion: typeof LEDGER_VERSION;
|
|
186
|
+
event: "episode_outcome";
|
|
187
|
+
episodeId: string;
|
|
188
|
+
commit: string;
|
|
189
|
+
survived: boolean;
|
|
190
|
+
ci: "green" | "red" | "none";
|
|
191
|
+
amended: boolean;
|
|
192
|
+
corrected: boolean;
|
|
193
|
+
label: "positive" | "negative" | "unknown";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface EpisodeCostGateEvent extends Omit<LedgerEventBase, "episodeId" | "correlation"> {
|
|
197
|
+
ledgerVersion: typeof LEDGER_VERSION;
|
|
198
|
+
event: "cost_gate";
|
|
199
|
+
episodeId: string;
|
|
200
|
+
executionId: string;
|
|
201
|
+
parentExecutionId: string | null;
|
|
202
|
+
childId: string;
|
|
203
|
+
gate: "episode_cost";
|
|
204
|
+
cost: number;
|
|
205
|
+
ceiling: number;
|
|
206
|
+
outcome: "continued" | "stopped";
|
|
207
|
+
newCeiling?: number;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface SessionConfigEvent extends Omit<LedgerEventBase, "correlation"> {
|
|
211
|
+
ledgerVersion: typeof LEDGER_VERSION;
|
|
212
|
+
event: "session_config";
|
|
213
|
+
episodeId: string;
|
|
214
|
+
outcome: "kept" | "changed";
|
|
215
|
+
trigger: "first-delegation" | "grants-models";
|
|
216
|
+
overrides: Record<string, { model: string; thinking: string }>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export type RuntimeLedgerEvent =
|
|
220
|
+
| CapabilityDecisionEvent
|
|
221
|
+
| WorkspaceLeaseEvent
|
|
222
|
+
| ChildLifecycleEvent
|
|
223
|
+
| EpisodeCostGateEvent
|
|
224
|
+
| SessionConfigEvent
|
|
225
|
+
| EpisodeOutcomeEvent;
|
|
226
|
+
|
|
227
|
+
export function buildSessionConfigEvent(args: {
|
|
228
|
+
episodeId: string;
|
|
229
|
+
outcome: "kept" | "changed";
|
|
230
|
+
trigger: "first-delegation" | "grants-models";
|
|
231
|
+
overrides: ReadonlyMap<string, { model?: string; thinking?: string }>;
|
|
232
|
+
now: Date;
|
|
233
|
+
}): SessionConfigEvent {
|
|
234
|
+
return assertLedgerV3Wire({
|
|
235
|
+
ledgerVersion: LEDGER_VERSION,
|
|
236
|
+
event: "session_config",
|
|
237
|
+
ts: args.now.toISOString(),
|
|
238
|
+
episodeId: args.episodeId,
|
|
239
|
+
outcome: args.outcome,
|
|
240
|
+
trigger: args.trigger,
|
|
241
|
+
overrides: Object.fromEntries(
|
|
242
|
+
[...args.overrides].map(([name, value]) => [name, { model: value.model!, thinking: value.thinking! }]),
|
|
243
|
+
),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function buildEpisodeOutcomeEvent(args: {
|
|
248
|
+
episodeId: string;
|
|
249
|
+
commit: string;
|
|
250
|
+
survived: boolean;
|
|
251
|
+
ci: "green" | "red" | "none";
|
|
252
|
+
amended: boolean;
|
|
253
|
+
corrected: boolean;
|
|
254
|
+
label: "positive" | "negative" | "unknown";
|
|
255
|
+
now: Date;
|
|
256
|
+
}): EpisodeOutcomeEvent {
|
|
257
|
+
return assertLedgerV3Wire({
|
|
258
|
+
ledgerVersion: LEDGER_VERSION,
|
|
259
|
+
event: "episode_outcome",
|
|
260
|
+
ts: args.now.toISOString(),
|
|
261
|
+
episodeId: args.episodeId,
|
|
262
|
+
commit: args.commit,
|
|
263
|
+
survived: args.survived,
|
|
264
|
+
ci: args.ci,
|
|
265
|
+
amended: args.amended,
|
|
266
|
+
corrected: args.corrected,
|
|
267
|
+
label: args.label,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function buildEpisodeCostGateEvent(args: {
|
|
272
|
+
episodeId: string;
|
|
273
|
+
executionId: string;
|
|
274
|
+
parentExecutionId: string | null;
|
|
275
|
+
childId: string;
|
|
276
|
+
cost: number;
|
|
277
|
+
ceiling: number;
|
|
278
|
+
outcome: "continued" | "stopped";
|
|
279
|
+
newCeiling?: number;
|
|
280
|
+
now: Date;
|
|
281
|
+
}): EpisodeCostGateEvent {
|
|
282
|
+
assertEventIdentity(args);
|
|
283
|
+
return assertLedgerV3Wire({
|
|
284
|
+
ledgerVersion: LEDGER_VERSION,
|
|
285
|
+
event: "cost_gate",
|
|
286
|
+
ts: args.now.toISOString(),
|
|
287
|
+
episodeId: args.episodeId,
|
|
288
|
+
executionId: args.executionId,
|
|
289
|
+
parentExecutionId: args.parentExecutionId,
|
|
290
|
+
childId: args.childId,
|
|
291
|
+
gate: "episode_cost",
|
|
292
|
+
cost: args.cost,
|
|
293
|
+
ceiling: args.ceiling,
|
|
294
|
+
outcome: args.outcome,
|
|
295
|
+
...(args.newCeiling !== undefined ? { newCeiling: args.newCeiling } : {}),
|
|
296
|
+
});
|
|
297
|
+
}
|
|
158
298
|
|
|
159
299
|
export function buildWorkspaceLeaseEvent(args: {
|
|
160
300
|
episodeId?: string;
|
|
@@ -212,12 +352,24 @@ export function buildChildLifecycleEvent(args: {
|
|
|
212
352
|
aborted?: boolean;
|
|
213
353
|
truncated?: boolean;
|
|
214
354
|
reason?: string;
|
|
355
|
+
resolvedModel?: { provider: string; modelId: string };
|
|
356
|
+
modelSource?: "explicit" | "session" | "definition" | "global" | "pi";
|
|
357
|
+
effectiveThinkingLevel?: string;
|
|
358
|
+
thinkingSource?: "explicit" | "session" | "advisor" | "definition" | "global" | "pi";
|
|
359
|
+
tokenDetail?: ChildTokenDetail;
|
|
215
360
|
usage?: ChildUsageTotals;
|
|
361
|
+
compactionCount?: number;
|
|
216
362
|
usageUnavailable?: "session-missing" | "session-invalid" | "usage-missing";
|
|
363
|
+
exportedEnvironment?: readonly string[];
|
|
364
|
+
definitionHash?: string;
|
|
365
|
+
definitionPackageVersion?: string;
|
|
217
366
|
correlation?: CorrelationMetadata;
|
|
218
367
|
now: Date;
|
|
219
368
|
}): ChildLifecycleEvent {
|
|
220
369
|
assertEventIdentity(args);
|
|
370
|
+
if (args.definitionHash && !/^[a-f0-9]{64}$/i.test(args.definitionHash)) {
|
|
371
|
+
throw new TypeError("definitionHash must be a SHA-256 hex digest");
|
|
372
|
+
}
|
|
221
373
|
return assertLedgerV3Wire({
|
|
222
374
|
ledgerVersion: LEDGER_VERSION,
|
|
223
375
|
event: "child_lifecycle",
|
|
@@ -238,8 +390,28 @@ export function buildChildLifecycleEvent(args: {
|
|
|
238
390
|
...(args.aborted ? { aborted: true } : {}),
|
|
239
391
|
...(args.truncated ? { truncated: true } : {}),
|
|
240
392
|
...(args.reason ? { reason: args.reason } : {}),
|
|
393
|
+
resolvedModel: args.resolvedModel ? structuredClone(args.resolvedModel) : null,
|
|
394
|
+
...(args.modelSource ? { modelSource: args.modelSource } : {}),
|
|
395
|
+
thinkingLevel:
|
|
396
|
+
args.effectiveThinkingLevel && args.thinkingSource
|
|
397
|
+
? { level: args.effectiveThinkingLevel, source: args.thinkingSource }
|
|
398
|
+
: null,
|
|
399
|
+
...(args.thinkingSource ? { thinkingSource: args.thinkingSource } : {}),
|
|
400
|
+
tokenDetail: args.tokenDetail
|
|
401
|
+
? structuredClone(args.tokenDetail)
|
|
402
|
+
: {
|
|
403
|
+
inputTokens: null,
|
|
404
|
+
outputTokens: null,
|
|
405
|
+
cacheReadTokens: null,
|
|
406
|
+
cacheWriteTokens: null,
|
|
407
|
+
reasoningTokens: null,
|
|
408
|
+
},
|
|
241
409
|
...(args.usage ? { usage: structuredClone(args.usage) } : {}),
|
|
410
|
+
...(args.compactionCount !== undefined ? { compactionCount: args.compactionCount } : {}),
|
|
242
411
|
...(args.usageUnavailable ? { usageUnavailable: args.usageUnavailable } : {}),
|
|
412
|
+
...(args.exportedEnvironment ? { exportedEnvironment: [...args.exportedEnvironment] } : {}),
|
|
413
|
+
...(args.definitionHash ? { definitionHash: args.definitionHash } : {}),
|
|
414
|
+
...(args.definitionPackageVersion ? { definitionPackageVersion: args.definitionPackageVersion } : {}),
|
|
243
415
|
...(args.correlation ? { correlation: structuredClone(args.correlation) } : {}),
|
|
244
416
|
});
|
|
245
417
|
}
|
|
@@ -3,6 +3,8 @@ import { isExecutionId } from "../kernel/execution-id.ts";
|
|
|
3
3
|
import { isEpisodeId } from "../kernel/episode-id.ts";
|
|
4
4
|
import { REFUSAL_CODES } from "../kernel/refusals.ts";
|
|
5
5
|
import { isLedgerCapabilityIdentifier, isLedgerDisplayIdentifier } from "../kernel/ledger-identifiers.ts";
|
|
6
|
+
import { CHILD_ATTRIBUTION_ENV_KEYS } from "../kernel/env-names.ts";
|
|
7
|
+
import { isDefinitionPackageVersion } from "../kernel/definitions.ts";
|
|
6
8
|
|
|
7
9
|
export type LedgerV3Object = Record<string, unknown>;
|
|
8
10
|
|
|
@@ -103,6 +105,8 @@ const FIELDS = {
|
|
|
103
105
|
"humanDenied",
|
|
104
106
|
"gateOutcome",
|
|
105
107
|
"definitionDigest",
|
|
108
|
+
"definitionHash",
|
|
109
|
+
"definitionPackageVersion",
|
|
106
110
|
"handoff",
|
|
107
111
|
"executor",
|
|
108
112
|
"taskFrom",
|
|
@@ -128,6 +132,33 @@ const FIELDS = {
|
|
|
128
132
|
"refusal",
|
|
129
133
|
"correlation",
|
|
130
134
|
]),
|
|
135
|
+
cost_gate: new Set([
|
|
136
|
+
"ledgerVersion",
|
|
137
|
+
"event",
|
|
138
|
+
"ts",
|
|
139
|
+
"episodeId",
|
|
140
|
+
"executionId",
|
|
141
|
+
"parentExecutionId",
|
|
142
|
+
"childId",
|
|
143
|
+
"gate",
|
|
144
|
+
"cost",
|
|
145
|
+
"ceiling",
|
|
146
|
+
"outcome",
|
|
147
|
+
"newCeiling",
|
|
148
|
+
]),
|
|
149
|
+
session_config: new Set(["ledgerVersion", "event", "ts", "episodeId", "outcome", "trigger", "overrides"]),
|
|
150
|
+
episode_outcome: new Set([
|
|
151
|
+
"ledgerVersion",
|
|
152
|
+
"event",
|
|
153
|
+
"ts",
|
|
154
|
+
"episodeId",
|
|
155
|
+
"commit",
|
|
156
|
+
"survived",
|
|
157
|
+
"ci",
|
|
158
|
+
"amended",
|
|
159
|
+
"corrected",
|
|
160
|
+
"label",
|
|
161
|
+
]),
|
|
131
162
|
child_lifecycle: new Set([
|
|
132
163
|
"ledgerVersion",
|
|
133
164
|
"event",
|
|
@@ -146,8 +177,17 @@ const FIELDS = {
|
|
|
146
177
|
"reason",
|
|
147
178
|
"deadlineAt",
|
|
148
179
|
"idleTimeoutMs",
|
|
180
|
+
"resolvedModel",
|
|
181
|
+
"modelSource",
|
|
182
|
+
"thinkingLevel",
|
|
183
|
+
"thinkingSource",
|
|
184
|
+
"tokenDetail",
|
|
149
185
|
"usage",
|
|
186
|
+
"compactionCount",
|
|
150
187
|
"usageUnavailable",
|
|
188
|
+
"exportedEnvironment",
|
|
189
|
+
"definitionHash",
|
|
190
|
+
"definitionPackageVersion",
|
|
151
191
|
"herdrPaneId",
|
|
152
192
|
"herdrAgentName",
|
|
153
193
|
"correlation",
|
|
@@ -297,6 +337,8 @@ function validateCapabilityDecision(event: LedgerV3Object): string | null {
|
|
|
297
337
|
!optional(event, "humanDenied", (value) => value === true) ||
|
|
298
338
|
!optional(event, "gateOutcome", (value) => GATE_OUTCOMES.has(String(value))) ||
|
|
299
339
|
!optional(event, "definitionDigest", validDefinitionDigest) ||
|
|
340
|
+
!optional(event, "definitionHash", (value) => SHA256_RE.test(String(value))) ||
|
|
341
|
+
!optional(event, "definitionPackageVersion", isDefinitionPackageVersion) ||
|
|
300
342
|
!optional(event, "taskFrom", isLedgerDisplayIdentifier) ||
|
|
301
343
|
!optional(event, "taskFromExecutionId", isExecutionId) ||
|
|
302
344
|
!optional(event, "correlation", validCorrelation) ||
|
|
@@ -354,6 +396,81 @@ function validUsage(value: unknown): boolean {
|
|
|
354
396
|
);
|
|
355
397
|
}
|
|
356
398
|
|
|
399
|
+
function validResolvedModel(value: unknown): boolean {
|
|
400
|
+
return (
|
|
401
|
+
value === null ||
|
|
402
|
+
(isLedgerObject(value) &&
|
|
403
|
+
Object.keys(value).every((key) => ["provider", "modelId"].includes(key)) &&
|
|
404
|
+
isNonEmptyString(value.provider) &&
|
|
405
|
+
isNonEmptyString(value.modelId))
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const MODEL_SOURCES = ["explicit", "session", "definition", "global", "pi"];
|
|
410
|
+
const THINKING_SOURCES = [...MODEL_SOURCES, "advisor"];
|
|
411
|
+
function validThinkingLevel(value: unknown): boolean {
|
|
412
|
+
return (
|
|
413
|
+
value === null ||
|
|
414
|
+
(isLedgerObject(value) &&
|
|
415
|
+
Object.keys(value).every((key) => ["level", "source"].includes(key)) &&
|
|
416
|
+
isNonEmptyString(value.level) &&
|
|
417
|
+
THINKING_SOURCES.includes(String(value.source)))
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function validTokenDetail(value: unknown): boolean {
|
|
422
|
+
if (!isLedgerObject(value)) return false;
|
|
423
|
+
const fields = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens", "reasoningTokens"];
|
|
424
|
+
return (
|
|
425
|
+
Object.keys(value).length === fields.length &&
|
|
426
|
+
fields.every((field) => value[field] === null || (nonNegativeNumber(value[field]) && (value[field] as number) > 0))
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function validExportedEnvironment(value: unknown): boolean {
|
|
431
|
+
return (
|
|
432
|
+
Array.isArray(value) &&
|
|
433
|
+
value.length === CHILD_ATTRIBUTION_ENV_KEYS.length &&
|
|
434
|
+
CHILD_ATTRIBUTION_ENV_KEYS.every((name) => value.filter((entry) => entry === name).length === 1)
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function validateCostGate(event: LedgerV3Object): string | null {
|
|
439
|
+
if (
|
|
440
|
+
event.gate !== "episode_cost" ||
|
|
441
|
+
!nonNegativeNumber(event.cost) ||
|
|
442
|
+
!nonNegativeNumber(event.ceiling) ||
|
|
443
|
+
(event.ceiling as number) <= 0 ||
|
|
444
|
+
!["continued", "stopped"].includes(String(event.outcome))
|
|
445
|
+
) {
|
|
446
|
+
return "cost gate required fields are invalid";
|
|
447
|
+
}
|
|
448
|
+
if (
|
|
449
|
+
!optional(event, "newCeiling", (value) => nonNegativeNumber(value) && (value as number) > (event.cost as number))
|
|
450
|
+
) {
|
|
451
|
+
return "cost gate new ceiling is invalid";
|
|
452
|
+
}
|
|
453
|
+
if ((event.outcome === "continued") !== Object.hasOwn(event, "newCeiling")) {
|
|
454
|
+
return "continued cost gate requires exactly one new ceiling";
|
|
455
|
+
}
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function validateEpisodeOutcome(event: LedgerV3Object): string | null {
|
|
460
|
+
if (
|
|
461
|
+
!isTimestamp(event.ts) ||
|
|
462
|
+
!isEpisodeId(event.episodeId) ||
|
|
463
|
+
!/^[a-f0-9]{40}$/i.test(String(event.commit)) ||
|
|
464
|
+
typeof event.survived !== "boolean" ||
|
|
465
|
+
!["green", "red", "none"].includes(String(event.ci)) ||
|
|
466
|
+
typeof event.amended !== "boolean" ||
|
|
467
|
+
typeof event.corrected !== "boolean" ||
|
|
468
|
+
!["positive", "negative", "unknown"].includes(String(event.label))
|
|
469
|
+
)
|
|
470
|
+
return "episode outcome fields are invalid";
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
357
474
|
function validateChildLifecycle(event: LedgerV3Object): string | null {
|
|
358
475
|
if (
|
|
359
476
|
!["starting", "running", "completed", "failed"].includes(String(event.state)) ||
|
|
@@ -373,10 +490,19 @@ function validateChildLifecycle(event: LedgerV3Object): string | null {
|
|
|
373
490
|
!optional(event, "aborted", (value) => value === true) ||
|
|
374
491
|
!optional(event, "truncated", (value) => value === true) ||
|
|
375
492
|
!optional(event, "reason", (value) => typeof value === "string") ||
|
|
493
|
+
!optional(event, "resolvedModel", validResolvedModel) ||
|
|
494
|
+
!optional(event, "modelSource", (value) => MODEL_SOURCES.includes(String(value))) ||
|
|
495
|
+
!optional(event, "thinkingLevel", validThinkingLevel) ||
|
|
496
|
+
!optional(event, "thinkingSource", (value) => THINKING_SOURCES.includes(String(value))) ||
|
|
497
|
+
!optional(event, "tokenDetail", validTokenDetail) ||
|
|
376
498
|
!optional(event, "usage", validUsage) ||
|
|
499
|
+
!optional(event, "compactionCount", (value) => Number.isInteger(value) && (value as number) >= 0) ||
|
|
377
500
|
!optional(event, "usageUnavailable", (value) =>
|
|
378
501
|
["session-missing", "session-invalid", "usage-missing"].includes(String(value)),
|
|
379
502
|
) ||
|
|
503
|
+
!optional(event, "exportedEnvironment", validExportedEnvironment) ||
|
|
504
|
+
!optional(event, "definitionHash", (value) => SHA256_RE.test(String(value))) ||
|
|
505
|
+
!optional(event, "definitionPackageVersion", isDefinitionPackageVersion) ||
|
|
380
506
|
!optional(event, "correlation", validCorrelation)
|
|
381
507
|
) {
|
|
382
508
|
return "child lifecycle optional fields are invalid";
|
|
@@ -407,10 +533,30 @@ export function validateLedgerV3Event(event: LedgerV3Object): string | null {
|
|
|
407
533
|
if (typeof kind !== "string" || !Object.hasOwn(FIELDS, kind)) return "unknown ledger event discriminator";
|
|
408
534
|
const allowed = FIELDS[kind as EventKind];
|
|
409
535
|
if (Object.keys(event).some((field) => !allowed.has(field))) return "ledger v3 contains an unsupported field";
|
|
536
|
+
if (kind === "session_config") {
|
|
537
|
+
if (
|
|
538
|
+
!isEpisodeId(event.episodeId) ||
|
|
539
|
+
!["kept", "changed"].includes(String(event.outcome)) ||
|
|
540
|
+
!["first-delegation", "grants-models"].includes(String(event.trigger)) ||
|
|
541
|
+
!isLedgerObject(event.overrides) ||
|
|
542
|
+
!Object.entries(event.overrides).every(
|
|
543
|
+
([name, value]) =>
|
|
544
|
+
isNonEmptyString(name) &&
|
|
545
|
+
isLedgerObject(value) &&
|
|
546
|
+
Object.keys(value).length === 2 &&
|
|
547
|
+
isNonEmptyString(value.model) &&
|
|
548
|
+
isNonEmptyString(value.thinking),
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
return "session config fields are invalid";
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
if (kind === "episode_outcome") return validateEpisodeOutcome(event);
|
|
410
555
|
const base = validateBase(event);
|
|
411
556
|
if (base) return base;
|
|
412
557
|
if (kind === "capability_decision") return validateCapabilityDecision(event);
|
|
413
558
|
if (kind === "workspace_lease") return validateWorkspaceLease(event);
|
|
559
|
+
if (kind === "cost_gate") return validateCostGate(event);
|
|
414
560
|
return validateChildLifecycle(event);
|
|
415
561
|
}
|
|
416
562
|
|