pi-daddy 0.40.1 → 0.41.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 +11 -0
- package/contracts/ledger-record/v1/fixtures/capability-decision.json +1 -0
- package/contracts/ledger-record/v1/fixtures/child-lifecycle.json +16 -0
- package/contracts/ledger-record/v1/fixtures/ledger-record.jsonl +3 -3
- package/contracts/ledger-record/v1/fixtures/workspace-lease.json +1 -0
- package/contracts/ledger-record/v1/governance-event.schema.json +100 -0
- package/dist/advisors/advisor.d.ts +19 -1
- package/dist/advisors/advisor.d.ts.map +1 -1
- package/dist/advisors/advisor.js +62 -2
- package/dist/advisors/advisor.js.map +1 -1
- package/dist/advisors/decider.d.ts +2 -4
- package/dist/advisors/decider.d.ts.map +1 -1
- package/dist/advisors/decider.js.map +1 -1
- package/dist/advisors/settings.d.ts +3 -1
- package/dist/advisors/settings.d.ts.map +1 -1
- package/dist/advisors/settings.js +4 -3
- package/dist/advisors/settings.js.map +1 -1
- package/dist/executors/activity-session.d.ts +7 -0
- package/dist/executors/activity-session.d.ts.map +1 -1
- package/dist/executors/activity-session.js +101 -3
- package/dist/executors/activity-session.js.map +1 -1
- package/dist/governance/ledger-events.d.ts +23 -0
- package/dist/governance/ledger-events.d.ts.map +1 -1
- package/dist/governance/ledger-events.js +7 -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 +29 -0
- package/dist/governance/ledger-v3-validation.js.map +1 -1
- package/dist/governance/ledger.d.ts +5 -1
- package/dist/governance/ledger.d.ts.map +1 -1
- package/dist/governance/ledger.js +4 -0
- package/dist/governance/ledger.js.map +1 -1
- package/dist/kernel/delegate-types.d.ts +2 -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 +3 -1
- package/dist/kernel/delegate.js.map +1 -1
- package/dist/kernel/env-names.d.ts +3 -0
- package/dist/kernel/env-names.d.ts.map +1 -1
- package/dist/kernel/env-names.js +5 -0
- package/dist/kernel/env-names.js.map +1 -1
- package/dist/kernel/episode-id.d.ts +6 -0
- package/dist/kernel/episode-id.d.ts.map +1 -0
- package/dist/kernel/episode-id.js +13 -0
- package/dist/kernel/episode-id.js.map +1 -0
- package/dist/kernel/propagation.d.ts +4 -2
- package/dist/kernel/propagation.d.ts.map +1 -1
- package/dist/kernel/propagation.js +6 -2
- package/dist/kernel/propagation.js.map +1 -1
- package/dist/products/activity-timeline.d.ts +3 -0
- package/dist/products/activity-timeline.d.ts.map +1 -1
- package/dist/products/activity-timeline.js +6 -0
- package/dist/products/activity-timeline.js.map +1 -1
- package/extensions/activity-timeline.ts +11 -5
- package/extensions/advisor-session.ts +3 -0
- package/extensions/chain-ledger.ts +1 -0
- package/extensions/delegation-ledger.ts +1 -0
- package/extensions/effort-advice.ts +6 -5
- package/extensions/execute-child.ts +16 -0
- package/extensions/grant-store-refusal.ts +1 -0
- package/extensions/grants-command.ts +4 -4
- package/extensions/grants.ts +4 -1
- package/extensions/pruning-advice.ts +37 -6
- package/extensions/reload-environment.ts +1 -0
- package/extensions/run-delegation.ts +10 -1
- package/extensions/session-environment.ts +9 -0
- package/extensions/session.ts +27 -4
- package/extensions/workspace-runtime.ts +5 -0
- package/package.json +1 -1
- package/src/advisors/advisor.ts +83 -3
- package/src/advisors/decider.ts +2 -4
- package/src/advisors/settings.ts +6 -4
- package/src/executors/activity-session.ts +105 -3
- package/src/governance/ledger-events.ts +35 -1
- package/src/governance/ledger-v3-validation.ts +44 -0
- package/src/governance/ledger.ts +8 -0
- package/src/kernel/delegate-types.ts +2 -0
- package/src/kernel/delegate.ts +2 -0
- package/src/kernel/env-names.ts +5 -0
- package/src/kernel/episode-id.ts +18 -0
- package/src/kernel/propagation.ts +8 -0
- package/src/products/activity-timeline.ts +8 -0
package/src/advisors/decider.ts
CHANGED
|
@@ -35,10 +35,8 @@ export interface AdviceRequest {
|
|
|
35
35
|
* What the advisor is told about the situation, composed by the caller.
|
|
36
36
|
*
|
|
37
37
|
* **Sent, never recorded.** The task is never STORED (ADR-0021) and that still holds — `createAdvisor` writes the
|
|
38
|
-
* question keys and the answers and never this object.
|
|
39
|
-
*
|
|
40
|
-
* default. An earlier draft of this paragraph said the raw task "must not be shipped to a third party either",
|
|
41
|
-
* which the first decision point then did; the rule that survived review is the narrower and true one.
|
|
38
|
+
* question keys and the answers and never this object. Raw task text crosses only when the independent egress
|
|
39
|
+
* switch permits it; otherwise callers send a structural digest. Neither representation is copied into the record.
|
|
42
40
|
*/
|
|
43
41
|
state: Readonly<Record<string, unknown>>;
|
|
44
42
|
questions: Readonly<Record<string, Question>>;
|
package/src/advisors/settings.ts
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
|
|
26
26
|
// Spelled once, in the kernel's table, so this layer cannot drift from the list `childEnv` refuses to write.
|
|
27
27
|
export { ENV_ADVISOR_KEY as ADVISOR_KEY_ENV } from "../kernel/env-names.ts";
|
|
28
|
-
import { ENV_ADVISOR, ENV_ADVISOR_KEY, ENV_ADVISOR_MODEL } from "../kernel/env-names.ts";
|
|
29
|
-
import { DEFAULT_ADVICE_TIMEOUT_MS } from "./advisor.ts";
|
|
30
|
-
export { ENV_ADVISOR_MODEL } from "../kernel/env-names.ts";
|
|
28
|
+
import { ENV_ADVISOR, ENV_ADVISOR_KEY, ENV_ADVISOR_MODEL, ENV_ADVISOR_TASK_EGRESS } from "../kernel/env-names.ts";
|
|
29
|
+
import { DEFAULT_ADVICE_TIMEOUT_MS, type TaskEgressMode } from "./advisor.ts";
|
|
30
|
+
export { ENV_ADVISOR_MODEL, ENV_ADVISOR_TASK_EGRESS } from "../kernel/env-names.ts";
|
|
31
31
|
export { ENV_ADVISOR } from "../kernel/env-names.ts";
|
|
32
32
|
|
|
33
33
|
export interface AdvisorSettings {
|
|
@@ -37,11 +37,12 @@ export interface AdvisorSettings {
|
|
|
37
37
|
/** Overrides the adapter's pinned model id; absent means the adapter's own default. */
|
|
38
38
|
model?: string;
|
|
39
39
|
timeoutMs?: number;
|
|
40
|
+
taskEgress: TaskEgressMode;
|
|
40
41
|
/** Why an advisor is off when the settings asked for one on — reported, never silently applied. */
|
|
41
42
|
refusal?: string;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
export const ADVISOR_OFF: AdvisorSettings = Object.freeze({ enabled: false, decider: "none" });
|
|
45
|
+
export const ADVISOR_OFF: AdvisorSettings = Object.freeze({ enabled: false, decider: "none", taskEgress: "digest" });
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Read the `advisor` block of a project settings file. Absent is off; malformed is off WITH a reason.
|
|
@@ -103,6 +104,7 @@ export function advisorSettingsFrom(raw: unknown, env: NodeJS.ProcessEnv = proce
|
|
|
103
104
|
return {
|
|
104
105
|
enabled: true,
|
|
105
106
|
decider: "jev",
|
|
107
|
+
taskEgress: env[ENV_ADVISOR_TASK_EGRESS]?.trim() === "raw" ? "raw" : "digest",
|
|
106
108
|
...(model ? { model } : {}),
|
|
107
109
|
// Clamped, never raised: a longer bound is not a narrowing either, and a child-writable 30s would be a stall on
|
|
108
110
|
// every delegation.
|
|
@@ -8,10 +8,18 @@
|
|
|
8
8
|
* already carries `--session` (native-session retention), that file is the probe; otherwise a private temporary
|
|
9
9
|
* file is allocated for the run and removed afterwards.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { createReadStream } from "node:fs";
|
|
12
|
+
import { mkdtemp, readdir, rm, stat } from "node:fs/promises";
|
|
13
|
+
import { createInterface } from "node:readline";
|
|
14
|
+
import type { ChildUsageTotals } from "../governance/ledger-events.ts";
|
|
12
15
|
import { tmpdir } from "node:os";
|
|
13
16
|
import { join } from "node:path";
|
|
14
17
|
|
|
18
|
+
export interface ChildUsageObservation {
|
|
19
|
+
usage?: ChildUsageTotals;
|
|
20
|
+
unavailable?: "session-missing" | "session-invalid" | "usage-missing";
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
export interface ActivitySession {
|
|
16
24
|
/** The child argv, with `--session <file>` in place of `--no-session` when a file was allocated here. */
|
|
17
25
|
readonly args: string[];
|
|
@@ -19,6 +27,8 @@ export interface ActivitySession {
|
|
|
19
27
|
readonly path: string;
|
|
20
28
|
/** A marker that changes whenever pi appended to the file; `undefined` until the file exists. */
|
|
21
29
|
probe(): Promise<string | undefined>;
|
|
30
|
+
/** Aggregate only the current child turn's usage; no transcript content leaves this reader. */
|
|
31
|
+
usage(): Promise<ChildUsageObservation>;
|
|
22
32
|
/** Remove the temporary file, if this run allocated one. Never removes a retention target. */
|
|
23
33
|
dispose(): Promise<void>;
|
|
24
34
|
}
|
|
@@ -50,7 +60,13 @@ export async function activitySessionFor(planArgs: string[], executionId: string
|
|
|
50
60
|
const fork = planArgs.indexOf("--fork");
|
|
51
61
|
if (fork >= 0) {
|
|
52
62
|
const dir = planArgs[planArgs.indexOf("--session-dir") + 1];
|
|
53
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
args: planArgs,
|
|
65
|
+
path: dir,
|
|
66
|
+
probe: probeDirectory(dir),
|
|
67
|
+
usage: () => readChildUsage(newestSessionFile(dir)),
|
|
68
|
+
dispose: async () => undefined,
|
|
69
|
+
};
|
|
54
70
|
}
|
|
55
71
|
const flag = planArgs.indexOf("--session");
|
|
56
72
|
const probeFor = (path: string) => async () => {
|
|
@@ -63,7 +79,13 @@ export async function activitySessionFor(planArgs: string[], executionId: string
|
|
|
63
79
|
};
|
|
64
80
|
if (flag >= 0 && planArgs[flag + 1]) {
|
|
65
81
|
const path = planArgs[flag + 1];
|
|
66
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
args: planArgs,
|
|
84
|
+
path,
|
|
85
|
+
probe: probeFor(path),
|
|
86
|
+
usage: () => readChildUsage(path),
|
|
87
|
+
dispose: async () => undefined,
|
|
88
|
+
};
|
|
67
89
|
}
|
|
68
90
|
// Private to this uid (mkdtemp is 0o700) and named by the execution so a leaked directory is attributable.
|
|
69
91
|
const directory = await mkdtemp(join(tmpdir(), `pi-daddy-${executionId.replace(/[^a-zA-Z0-9_-]/g, "_")}-`));
|
|
@@ -77,6 +99,86 @@ export async function activitySessionFor(planArgs: string[], executionId: string
|
|
|
77
99
|
args,
|
|
78
100
|
path,
|
|
79
101
|
probe: probeFor(path),
|
|
102
|
+
usage: () => readChildUsage(path),
|
|
80
103
|
dispose: () => rm(directory, { recursive: true, force: true }).catch(() => undefined),
|
|
81
104
|
};
|
|
82
105
|
}
|
|
106
|
+
|
|
107
|
+
async function newestSessionFile(directory: string): Promise<string | undefined> {
|
|
108
|
+
try {
|
|
109
|
+
const entries = await Promise.all(
|
|
110
|
+
(await readdir(directory))
|
|
111
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
112
|
+
.map(async (name) => ({ path: join(directory, name), modified: (await stat(join(directory, name))).mtimeMs })),
|
|
113
|
+
);
|
|
114
|
+
return entries.sort((a, b) => b.modified - a.modified)[0]?.path;
|
|
115
|
+
} catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const TOKEN_FIELDS = ["input", "output", "cacheRead", "cacheWrite", "totalTokens"] as const;
|
|
121
|
+
const COST_FIELDS = ["input", "output", "cacheRead", "cacheWrite", "total"] as const;
|
|
122
|
+
|
|
123
|
+
async function readChildUsage(path: string | Promise<string | undefined>): Promise<ChildUsageObservation> {
|
|
124
|
+
const resolved = await path;
|
|
125
|
+
if (!resolved) return { unavailable: "session-missing" };
|
|
126
|
+
const totals = emptyUsage();
|
|
127
|
+
let found = false;
|
|
128
|
+
let reasoning = 0;
|
|
129
|
+
let sawReasoning = false;
|
|
130
|
+
try {
|
|
131
|
+
const lines = createInterface({ input: createReadStream(resolved, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
132
|
+
for await (const line of lines) {
|
|
133
|
+
let entry: unknown;
|
|
134
|
+
try {
|
|
135
|
+
entry = JSON.parse(line);
|
|
136
|
+
} catch {
|
|
137
|
+
return { unavailable: "session-invalid" };
|
|
138
|
+
}
|
|
139
|
+
const message = object(object(entry)?.message);
|
|
140
|
+
if (!message) continue;
|
|
141
|
+
if (message.role === "user") {
|
|
142
|
+
Object.assign(totals, emptyUsage());
|
|
143
|
+
found = false;
|
|
144
|
+
reasoning = 0;
|
|
145
|
+
sawReasoning = false;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (message.role !== "assistant" && message.role !== "toolResult") continue;
|
|
149
|
+
const usage = object(message.usage);
|
|
150
|
+
if (message.role === "toolResult" && !usage) continue;
|
|
151
|
+
const cost = object(usage?.cost);
|
|
152
|
+
if (!usage || !cost) return { unavailable: "session-invalid" };
|
|
153
|
+
if (!TOKEN_FIELDS.every((field) => nonNegative(usage[field]))) return { unavailable: "session-invalid" };
|
|
154
|
+
if (!COST_FIELDS.every((field) => nonNegative(cost[field]))) return { unavailable: "session-invalid" };
|
|
155
|
+
if (usage.reasoning !== undefined && !nonNegative(usage.reasoning)) return { unavailable: "session-invalid" };
|
|
156
|
+
for (const field of TOKEN_FIELDS) totals[field] += usage[field] as number;
|
|
157
|
+
for (const field of COST_FIELDS) totals.cost[field] += cost[field] as number;
|
|
158
|
+
if (usage.reasoning !== undefined) {
|
|
159
|
+
reasoning += usage.reasoning as number;
|
|
160
|
+
sawReasoning = true;
|
|
161
|
+
}
|
|
162
|
+
found = true;
|
|
163
|
+
}
|
|
164
|
+
return found ? { usage: { ...totals, ...(sawReasoning ? { reasoning } : {}) } } : { unavailable: "usage-missing" };
|
|
165
|
+
} catch {
|
|
166
|
+
return { unavailable: "session-missing" };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function emptyUsage(): ChildUsageTotals {
|
|
171
|
+
return {
|
|
172
|
+
input: 0,
|
|
173
|
+
output: 0,
|
|
174
|
+
cacheRead: 0,
|
|
175
|
+
cacheWrite: 0,
|
|
176
|
+
totalTokens: 0,
|
|
177
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const object = (value: unknown): Record<string, unknown> | undefined =>
|
|
182
|
+
value !== null && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
|
183
|
+
const nonNegative = (value: unknown): value is number =>
|
|
184
|
+
typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
@@ -9,6 +9,7 @@ import type { ExecutorKind } from "../kernel/delegate-types.ts";
|
|
|
9
9
|
import type { CorrelationMetadata } from "../kernel/correlation.ts";
|
|
10
10
|
import type { StructuredRefusal } from "../kernel/refusals.ts";
|
|
11
11
|
import { assertExecutionId } from "../kernel/execution-id.ts";
|
|
12
|
+
import { assertEpisodeId } from "../kernel/episode-id.ts";
|
|
12
13
|
import { assertLedgerV3Wire } from "./ledger-v3-validation.ts";
|
|
13
14
|
|
|
14
15
|
export const WORKSPACE_ACCESSES = ["read", "write"] as const;
|
|
@@ -62,6 +63,22 @@ export const CHILD_PROCESS_SIGNALS = [
|
|
|
62
63
|
] as const satisfies readonly NodeJS.Signals[];
|
|
63
64
|
export type ChildProcessSignal = (typeof CHILD_PROCESS_SIGNALS)[number];
|
|
64
65
|
|
|
66
|
+
export interface ChildUsageTotals {
|
|
67
|
+
input: number;
|
|
68
|
+
output: number;
|
|
69
|
+
cacheRead: number;
|
|
70
|
+
cacheWrite: number;
|
|
71
|
+
reasoning?: number;
|
|
72
|
+
totalTokens: number;
|
|
73
|
+
cost: {
|
|
74
|
+
input: number;
|
|
75
|
+
output: number;
|
|
76
|
+
cacheRead: number;
|
|
77
|
+
cacheWrite: number;
|
|
78
|
+
total: number;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
65
82
|
/**
|
|
66
83
|
* `released` is a handover this owner performed. FOUR members were added by the 0.18.0 review pass, and
|
|
67
84
|
* they were not all previously recorded the same way — `uncontended` was recorded as an *acquisition*, and
|
|
@@ -123,6 +140,10 @@ export interface ChildLifecycleEvent extends LedgerEventBase {
|
|
|
123
140
|
reason?: string;
|
|
124
141
|
/** The inactivity bound (ms) that governed this child beside the `deadlineAt` ceiling (PR 3e). */
|
|
125
142
|
idleTimeoutMs?: number;
|
|
143
|
+
/** Aggregate model usage read from the child's pi session file after it stopped. */
|
|
144
|
+
usage?: ChildUsageTotals;
|
|
145
|
+
/** Why totals could not be read; a fixed code, never transcript content. */
|
|
146
|
+
usageUnavailable?: "session-missing" | "session-invalid" | "usage-missing";
|
|
126
147
|
}
|
|
127
148
|
|
|
128
149
|
export type CapabilityDecisionEvent = GrantRecord & {
|
|
@@ -136,6 +157,7 @@ export type CapabilityDecisionEvent = GrantRecord & {
|
|
|
136
157
|
export type RuntimeLedgerEvent = CapabilityDecisionEvent | WorkspaceLeaseEvent | ChildLifecycleEvent;
|
|
137
158
|
|
|
138
159
|
export function buildWorkspaceLeaseEvent(args: {
|
|
160
|
+
episodeId?: string;
|
|
139
161
|
executionId: string;
|
|
140
162
|
parentExecutionId: string | null;
|
|
141
163
|
childId: string;
|
|
@@ -154,6 +176,7 @@ export function buildWorkspaceLeaseEvent(args: {
|
|
|
154
176
|
ledgerVersion: LEDGER_VERSION,
|
|
155
177
|
event: "workspace_lease",
|
|
156
178
|
ts: args.now.toISOString(),
|
|
179
|
+
...(args.episodeId ? { episodeId: args.episodeId } : {}),
|
|
157
180
|
executionId: args.executionId,
|
|
158
181
|
parentExecutionId: args.parentExecutionId,
|
|
159
182
|
childId: args.childId,
|
|
@@ -173,6 +196,7 @@ export function buildWorkspaceLeaseEvent(args: {
|
|
|
173
196
|
}
|
|
174
197
|
|
|
175
198
|
export function buildChildLifecycleEvent(args: {
|
|
199
|
+
episodeId?: string;
|
|
176
200
|
executionId: string;
|
|
177
201
|
parentExecutionId: string | null;
|
|
178
202
|
childId: string;
|
|
@@ -188,6 +212,8 @@ export function buildChildLifecycleEvent(args: {
|
|
|
188
212
|
aborted?: boolean;
|
|
189
213
|
truncated?: boolean;
|
|
190
214
|
reason?: string;
|
|
215
|
+
usage?: ChildUsageTotals;
|
|
216
|
+
usageUnavailable?: "session-missing" | "session-invalid" | "usage-missing";
|
|
191
217
|
correlation?: CorrelationMetadata;
|
|
192
218
|
now: Date;
|
|
193
219
|
}): ChildLifecycleEvent {
|
|
@@ -196,6 +222,7 @@ export function buildChildLifecycleEvent(args: {
|
|
|
196
222
|
ledgerVersion: LEDGER_VERSION,
|
|
197
223
|
event: "child_lifecycle",
|
|
198
224
|
ts: args.now.toISOString(),
|
|
225
|
+
...(args.episodeId ? { episodeId: args.episodeId } : {}),
|
|
199
226
|
executionId: args.executionId,
|
|
200
227
|
parentExecutionId: args.parentExecutionId,
|
|
201
228
|
childId: args.childId,
|
|
@@ -211,11 +238,18 @@ export function buildChildLifecycleEvent(args: {
|
|
|
211
238
|
...(args.aborted ? { aborted: true } : {}),
|
|
212
239
|
...(args.truncated ? { truncated: true } : {}),
|
|
213
240
|
...(args.reason ? { reason: args.reason } : {}),
|
|
241
|
+
...(args.usage ? { usage: structuredClone(args.usage) } : {}),
|
|
242
|
+
...(args.usageUnavailable ? { usageUnavailable: args.usageUnavailable } : {}),
|
|
214
243
|
...(args.correlation ? { correlation: structuredClone(args.correlation) } : {}),
|
|
215
244
|
});
|
|
216
245
|
}
|
|
217
246
|
|
|
218
|
-
function assertEventIdentity(args: {
|
|
247
|
+
function assertEventIdentity(args: {
|
|
248
|
+
episodeId?: string;
|
|
249
|
+
executionId: string;
|
|
250
|
+
parentExecutionId: string | null;
|
|
251
|
+
}): void {
|
|
252
|
+
if (args.episodeId !== undefined) assertEpisodeId(args.episodeId);
|
|
219
253
|
assertExecutionId(args.executionId);
|
|
220
254
|
if (args.parentExecutionId !== null) assertExecutionId(args.parentExecutionId, "parentExecutionId");
|
|
221
255
|
if (args.parentExecutionId === args.executionId) throw new TypeError("an execution cannot be its own parent");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normaliseCorrelation, type CorrelationMetadata } from "../kernel/correlation.ts";
|
|
2
2
|
import { isExecutionId } from "../kernel/execution-id.ts";
|
|
3
|
+
import { isEpisodeId } from "../kernel/episode-id.ts";
|
|
3
4
|
import { REFUSAL_CODES } from "../kernel/refusals.ts";
|
|
4
5
|
import { isLedgerCapabilityIdentifier, isLedgerDisplayIdentifier } from "../kernel/ledger-identifiers.ts";
|
|
5
6
|
|
|
@@ -77,6 +78,7 @@ const FIELDS = {
|
|
|
77
78
|
"ledgerVersion",
|
|
78
79
|
"event",
|
|
79
80
|
"ts",
|
|
81
|
+
"episodeId",
|
|
80
82
|
"executionId",
|
|
81
83
|
"parentExecutionId",
|
|
82
84
|
"parentId",
|
|
@@ -113,6 +115,7 @@ const FIELDS = {
|
|
|
113
115
|
"ledgerVersion",
|
|
114
116
|
"event",
|
|
115
117
|
"ts",
|
|
118
|
+
"episodeId",
|
|
116
119
|
"executionId",
|
|
117
120
|
"parentExecutionId",
|
|
118
121
|
"childId",
|
|
@@ -129,6 +132,7 @@ const FIELDS = {
|
|
|
129
132
|
"ledgerVersion",
|
|
130
133
|
"event",
|
|
131
134
|
"ts",
|
|
135
|
+
"episodeId",
|
|
132
136
|
"executionId",
|
|
133
137
|
"parentExecutionId",
|
|
134
138
|
"childId",
|
|
@@ -142,6 +146,8 @@ const FIELDS = {
|
|
|
142
146
|
"reason",
|
|
143
147
|
"deadlineAt",
|
|
144
148
|
"idleTimeoutMs",
|
|
149
|
+
"usage",
|
|
150
|
+
"usageUnavailable",
|
|
145
151
|
"herdrPaneId",
|
|
146
152
|
"herdrAgentName",
|
|
147
153
|
"correlation",
|
|
@@ -159,6 +165,9 @@ export function isNonEmptyString(value: unknown): value is string {
|
|
|
159
165
|
return typeof value === "string" && value.length > 0;
|
|
160
166
|
}
|
|
161
167
|
|
|
168
|
+
const nonNegativeNumber = (value: unknown): value is number =>
|
|
169
|
+
typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
170
|
+
|
|
162
171
|
/** Ledger timestamp profile: JSON Schema `date-time`, narrowed to seconds 00-59 for JS date arithmetic. */
|
|
163
172
|
export function isTimestamp(value: unknown): value is string {
|
|
164
173
|
if (typeof value !== "string") return false;
|
|
@@ -245,6 +254,7 @@ function validApprovalUse(value: unknown): boolean {
|
|
|
245
254
|
|
|
246
255
|
function validateBase(event: LedgerV3Object): string | null {
|
|
247
256
|
if (!isTimestamp(event.ts)) return "ts must be an RFC 3339 timestamp";
|
|
257
|
+
if (!optional(event, "episodeId", isEpisodeId)) return "episodeId is invalid";
|
|
248
258
|
if (!isExecutionId(event.executionId)) return "executionId is missing or invalid";
|
|
249
259
|
if (event.parentExecutionId !== null && !isExecutionId(event.parentExecutionId)) {
|
|
250
260
|
return "parentExecutionId must be an execution id or null";
|
|
@@ -317,6 +327,33 @@ function validateWorkspaceLease(event: LedgerV3Object): string | null {
|
|
|
317
327
|
return null;
|
|
318
328
|
}
|
|
319
329
|
|
|
330
|
+
function validUsage(value: unknown): boolean {
|
|
331
|
+
if (
|
|
332
|
+
!isLedgerObject(value) ||
|
|
333
|
+
Object.keys(value).some(
|
|
334
|
+
(key) => !["input", "output", "cacheRead", "cacheWrite", "reasoning", "totalTokens", "cost"].includes(key),
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
return false;
|
|
338
|
+
const tokenFields = ["input", "output", "cacheRead", "cacheWrite", "totalTokens"];
|
|
339
|
+
if (
|
|
340
|
+
!tokenFields.every(
|
|
341
|
+
(field) => typeof value[field] === "number" && Number.isFinite(value[field]) && (value[field] as number) >= 0,
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
return false;
|
|
345
|
+
if (!optional(value, "reasoning", nonNegativeNumber)) return false;
|
|
346
|
+
const cost = value.cost;
|
|
347
|
+
if (
|
|
348
|
+
!isLedgerObject(cost) ||
|
|
349
|
+
Object.keys(cost).some((key) => !["input", "output", "cacheRead", "cacheWrite", "total"].includes(key))
|
|
350
|
+
)
|
|
351
|
+
return false;
|
|
352
|
+
return ["input", "output", "cacheRead", "cacheWrite", "total"].every(
|
|
353
|
+
(field) => typeof cost[field] === "number" && Number.isFinite(cost[field]) && (cost[field] as number) >= 0,
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
320
357
|
function validateChildLifecycle(event: LedgerV3Object): string | null {
|
|
321
358
|
if (
|
|
322
359
|
!["starting", "running", "completed", "failed"].includes(String(event.state)) ||
|
|
@@ -336,10 +373,17 @@ function validateChildLifecycle(event: LedgerV3Object): string | null {
|
|
|
336
373
|
!optional(event, "aborted", (value) => value === true) ||
|
|
337
374
|
!optional(event, "truncated", (value) => value === true) ||
|
|
338
375
|
!optional(event, "reason", (value) => typeof value === "string") ||
|
|
376
|
+
!optional(event, "usage", validUsage) ||
|
|
377
|
+
!optional(event, "usageUnavailable", (value) =>
|
|
378
|
+
["session-missing", "session-invalid", "usage-missing"].includes(String(value)),
|
|
379
|
+
) ||
|
|
339
380
|
!optional(event, "correlation", validCorrelation)
|
|
340
381
|
) {
|
|
341
382
|
return "child lifecycle optional fields are invalid";
|
|
342
383
|
}
|
|
384
|
+
if (Object.hasOwn(event, "usage") && Object.hasOwn(event, "usageUnavailable")) {
|
|
385
|
+
return "child lifecycle usage and usageUnavailable are mutually exclusive";
|
|
386
|
+
}
|
|
343
387
|
if (
|
|
344
388
|
!optional(event, "herdrPaneId", isLedgerDisplayIdentifier) ||
|
|
345
389
|
!optional(event, "herdrAgentName", isLedgerDisplayIdentifier)
|
package/src/governance/ledger.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type { StructuredRefusal } from "../kernel/refusals.ts";
|
|
|
31
31
|
// Type-only, so the cycle with ./ledger-events.ts is erased at runtime.
|
|
32
32
|
import type { RuntimeLedgerEvent } from "./ledger-events.ts";
|
|
33
33
|
import { assertExecutionId } from "../kernel/execution-id.ts";
|
|
34
|
+
import { assertEpisodeId } from "../kernel/episode-id.ts";
|
|
34
35
|
import { assertLedgerV3Wire } from "./ledger-v3-validation.ts";
|
|
35
36
|
|
|
36
37
|
export const LEDGER_VERSION = 3 as const;
|
|
@@ -44,6 +45,8 @@ export interface LedgerEventBase {
|
|
|
44
45
|
ledgerVersion?: typeof LEDGER_VERSION;
|
|
45
46
|
event?: LedgerEventKind;
|
|
46
47
|
ts: string;
|
|
48
|
+
/** Stable root episode; absent only on records written before episode identity shipped. */
|
|
49
|
+
episodeId?: string;
|
|
47
50
|
/** Unique execution occurrence. Optional only for legacy, unversioned GrantRecord values. */
|
|
48
51
|
executionId?: string;
|
|
49
52
|
/** Explicit execution parent; null means the delegating session is not itself a governed child. */
|
|
@@ -206,6 +209,8 @@ export interface LedgerOptions {
|
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
export function buildRecord(args: {
|
|
212
|
+
/** Stable root episode; optional only for source compatibility with historical callers. */
|
|
213
|
+
episodeId?: string;
|
|
209
214
|
/** Required whenever taskDigest makes this an explicit v3 event. */
|
|
210
215
|
executionId?: string;
|
|
211
216
|
/** Required (including explicit null) whenever taskDigest makes this an explicit v3 event. */
|
|
@@ -239,6 +244,7 @@ export function buildRecord(args: {
|
|
|
239
244
|
refusal?: StructuredRefusal;
|
|
240
245
|
now: Date;
|
|
241
246
|
}): GrantRecord {
|
|
247
|
+
if (args.episodeId !== undefined) assertEpisodeId(args.episodeId);
|
|
242
248
|
if (args.taskDigest !== undefined) {
|
|
243
249
|
if (!/^[a-f0-9]{64}$/i.test(args.taskDigest)) throw new TypeError("taskDigest must be a SHA-256 hex digest");
|
|
244
250
|
assertExecutionId(args.executionId);
|
|
@@ -267,6 +273,7 @@ export function buildRecord(args: {
|
|
|
267
273
|
}
|
|
268
274
|
: {}),
|
|
269
275
|
ts: args.now.toISOString(),
|
|
276
|
+
...(args.episodeId ? { episodeId: args.episodeId } : {}),
|
|
270
277
|
parentId: args.parentId,
|
|
271
278
|
childId: args.childId,
|
|
272
279
|
depth: args.depth,
|
|
@@ -388,6 +395,7 @@ export {
|
|
|
388
395
|
type ChildLifecycleEvent,
|
|
389
396
|
type ChildLifecycleState,
|
|
390
397
|
type ChildProcessSignal,
|
|
398
|
+
type ChildUsageTotals,
|
|
391
399
|
type RuntimeLedgerEvent,
|
|
392
400
|
type WorkspaceAccess,
|
|
393
401
|
type WorkspaceLeaseEvent,
|
|
@@ -59,6 +59,8 @@ export interface DelegationRequest {
|
|
|
59
59
|
|
|
60
60
|
export interface DelegationContext {
|
|
61
61
|
ownGrant: Capability[];
|
|
62
|
+
/** Stable identity shared by the root session and every descendant. */
|
|
63
|
+
episodeId?: string;
|
|
62
64
|
depth: number;
|
|
63
65
|
maxDepth: number;
|
|
64
66
|
gated: Capability[];
|
package/src/kernel/delegate.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
ENV_APPROVED,
|
|
18
18
|
ENV_DEPTH,
|
|
19
19
|
ENV_EXECUTION_ID,
|
|
20
|
+
ENV_EPISODE_ID,
|
|
20
21
|
ENV_FANOUT,
|
|
21
22
|
ENV_GATED,
|
|
22
23
|
ENV_GRANT,
|
|
@@ -389,6 +390,7 @@ export function planDelegation(request: DelegationRequest, ctx: DelegationContex
|
|
|
389
390
|
if (ctx.fanoutBudget !== undefined) env[ENV_FANOUT] = String(ctx.fanoutBudget);
|
|
390
391
|
if (ctx.childSpawnId) env[ENV_PARENT_ID] = ctx.childSpawnId;
|
|
391
392
|
if (ctx.childExecutionId) env[ENV_EXECUTION_ID] = ctx.childExecutionId;
|
|
393
|
+
if (ctx.episodeId) env[ENV_EPISODE_ID] = ctx.episodeId;
|
|
392
394
|
if (ctx.gated.length > 0) env[ENV_GATED] = ctx.gated.join(",");
|
|
393
395
|
// Approvals ride down with the grant, but only ever for what this child actually received — so
|
|
394
396
|
// `approved ⊆ grant` holds at every level (ADR-0010). Written even when empty, so this object states
|
package/src/kernel/env-names.ts
CHANGED
|
@@ -23,6 +23,7 @@ export const ENV_APPROVED = "PI_DADDY_APPROVED";
|
|
|
23
23
|
export const ENV_FANOUT = "PI_DADDY_FANOUT";
|
|
24
24
|
export const ENV_PARENT_ID = "PI_DADDY_PARENT_ID";
|
|
25
25
|
export const ENV_EXECUTION_ID = "PI_DADDY_EXECUTION_ID";
|
|
26
|
+
export const ENV_EPISODE_ID = "PI_DADDY_EPISODE_ID";
|
|
26
27
|
|
|
27
28
|
// Operator preferences a child inherits unchanged. Still governance: they decide executor, deadlines, gates.
|
|
28
29
|
export const ENV_HERDR = "PI_DADDY_HERDR";
|
|
@@ -64,6 +65,8 @@ export const ENV_ADVISOR_KEY = "PI_DADDY_ADVISOR_KEY";
|
|
|
64
65
|
export const ENV_ADVISOR = "PI_DADDY_ADVISOR";
|
|
65
66
|
/** Overrides the adapter's pinned model. In the environment, never the workspace file: a model is a destination. */
|
|
66
67
|
export const ENV_ADVISOR_MODEL = "PI_DADDY_ADVISOR_MODEL";
|
|
68
|
+
/** Exact `raw` opts into sending task text; absent or any other value uses a structural digest. */
|
|
69
|
+
export const ENV_ADVISOR_TASK_EGRESS = "PI_DADDY_ADVISOR_TASK_EGRESS";
|
|
67
70
|
|
|
68
71
|
/** Every variable that shapes governance. The `childEnv` hook may set none of these. */
|
|
69
72
|
export const GOVERNANCE_ENV_KEYS: readonly string[] = Object.freeze([
|
|
@@ -76,6 +79,7 @@ export const GOVERNANCE_ENV_KEYS: readonly string[] = Object.freeze([
|
|
|
76
79
|
ENV_FANOUT,
|
|
77
80
|
ENV_PARENT_ID,
|
|
78
81
|
ENV_EXECUTION_ID,
|
|
82
|
+
ENV_EPISODE_ID,
|
|
79
83
|
ENV_HERDR,
|
|
80
84
|
ENV_HERDR_WORKSPACE,
|
|
81
85
|
ENV_HERDR_KEEP_PANE,
|
|
@@ -93,6 +97,7 @@ export const GOVERNANCE_ENV_KEYS: readonly string[] = Object.freeze([
|
|
|
93
97
|
ENV_ADVISOR_KEY,
|
|
94
98
|
ENV_ADVISOR,
|
|
95
99
|
ENV_ADVISOR_MODEL,
|
|
100
|
+
ENV_ADVISOR_TASK_EGRESS,
|
|
96
101
|
]);
|
|
97
102
|
|
|
98
103
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Stable identity for one root session and every governed descendant it starts. */
|
|
4
|
+
export type EpisodeId = `episode:${string}`;
|
|
5
|
+
|
|
6
|
+
const EPISODE_ID_RE = /^episode:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
7
|
+
|
|
8
|
+
export function isEpisodeId(value: unknown): value is EpisodeId {
|
|
9
|
+
return typeof value === "string" && EPISODE_ID_RE.test(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function newEpisodeId(): EpisodeId {
|
|
13
|
+
return `episode:${randomUUID()}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function assertEpisodeId(value: unknown, field = "episodeId"): asserts value is EpisodeId {
|
|
17
|
+
if (!isEpisodeId(value)) throw new TypeError(`${field} must be a pi-daddy episode id`);
|
|
18
|
+
}
|
|
@@ -33,12 +33,14 @@ import { inheritApprovals, type InheritableApproval } from "./approval.ts";
|
|
|
33
33
|
import { assertCapabilitiesArePropagatable } from "./capabilities.ts";
|
|
34
34
|
import {
|
|
35
35
|
ENV_ADVISOR_MODEL,
|
|
36
|
+
ENV_ADVISOR_TASK_EGRESS,
|
|
36
37
|
ENV_ADVISOR_KEY,
|
|
37
38
|
ENV_ADVISOR,
|
|
38
39
|
ENV_GRANT,
|
|
39
40
|
ENV_FANOUT,
|
|
40
41
|
ENV_PARENT_ID,
|
|
41
42
|
ENV_EXECUTION_ID,
|
|
43
|
+
ENV_EPISODE_ID,
|
|
42
44
|
ENV_DEPTH,
|
|
43
45
|
ENV_MAX_DEPTH,
|
|
44
46
|
ENV_GATED,
|
|
@@ -51,6 +53,7 @@ export {
|
|
|
51
53
|
ENV_FANOUT,
|
|
52
54
|
ENV_PARENT_ID,
|
|
53
55
|
ENV_EXECUTION_ID,
|
|
56
|
+
ENV_EPISODE_ID,
|
|
54
57
|
ENV_DEPTH,
|
|
55
58
|
ENV_MAX_DEPTH,
|
|
56
59
|
ENV_GATED,
|
|
@@ -88,6 +91,7 @@ export const GRANT_ENV_KEYS = [
|
|
|
88
91
|
// child. Named here rather than claimed away: "stripped from every child" would be false on that path.
|
|
89
92
|
ENV_ADVISOR_KEY,
|
|
90
93
|
ENV_ADVISOR,
|
|
94
|
+
ENV_ADVISOR_TASK_EGRESS,
|
|
91
95
|
ENV_GRANT,
|
|
92
96
|
ENV_DEPTH,
|
|
93
97
|
ENV_MAX_DEPTH,
|
|
@@ -97,6 +101,7 @@ export const GRANT_ENV_KEYS = [
|
|
|
97
101
|
ENV_FANOUT,
|
|
98
102
|
ENV_PARENT_ID,
|
|
99
103
|
ENV_EXECUTION_ID,
|
|
104
|
+
ENV_EPISODE_ID,
|
|
100
105
|
// ADR-0042: a child must never keep its parent's unnarrowed pin, so it is stripped like every other
|
|
101
106
|
// governance value and re-supplied only by the spawn plan.
|
|
102
107
|
ENV_WORKSPACE_PIN,
|
|
@@ -262,6 +267,8 @@ export interface ChildEnvInput {
|
|
|
262
267
|
* child then inherits no pin and can route nowhere — which is the fail-closed direction.
|
|
263
268
|
*/
|
|
264
269
|
workspacePin?: WorkspacePins;
|
|
270
|
+
/** Stable identity shared by this root session and every descendant. */
|
|
271
|
+
episodeId?: string;
|
|
265
272
|
/** This session's depth; children are one deeper. */
|
|
266
273
|
depth: number;
|
|
267
274
|
maxDepth: number;
|
|
@@ -342,6 +349,7 @@ export function childEnv(input: ChildEnvInput): Record<string, string> {
|
|
|
342
349
|
env[ENV_APPROVED] = inheritApprovals(input.approved ?? [], inheritable).join(",");
|
|
343
350
|
// Empty is an explicit one-run ledger opt-out and must overwrite a prior publication too.
|
|
344
351
|
if (input.ledgerPath !== undefined) env[ENV_LEDGER] = input.ledgerPath;
|
|
352
|
+
if (input.episodeId !== undefined) env[ENV_EPISODE_ID] = input.episodeId;
|
|
345
353
|
// ADR-0042. ALWAYS written when this session has any pin at all, empty string included, for the same reason
|
|
346
354
|
// `ENV_APPROVED` is: an omitted key does not overwrite, so a child would inherit the PARENT's unnarrowed pin
|
|
347
355
|
// through the process-global publication path. An empty value parses back as "a pin was established and you
|
|
@@ -4,6 +4,8 @@ import { constants } from "node:fs";
|
|
|
4
4
|
import { appendFile, mkdir, open, readFile, realpath, rename, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, join, resolve, sep } from "node:path";
|
|
6
6
|
import { activityTimelinePath } from "../kernel/project-paths.ts";
|
|
7
|
+
import { isEpisodeId, newEpisodeId } from "../kernel/episode-id.ts";
|
|
8
|
+
import { ENV_EPISODE_ID } from "../kernel/env-names.ts";
|
|
7
9
|
import { appendRecord, readRecords } from "../governance/record.ts";
|
|
8
10
|
|
|
9
11
|
export const ACTIVITY_TIMELINE_VERSION = 1 as const;
|
|
@@ -35,6 +37,8 @@ export interface ActivityEvent {
|
|
|
35
37
|
kind: string;
|
|
36
38
|
at: string;
|
|
37
39
|
rootId: string;
|
|
40
|
+
/** Added after v1 shipped; absent on retained events written before episode identity existed. */
|
|
41
|
+
episodeId?: string;
|
|
38
42
|
taskId: string;
|
|
39
43
|
parentTaskId?: string;
|
|
40
44
|
agentId?: string;
|
|
@@ -541,6 +545,7 @@ export class ActivityTimelineRecorder {
|
|
|
541
545
|
readonly contentEnabled: boolean;
|
|
542
546
|
readonly path: string;
|
|
543
547
|
readonly rootId: string;
|
|
548
|
+
readonly episodeId: string;
|
|
544
549
|
private taskId?: string;
|
|
545
550
|
constructor(cwd: string, env: NodeJS.ProcessEnv = process.env) {
|
|
546
551
|
const configured = env[ENV_ACTIVITY_TIMELINE]?.trim();
|
|
@@ -550,6 +555,8 @@ export class ActivityTimelineRecorder {
|
|
|
550
555
|
const identity = activityIdentity(cwd, env);
|
|
551
556
|
this.path = identity.path;
|
|
552
557
|
this.rootId = identity.rootId;
|
|
558
|
+
const inheritedEpisodeId = env[ENV_EPISODE_ID]?.trim();
|
|
559
|
+
this.episodeId = isEpisodeId(inheritedEpisodeId) ? inheritedEpisodeId : newEpisodeId();
|
|
553
560
|
this.taskId = identity.taskId;
|
|
554
561
|
}
|
|
555
562
|
get task(): string | undefined {
|
|
@@ -620,6 +627,7 @@ export class ActivityTimelineRecorder {
|
|
|
620
627
|
kind,
|
|
621
628
|
at: new Date().toISOString(),
|
|
622
629
|
rootId: this.rootId,
|
|
630
|
+
episodeId: this.episodeId,
|
|
623
631
|
taskId: this.taskId,
|
|
624
632
|
...fields,
|
|
625
633
|
};
|