pi-daddy 0.43.1 → 0.44.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 +93 -31
- package/README.md +15 -0
- package/THIRD_PARTY_LICENSES/musl-COPYRIGHT.txt +193 -0
- package/THIRD_PARTY_LICENSES/zig-MIT.txt +21 -0
- package/THIRD_PARTY_NOTICES.md +9 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -5
- package/dist/cli.js.map +1 -1
- package/dist/executors/activity-session.js +116 -65
- package/dist/executors/activity-session.js.map +1 -1
- package/dist/executors/captured-execution.d.ts +13 -0
- package/dist/executors/captured-execution.d.ts.map +1 -0
- package/dist/executors/captured-execution.js +53 -0
- package/dist/executors/captured-execution.js.map +1 -0
- package/dist/executors/child-final.d.ts +31 -0
- package/dist/executors/child-final.d.ts.map +1 -0
- package/dist/executors/child-final.js +197 -0
- package/dist/executors/child-final.js.map +1 -0
- package/dist/executors/executor.d.ts +2 -1
- package/dist/executors/executor.d.ts.map +1 -1
- package/dist/executors/executor.js +12 -0
- package/dist/executors/executor.js.map +1 -1
- package/dist/executors/owned-worker.d.ts +18 -0
- package/dist/executors/owned-worker.d.ts.map +1 -0
- package/dist/executors/owned-worker.js +295 -0
- package/dist/executors/owned-worker.js.map +1 -0
- package/dist/executors/worker-artifact.d.ts +3 -0
- package/dist/executors/worker-artifact.d.ts.map +1 -0
- package/dist/executors/worker-artifact.js +3 -0
- package/dist/executors/worker-artifact.js.map +1 -0
- package/dist/governance/approval-prompt.d.ts +4 -3
- package/dist/governance/approval-prompt.d.ts.map +1 -1
- package/dist/governance/approval-prompt.js +13 -6
- package/dist/governance/approval-prompt.js.map +1 -1
- package/dist/governance/captured-worker-record.d.ts +9 -0
- package/dist/governance/captured-worker-record.d.ts.map +1 -0
- package/dist/governance/captured-worker-record.js +123 -0
- package/dist/governance/captured-worker-record.js.map +1 -0
- package/dist/governance/lease-record.d.ts +4 -0
- package/dist/governance/lease-record.d.ts.map +1 -1
- package/dist/governance/lease-record.js +19 -2
- package/dist/governance/lease-record.js.map +1 -1
- package/dist/governance/ledger-events.d.ts +5 -5
- package/dist/governance/ledger-events.d.ts.map +1 -1
- package/dist/governance/ledger-v3-validation.js +1 -1
- package/dist/governance/ledger-v3-validation.js.map +1 -1
- package/dist/governance/workspace-acceptance.d.ts +3 -1
- package/dist/governance/workspace-acceptance.d.ts.map +1 -1
- package/dist/governance/workspace-acceptance.js +21 -5
- package/dist/governance/workspace-acceptance.js.map +1 -1
- package/dist/governance/workspace-lease.d.ts.map +1 -1
- package/dist/governance/workspace-lease.js +44 -1
- package/dist/governance/workspace-lease.js.map +1 -1
- package/dist/kernel/authority-text.d.ts +3 -0
- package/dist/kernel/authority-text.d.ts.map +1 -0
- package/dist/kernel/authority-text.js +22 -0
- package/dist/kernel/authority-text.js.map +1 -0
- package/dist/kernel/bounded-read-failures.d.ts +9 -0
- package/dist/kernel/bounded-read-failures.d.ts.map +1 -0
- package/dist/kernel/bounded-read-failures.js +33 -0
- package/dist/kernel/bounded-read-failures.js.map +1 -0
- package/dist/kernel/bounded-read-sync.d.ts +24 -0
- package/dist/kernel/bounded-read-sync.d.ts.map +1 -0
- package/dist/kernel/bounded-read-sync.js +75 -0
- package/dist/kernel/bounded-read-sync.js.map +1 -0
- package/dist/kernel/bounded-read.d.ts +29 -2
- package/dist/kernel/bounded-read.d.ts.map +1 -1
- package/dist/kernel/bounded-read.js +98 -9
- package/dist/kernel/bounded-read.js.map +1 -1
- package/dist/kernel/capacity.d.ts +36 -0
- package/dist/kernel/capacity.d.ts.map +1 -0
- package/dist/kernel/capacity.js +94 -0
- package/dist/kernel/capacity.js.map +1 -0
- package/dist/kernel/captured-worker-contract.d.ts +38 -0
- package/dist/kernel/captured-worker-contract.d.ts.map +1 -0
- package/dist/kernel/captured-worker-contract.js +2 -0
- package/dist/kernel/captured-worker-contract.js.map +1 -0
- package/dist/kernel/catalog.d.ts +12 -3
- package/dist/kernel/catalog.d.ts.map +1 -1
- package/dist/kernel/catalog.js +28 -8
- package/dist/kernel/catalog.js.map +1 -1
- package/dist/kernel/chain.d.ts +1 -0
- package/dist/kernel/chain.d.ts.map +1 -1
- package/dist/kernel/chain.js.map +1 -1
- package/dist/kernel/definitions.d.ts +11 -1
- package/dist/kernel/definitions.d.ts.map +1 -1
- package/dist/kernel/definitions.js +6 -3
- package/dist/kernel/definitions.js.map +1 -1
- package/dist/kernel/fanout.d.ts +5 -25
- package/dist/kernel/fanout.d.ts.map +1 -1
- package/dist/kernel/fanout.js +15 -30
- package/dist/kernel/fanout.js.map +1 -1
- package/dist/kernel/grant-env.d.ts +0 -2
- package/dist/kernel/grant-env.d.ts.map +1 -1
- package/dist/kernel/grant-env.js +0 -1
- package/dist/kernel/grant-env.js.map +1 -1
- package/dist/kernel/model-preflight.d.ts +8 -0
- package/dist/kernel/model-preflight.d.ts.map +1 -1
- package/dist/kernel/model-preflight.js +23 -0
- package/dist/kernel/model-preflight.js.map +1 -1
- package/dist/kernel/selected-definitions.d.ts +15 -0
- package/dist/kernel/selected-definitions.d.ts.map +1 -0
- package/dist/kernel/selected-definitions.js +146 -0
- package/dist/kernel/selected-definitions.js.map +1 -0
- package/dist/kernel/skill-packages.d.ts +4 -2
- package/dist/kernel/skill-packages.d.ts.map +1 -1
- package/dist/kernel/skill-packages.js +34 -41
- package/dist/kernel/skill-packages.js.map +1 -1
- package/dist/kernel/skill-resources.d.ts.map +1 -1
- package/dist/kernel/skill-resources.js +8 -11
- package/dist/kernel/skill-resources.js.map +1 -1
- package/dist/kernel/workspace.d.ts +6 -3
- package/dist/kernel/workspace.d.ts.map +1 -1
- package/dist/kernel/workspace.js +10 -6
- package/dist/kernel/workspace.js.map +1 -1
- package/dist/products/dashboard-cli.js +1 -1
- package/dist/products/dashboard-cli.js.map +1 -1
- package/dist/products/dashboard-render.d.ts +1 -2
- package/dist/products/dashboard-render.d.ts.map +1 -1
- package/dist/products/dashboard-render.js +3 -1
- package/dist/products/dashboard-render.js.map +1 -1
- package/dist/products/dashboard-session-client.d.ts +1 -2
- package/dist/products/dashboard-session-client.d.ts.map +1 -1
- package/dist/products/dashboard-session-client.js.map +1 -1
- package/dist/products/episode-report.d.ts +12 -8
- package/dist/products/episode-report.d.ts.map +1 -1
- package/dist/products/episode-report.js +61 -12
- package/dist/products/episode-report.js.map +1 -1
- package/extensions/approvals.ts +1 -1
- package/extensions/chain-plan.ts +9 -3
- package/extensions/context-staging.ts +30 -35
- package/extensions/dashboard-session-server.ts +1 -2
- package/extensions/definition-describe.ts +44 -0
- package/extensions/definition-runtime.ts +89 -26
- package/extensions/delegate-chain.ts +273 -256
- package/extensions/delegation-activation.ts +1 -1
- package/extensions/delegation.ts +41 -53
- package/extensions/execute-child.ts +167 -134
- package/extensions/fanout-outcome.ts +1 -9
- package/extensions/grants-command.ts +2 -9
- package/extensions/grants.ts +106 -38
- package/extensions/init-command.ts +52 -12
- package/extensions/reload-environment.ts +21 -2
- package/extensions/retired-inputs.ts +7 -0
- package/extensions/run-delegation.ts +212 -265
- package/extensions/session-capacity.ts +53 -0
- package/extensions/session-environment.ts +5 -2
- package/extensions/session-model-prompt.ts +33 -32
- package/extensions/session-report.ts +28 -11
- package/extensions/session.ts +200 -157
- package/extensions/workspace-runtime.ts +2 -1
- package/native/README.md +25 -0
- package/native/linux-x64/worker +0 -0
- package/native/linux-x64/worker.sha256 +1 -0
- package/native/provenance.json +42 -0
- package/native/toolchain.json +18 -0
- package/native/worker.c +209 -0
- package/package.json +15 -7
- package/src/cli.ts +16 -6
- package/src/executors/activity-session.ts +120 -66
- package/src/executors/captured-execution.ts +61 -0
- package/src/executors/child-final.ts +190 -0
- package/src/executors/executor.ts +14 -1
- package/src/executors/owned-worker.ts +322 -0
- package/src/executors/worker-artifact.ts +2 -0
- package/src/governance/approval-prompt.ts +18 -8
- package/src/governance/captured-worker-record.ts +138 -0
- package/src/governance/lease-record.ts +21 -2
- package/src/governance/ledger-events.ts +5 -5
- package/src/governance/ledger-v3-validation.ts +1 -1
- package/src/governance/workspace-acceptance.ts +20 -4
- package/src/governance/workspace-lease.ts +59 -1
- package/src/kernel/authority-text.ts +17 -0
- package/src/kernel/bounded-read-failures.ts +42 -0
- package/src/kernel/bounded-read-sync.ts +87 -0
- package/src/kernel/bounded-read.ts +128 -11
- package/src/kernel/capacity.ts +123 -0
- package/src/kernel/captured-worker-contract.ts +29 -0
- package/src/kernel/catalog.ts +45 -16
- package/src/kernel/chain.ts +1 -0
- package/src/kernel/definitions.ts +11 -2
- package/src/kernel/fanout.ts +17 -32
- package/src/kernel/grant-env.ts +0 -3
- package/src/kernel/model-preflight.ts +29 -0
- package/src/kernel/selected-definitions.ts +156 -0
- package/src/kernel/skill-packages.ts +40 -38
- package/src/kernel/skill-resources.ts +8 -9
- package/src/kernel/workspace.ts +12 -5
- package/src/products/dashboard-cli.ts +1 -1
- package/src/products/dashboard-render.ts +6 -2
- package/src/products/dashboard-session-client.ts +1 -2
- package/src/products/episode-report.ts +80 -17
- package/dist/advisors/advisor.d.ts +0 -90
- package/dist/advisors/advisor.d.ts.map +0 -1
- package/dist/advisors/advisor.js +0 -154
- package/dist/advisors/advisor.js.map +0 -1
- package/dist/advisors/decider.d.ts +0 -88
- package/dist/advisors/decider.d.ts.map +0 -1
- package/dist/advisors/decider.js +0 -29
- package/dist/advisors/decider.js.map +0 -1
- package/dist/advisors/jev.d.ts +0 -41
- package/dist/advisors/jev.d.ts.map +0 -1
- package/dist/advisors/jev.js +0 -139
- package/dist/advisors/jev.js.map +0 -1
- package/dist/advisors/settings.d.ts +0 -49
- package/dist/advisors/settings.d.ts.map +0 -1
- package/dist/advisors/settings.js +0 -99
- package/dist/advisors/settings.js.map +0 -1
- package/dist/governance/episode-cost-gate.d.ts +0 -31
- package/dist/governance/episode-cost-gate.d.ts.map +0 -1
- package/dist/governance/episode-cost-gate.js +0 -112
- package/dist/governance/episode-cost-gate.js.map +0 -1
- package/extensions/advisor-session.ts +0 -67
- package/extensions/effort-advice.ts +0 -112
- package/extensions/pruning-advice.ts +0 -133
- package/src/advisors/advisor.ts +0 -246
- package/src/advisors/decider.ts +0 -74
- package/src/advisors/jev.ts +0 -163
- package/src/advisors/settings.ts +0 -115
- package/src/governance/episode-cost-gate.ts +0 -128
|
@@ -33,6 +33,97 @@
|
|
|
33
33
|
import { constants } from "node:fs";
|
|
34
34
|
import { open, type FileHandle } from "node:fs/promises";
|
|
35
35
|
|
|
36
|
+
/** Explicitly trusted per-read I/O ports, never requester/environment input. */
|
|
37
|
+
export interface BoundedReadPorts {
|
|
38
|
+
open(path: string, flags: number): Promise<FileHandle>;
|
|
39
|
+
read(
|
|
40
|
+
handle: FileHandle,
|
|
41
|
+
buffer: Buffer,
|
|
42
|
+
offset: number,
|
|
43
|
+
length: number,
|
|
44
|
+
position: number,
|
|
45
|
+
): Promise<{ bytesRead: number }>;
|
|
46
|
+
close(handle: FileHandle): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
/** A failed read stays failed even after this explicit recovery capability succeeds. */
|
|
49
|
+
export class BoundedReadCleanupError extends Error {
|
|
50
|
+
readonly cleanup: () => Promise<void>;
|
|
51
|
+
/** Latest physical failure, including falsy rejections; cause remains the original failure. */
|
|
52
|
+
lastCause: unknown;
|
|
53
|
+
constructor(cause: unknown, cleanup: () => Promise<void>) {
|
|
54
|
+
super("bounded reader descriptor cleanup unresolved; retain and explicitly retry", { cause });
|
|
55
|
+
this.name = "BoundedReadCleanupError";
|
|
56
|
+
this.lastCause = cause;
|
|
57
|
+
this.cleanup = cleanup;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const retained = new Set<BoundedReadCleanup>();
|
|
62
|
+
/** Trusted diagnostics/recovery only; not admission, a quota, or a Root-wide shutdown join. */
|
|
63
|
+
export function retainedBoundedReadCleanups(): readonly BoundedReadCleanupOwner[] {
|
|
64
|
+
return [...retained];
|
|
65
|
+
}
|
|
66
|
+
export interface BoundedReadCleanupOwner {
|
|
67
|
+
readonly handle: FileHandle | undefined;
|
|
68
|
+
cleanup(): Promise<void>;
|
|
69
|
+
}
|
|
70
|
+
class BoundedReadCleanup implements BoundedReadCleanupOwner {
|
|
71
|
+
private held: FileHandle | undefined;
|
|
72
|
+
private active?: Promise<void>;
|
|
73
|
+
private failure?: BoundedReadCleanupError;
|
|
74
|
+
private finish!: () => void;
|
|
75
|
+
private readonly reading = new Promise<void>((resolve) => (this.finish = resolve));
|
|
76
|
+
private readonly close: BoundedReadPorts["close"];
|
|
77
|
+
constructor(close: BoundedReadPorts["close"]) {
|
|
78
|
+
this.close = close;
|
|
79
|
+
retained.add(this);
|
|
80
|
+
}
|
|
81
|
+
acquired(handle: FileHandle) {
|
|
82
|
+
this.held = handle;
|
|
83
|
+
}
|
|
84
|
+
get handle() {
|
|
85
|
+
return this.held;
|
|
86
|
+
}
|
|
87
|
+
readSettled() {
|
|
88
|
+
this.finish();
|
|
89
|
+
}
|
|
90
|
+
cleanup(): Promise<void> {
|
|
91
|
+
if (this.active) return this.active;
|
|
92
|
+
// Publish before calling any trusted port: reentrant/concurrent cleanup shares this operation.
|
|
93
|
+
const operation = this.reading.then(async () => {
|
|
94
|
+
if (!this.held) {
|
|
95
|
+
retained.delete(this);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await this.close(this.held);
|
|
100
|
+
} catch (cause) {
|
|
101
|
+
this.failure ??= new BoundedReadCleanupError(cause, () => this.cleanup());
|
|
102
|
+
this.failure.lastCause = cause;
|
|
103
|
+
throw this.failure;
|
|
104
|
+
}
|
|
105
|
+
this.held = undefined;
|
|
106
|
+
retained.delete(this);
|
|
107
|
+
});
|
|
108
|
+
this.active = operation;
|
|
109
|
+
void operation.then(
|
|
110
|
+
() => {
|
|
111
|
+
this.active = undefined;
|
|
112
|
+
},
|
|
113
|
+
() => {
|
|
114
|
+
this.active = undefined;
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
return operation;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const boundedReadPorts: BoundedReadPorts = {
|
|
122
|
+
open,
|
|
123
|
+
read: (handle, buffer, offset, length, position) => handle.read(buffer, offset, length, position),
|
|
124
|
+
close: (handle) => handle.close(),
|
|
125
|
+
};
|
|
126
|
+
|
|
36
127
|
export interface BoundedReadLimits {
|
|
37
128
|
/** Refuse anything at or over this, before allocating it. */
|
|
38
129
|
maxBytes: number;
|
|
@@ -50,12 +141,13 @@ export interface BoundedReadLimits {
|
|
|
50
141
|
* one definition that will not be offered — and a shared reader must not decide that for them.
|
|
51
142
|
*/
|
|
52
143
|
export type BoundedReadFailure =
|
|
53
|
-
| { why: "unopenable"; detail: string }
|
|
144
|
+
| { why: "unopenable"; detail: string; code?: string }
|
|
54
145
|
| { why: "not-a-regular-file"; detail: string }
|
|
55
146
|
| { why: "too-large"; detail: string; size: number }
|
|
56
147
|
| { why: "grew-while-reading"; detail: string }
|
|
57
148
|
| { why: "timed-out"; detail: string }
|
|
58
|
-
| { why: "
|
|
149
|
+
| { why: "invalid-utf8"; detail: string }
|
|
150
|
+
| { why: "unreadable"; detail: string; code?: string };
|
|
59
151
|
|
|
60
152
|
export type BoundedReadResult = { ok: true; text: string } | ({ ok: false } & BoundedReadFailure);
|
|
61
153
|
export type BoundedReadBytes = { ok: true; bytes: Buffer } | ({ ok: false } & BoundedReadFailure);
|
|
@@ -67,18 +159,42 @@ export type BoundedReadBytes = { ok: true; bytes: Buffer } | ({ ok: false } & Bo
|
|
|
67
159
|
* UTF-8 round trip unchanged — a latin-1 byte that decodes to U+FFFD changes the file's digest, and that
|
|
68
160
|
* check is only possible against the original buffer. Decoding here and re-encoding there would defeat it.
|
|
69
161
|
*/
|
|
70
|
-
export async function readBoundedFile(
|
|
71
|
-
|
|
72
|
-
|
|
162
|
+
export async function readBoundedFile(
|
|
163
|
+
path: string,
|
|
164
|
+
limits: BoundedReadLimits,
|
|
165
|
+
ports: BoundedReadPorts = boundedReadPorts,
|
|
166
|
+
): Promise<BoundedReadResult> {
|
|
167
|
+
const read = await readBoundedBytes(path, limits, ports);
|
|
168
|
+
if (!read.ok) return read;
|
|
169
|
+
try {
|
|
170
|
+
return { ok: true, text: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(read.bytes) };
|
|
171
|
+
} catch {
|
|
172
|
+
return { ok: false, why: "invalid-utf8", detail: `${path} is not valid UTF-8` };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Preserve typed I/O evidence; a caller must not classify absence by searching diagnostic prose. */
|
|
177
|
+
function ioCode(error: unknown): { code?: string } {
|
|
178
|
+
if (error && typeof error === "object" && "code" in error && typeof error.code === "string")
|
|
179
|
+
return { code: error.code };
|
|
180
|
+
return {};
|
|
73
181
|
}
|
|
74
182
|
|
|
75
|
-
export async function readBoundedBytes(
|
|
183
|
+
export async function readBoundedBytes(
|
|
184
|
+
path: string,
|
|
185
|
+
limits: BoundedReadLimits,
|
|
186
|
+
ports: BoundedReadPorts = boundedReadPorts,
|
|
187
|
+
): Promise<BoundedReadBytes> {
|
|
76
188
|
const now = limits.now ?? Date.now;
|
|
77
189
|
let handle: FileHandle;
|
|
190
|
+
const cleanup = new BoundedReadCleanup((held) => ports.close(held));
|
|
78
191
|
try {
|
|
79
|
-
handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
192
|
+
handle = await ports.open(path, constants.O_RDONLY | constants.O_NONBLOCK);
|
|
193
|
+
cleanup.acquired(handle);
|
|
80
194
|
} catch (error) {
|
|
81
|
-
|
|
195
|
+
cleanup.readSettled();
|
|
196
|
+
await cleanup.cleanup();
|
|
197
|
+
return { ok: false, why: "unopenable", detail: String(error), ...ioCode(error) };
|
|
82
198
|
}
|
|
83
199
|
try {
|
|
84
200
|
const info = await handle.stat();
|
|
@@ -106,7 +222,7 @@ export async function readBoundedBytes(path: string, limits: BoundedReadLimits):
|
|
|
106
222
|
why: "timed-out",
|
|
107
223
|
detail: `${path} did not finish reading within ${limits.timeoutMs}ms`,
|
|
108
224
|
};
|
|
109
|
-
const { bytesRead } = await
|
|
225
|
+
const { bytesRead } = await ports.read(handle, buffer, filled, buffer.length - filled, filled);
|
|
110
226
|
if (bytesRead === 0) break;
|
|
111
227
|
filled += bytesRead;
|
|
112
228
|
}
|
|
@@ -118,8 +234,9 @@ export async function readBoundedBytes(path: string, limits: BoundedReadLimits):
|
|
|
118
234
|
};
|
|
119
235
|
return { ok: true, bytes: buffer.subarray(0, filled) };
|
|
120
236
|
} catch (error) {
|
|
121
|
-
return { ok: false, why: "unreadable", detail: String(error) };
|
|
237
|
+
return { ok: false, why: "unreadable", detail: String(error), ...ioCode(error) };
|
|
122
238
|
} finally {
|
|
123
|
-
|
|
239
|
+
cleanup.readSettled();
|
|
240
|
+
await cleanup.cleanup();
|
|
124
241
|
}
|
|
125
242
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-local active descendant capacity. A reservation holds one child plus a disjoint subtree allowance.
|
|
3
|
+
* The allowance excludes this owner session. Conservatively hold all of it until the owned subtree settles;
|
|
4
|
+
* this permits reuse after settlement, but does not promise perfect utilization or a lifetime call limit.
|
|
5
|
+
*/
|
|
6
|
+
import type { CapturedWorkerCleanup, CapturedWorkerIdentity } from "./captured-worker-contract.ts";
|
|
7
|
+
|
|
8
|
+
export type CapacityReservationState = "reserved" | "retained" | "released";
|
|
9
|
+
export interface CapacityReservation {
|
|
10
|
+
readonly executionId: string;
|
|
11
|
+
readonly childAllowance: number;
|
|
12
|
+
readonly cost: number;
|
|
13
|
+
readonly state: CapacityReservationState;
|
|
14
|
+
readonly identity: CapturedWorkerIdentity | undefined;
|
|
15
|
+
/** Bind actual ownership before releasing the worker gate. Throws on replacement or a different execution. */
|
|
16
|
+
bindOwnership(identity: CapturedWorkerIdentity): void;
|
|
17
|
+
/** Only trusted executor facts belong here; model output and ledger projections never authorize release. */
|
|
18
|
+
finalize(cleanup: CapturedWorkerCleanup): CapacityReservationState;
|
|
19
|
+
}
|
|
20
|
+
export type CapacityReservationResult = { ok: true; reservation: CapacityReservation } | { ok: false; reason: string };
|
|
21
|
+
export interface CapacityAllocator {
|
|
22
|
+
readonly total: number;
|
|
23
|
+
readonly available: number;
|
|
24
|
+
readonly reserved: number;
|
|
25
|
+
/** Still-held occurrences; released reservations are removed, execution IDs remain tombstoned. */
|
|
26
|
+
readonly retainedReservations: readonly CapacityReservation[];
|
|
27
|
+
reserve(executionId: string, childAllowance: number): CapacityReservationResult;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const identityKeys = [
|
|
31
|
+
"revision",
|
|
32
|
+
"executionId",
|
|
33
|
+
"nonce",
|
|
34
|
+
"root",
|
|
35
|
+
"rootDevice",
|
|
36
|
+
"rootInode",
|
|
37
|
+
"bootId",
|
|
38
|
+
"pidNamespace",
|
|
39
|
+
"helperPid",
|
|
40
|
+
"helperStartTicks",
|
|
41
|
+
"helperSha256",
|
|
42
|
+
"workerPid",
|
|
43
|
+
"ownershipPath",
|
|
44
|
+
"receiptPath",
|
|
45
|
+
] as const;
|
|
46
|
+
function sameIdentity(a: CapturedWorkerIdentity, b: CapturedWorkerIdentity): boolean {
|
|
47
|
+
return identityKeys.every((key) => a[key] === b[key]);
|
|
48
|
+
}
|
|
49
|
+
const validAllowance = (value: number): boolean => Number.isSafeInteger(value) && value >= 0;
|
|
50
|
+
|
|
51
|
+
/** Synchronous reservations are atomic within the actual session owner, including overlapping tool calls. */
|
|
52
|
+
export function createCapacityAllocator(total: number): CapacityAllocator {
|
|
53
|
+
if (!validAllowance(total)) throw new Error("descendant capacity must be a non-negative safe integer");
|
|
54
|
+
let available = total;
|
|
55
|
+
// Keep occurrence tombstones: a stale finalizer must never release a newly reused execution identifier.
|
|
56
|
+
const executions = new Set<string>();
|
|
57
|
+
const held = new Map<string, CapacityReservation>();
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
total,
|
|
60
|
+
get available() {
|
|
61
|
+
return available;
|
|
62
|
+
},
|
|
63
|
+
get reserved() {
|
|
64
|
+
return total - available;
|
|
65
|
+
},
|
|
66
|
+
get retainedReservations() {
|
|
67
|
+
return [...held.values()].filter((reservation) => reservation.state === "retained");
|
|
68
|
+
},
|
|
69
|
+
reserve(executionId: string, childAllowance: number): CapacityReservationResult {
|
|
70
|
+
if (!executionId || executionId.length > 512 || executions.has(executionId))
|
|
71
|
+
return { ok: false, reason: "capacity reservation requires a fresh bounded execution identifier" };
|
|
72
|
+
if (!validAllowance(childAllowance) || !Number.isSafeInteger(childAllowance + 1))
|
|
73
|
+
return { ok: false, reason: "child allowance must be a non-negative safe integer below the numeric ceiling" };
|
|
74
|
+
const cost = childAllowance + 1;
|
|
75
|
+
if (cost > available)
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
reason: `active descendant capacity exhausted: ${cost} reserved units requested, ${available} available`,
|
|
79
|
+
};
|
|
80
|
+
available -= cost;
|
|
81
|
+
executions.add(executionId);
|
|
82
|
+
let state: CapacityReservationState = "reserved";
|
|
83
|
+
let identity: CapturedWorkerIdentity | undefined;
|
|
84
|
+
const reservation: CapacityReservation = Object.freeze({
|
|
85
|
+
executionId,
|
|
86
|
+
childAllowance,
|
|
87
|
+
cost,
|
|
88
|
+
get state() {
|
|
89
|
+
return state;
|
|
90
|
+
},
|
|
91
|
+
get identity() {
|
|
92
|
+
return identity;
|
|
93
|
+
},
|
|
94
|
+
bindOwnership(actual: CapturedWorkerIdentity): void {
|
|
95
|
+
if (
|
|
96
|
+
state === "released" ||
|
|
97
|
+
actual.executionId !== executionId ||
|
|
98
|
+
(identity && !sameIdentity(identity, actual))
|
|
99
|
+
)
|
|
100
|
+
throw new Error("capacity ownership does not match its reserved execution");
|
|
101
|
+
identity = Object.freeze({ ...actual });
|
|
102
|
+
},
|
|
103
|
+
finalize(cleanup: CapturedWorkerCleanup): CapacityReservationState {
|
|
104
|
+
if (state === "released") return state;
|
|
105
|
+
const provenNotStarted = cleanup.state === "not-started" && !identity && state === "reserved";
|
|
106
|
+
const settled =
|
|
107
|
+
cleanup.state === "settled" &&
|
|
108
|
+
identity !== undefined &&
|
|
109
|
+
sameIdentity(identity, cleanup.identity) &&
|
|
110
|
+
sameIdentity(identity, cleanup.receipt.identity) &&
|
|
111
|
+
cleanup.receipt.state === "settled" &&
|
|
112
|
+
cleanup.receipt.reapedAll === true;
|
|
113
|
+
if (!provenNotStarted && !settled) return (state = "retained");
|
|
114
|
+
held.delete(executionId);
|
|
115
|
+
available += cost;
|
|
116
|
+
return (state = "released");
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
held.set(executionId, reservation);
|
|
120
|
+
return { ok: true, reservation };
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Versioned, execution-bound worker facts. These records never grant capabilities. */
|
|
2
|
+
export interface CapturedWorkerIdentity {
|
|
3
|
+
revision: 1;
|
|
4
|
+
executionId: string;
|
|
5
|
+
nonce: string;
|
|
6
|
+
root: string;
|
|
7
|
+
rootDevice: string;
|
|
8
|
+
rootInode: string;
|
|
9
|
+
bootId: string;
|
|
10
|
+
pidNamespace: string;
|
|
11
|
+
helperPid: number;
|
|
12
|
+
helperStartTicks: string;
|
|
13
|
+
helperSha256: string;
|
|
14
|
+
workerPid: number;
|
|
15
|
+
ownershipPath: string;
|
|
16
|
+
receiptPath: string;
|
|
17
|
+
}
|
|
18
|
+
export interface CapturedWorkerReceipt {
|
|
19
|
+
state: "settled";
|
|
20
|
+
identity: CapturedWorkerIdentity;
|
|
21
|
+
workerCode: number | null;
|
|
22
|
+
workerSignal: number;
|
|
23
|
+
reason: "worker-exit" | "owner-loss" | "cancelled" | "helper-signal" | "ownership-write-failed" | "start-failed";
|
|
24
|
+
reapedAll: true;
|
|
25
|
+
}
|
|
26
|
+
export type CapturedWorkerCleanup =
|
|
27
|
+
| { state: "settled"; identity: CapturedWorkerIdentity; receipt: CapturedWorkerReceipt }
|
|
28
|
+
| { state: "unknown"; identity?: CapturedWorkerIdentity; reason: string }
|
|
29
|
+
| { state: "not-started"; reason: string };
|
package/src/kernel/catalog.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { BoundedReadCleanupError } from "./bounded-read.ts";
|
|
23
|
+
import { collectBoundedReadFailures } from "./bounded-read-failures.ts";
|
|
22
24
|
import { resolveSkillResources, skillResourceName } from "./skill-resources.ts";
|
|
23
25
|
import { join } from "node:path";
|
|
24
26
|
import { loadDefinitions, type SkillDefinition } from "./definitions.ts";
|
|
@@ -133,16 +135,26 @@ export function makeCatalog(entries: CatalogEntry[], registryRefusal?: string):
|
|
|
133
135
|
};
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/** Trusted loader dependencies, not catalog/request configuration. */
|
|
139
|
+
export interface CatalogLoaders {
|
|
140
|
+
definitions?: typeof loadDefinitions;
|
|
141
|
+
registry?: typeof loadWorkspaceRegistry;
|
|
142
|
+
}
|
|
143
|
+
|
|
136
144
|
/** Build the live catalog. `observedTools` comes from a provider payload; null when not yet seen. */
|
|
137
|
-
export async function buildCatalog(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
export async function buildCatalog(
|
|
146
|
+
input: {
|
|
147
|
+
cwd: string;
|
|
148
|
+
snapshot?: { skills: CatalogEntry[]; definitions: Map<string, SkillDefinition> };
|
|
149
|
+
observedTools: string[] | null;
|
|
150
|
+
/** Operator workspace registry (`PI_DADDY_WORKSPACE_REGISTRY`). Absent or unreadable yields no entries. */
|
|
151
|
+
registryPath?: string;
|
|
152
|
+
},
|
|
153
|
+
loaders: CatalogLoaders = {},
|
|
154
|
+
): Promise<Catalog> {
|
|
155
|
+
const outcomes = await Promise.allSettled([
|
|
156
|
+
Promise.resolve().then(() => input.snapshot?.skills ?? loadSkills(input.cwd)),
|
|
157
|
+
Promise.resolve().then(() => input.snapshot?.definitions ?? (loaders.definitions ?? loadDefinitions)(input.cwd)),
|
|
146
158
|
// Fails SOFT, and only because nothing here is an authority. A malformed registry must not stop a
|
|
147
159
|
// session from starting — `loadWorkspaceRegistry` throws a GovernanceRefusal naming the file, and that
|
|
148
160
|
// refusal is the operator's signal at the point of USE, where routing actually depends on it. Swallowing
|
|
@@ -151,15 +163,32 @@ export async function buildCatalog(input: {
|
|
|
151
163
|
// **What the refusal is no longer allowed to do is vanish.** This handler was `() => []`, so the display
|
|
152
164
|
// list was lost AND the reason with it. The reason now rides on the catalog and `/grants` prints it.
|
|
153
165
|
input.registryPath
|
|
154
|
-
?
|
|
155
|
-
(
|
|
156
|
-
(
|
|
157
|
-
entries:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
? Promise.resolve()
|
|
167
|
+
.then(() => (loaders.registry ?? loadWorkspaceRegistry)(input.registryPath!))
|
|
168
|
+
.then(
|
|
169
|
+
(r) => ({ entries: workspaceEntries(r, input.registryPath), refusal: undefined as string | undefined }),
|
|
170
|
+
(error: unknown) => {
|
|
171
|
+
if (error instanceof BoundedReadCleanupError) throw error;
|
|
172
|
+
return {
|
|
173
|
+
entries: [] as CatalogEntry[],
|
|
174
|
+
refusal: error instanceof Error ? error.message : String(error),
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
)
|
|
161
178
|
: Promise.resolve({ entries: [] as CatalogEntry[], refusal: undefined as string | undefined }),
|
|
162
179
|
]);
|
|
180
|
+
// Join EVERY started branch before ordinary discovery fallback can discard a physical close failure.
|
|
181
|
+
const cleanup = collectBoundedReadFailures(
|
|
182
|
+
outcomes.flatMap((result) => (result.status === "rejected" ? [result.reason] : [])),
|
|
183
|
+
);
|
|
184
|
+
if (cleanup) throw cleanup;
|
|
185
|
+
const value = <T>(result: PromiseSettledResult<T>): T => {
|
|
186
|
+
if (result.status === "rejected") throw result.reason;
|
|
187
|
+
return result.value;
|
|
188
|
+
};
|
|
189
|
+
const skills = value(outcomes[0]);
|
|
190
|
+
const definitions = value(outcomes[1]);
|
|
191
|
+
const workspaces = value(outcomes[2]);
|
|
163
192
|
return makeCatalog(
|
|
164
193
|
[
|
|
165
194
|
// pi's built-ins are seeded unconditionally, because they are known statically and the catalog is
|
package/src/kernel/chain.ts
CHANGED
|
@@ -152,6 +152,7 @@ export function composeStepTask(template: string, previous: string | undefined):
|
|
|
152
152
|
|
|
153
153
|
/** One chain step as `runOneDelegation` takes it: the operator's spec with its task composed. */
|
|
154
154
|
export interface ChainStep {
|
|
155
|
+
definitionId?: string;
|
|
155
156
|
task: string;
|
|
156
157
|
agent?: string;
|
|
157
158
|
tools?: string[];
|
|
@@ -61,6 +61,10 @@ export function isDefinitionPackageVersion(value: unknown): value is string {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export interface SkillDefinition {
|
|
64
|
+
/** Immutable selected-resource identity, independent of the model-authored name. */
|
|
65
|
+
definitionId?: string;
|
|
66
|
+
selectedSkillHash?: string;
|
|
67
|
+
binding?: Readonly<{ package: "principal-pi-skills"; phase: string }>;
|
|
64
68
|
/** From the path, never the frontmatter — see `parseSkillDefinition`. */
|
|
65
69
|
name: string;
|
|
66
70
|
description: string;
|
|
@@ -68,6 +72,8 @@ export interface SkillDefinition {
|
|
|
68
72
|
allowedTools?: string;
|
|
69
73
|
/** The spec's sanctioned extension point: a map of string keys to string values. */
|
|
70
74
|
metadata?: Record<string, string>;
|
|
75
|
+
/** Raw authored JSON pairs; validated only when runtime selection needs them. */
|
|
76
|
+
runtimePreferences?: string;
|
|
71
77
|
/** Everything after the frontmatter — the child's system prompt. */
|
|
72
78
|
body: string;
|
|
73
79
|
source: string;
|
|
@@ -160,6 +166,7 @@ export function parseSkillDefinition(source: string, text: string): SkillDefinit
|
|
|
160
166
|
name: skillResourceName(source),
|
|
161
167
|
description,
|
|
162
168
|
allowedTools: fields.get("allowed-tools"),
|
|
169
|
+
runtimePreferences: fields.get("runtime-preferences"),
|
|
163
170
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
164
171
|
body: text.slice(match[0].length).trim(),
|
|
165
172
|
source,
|
|
@@ -232,11 +239,13 @@ export const DEFINITION_READ_TIMEOUT_MS = 2_000;
|
|
|
232
239
|
export async function loadDefinitions(
|
|
233
240
|
cwd: string,
|
|
234
241
|
skipped?: (path: string, reason: string) => void,
|
|
242
|
+
// Trusted per-loader reader, never a skill's metadata or environment input.
|
|
243
|
+
readFile: typeof readBoundedFile = readBoundedFile,
|
|
235
244
|
): Promise<Map<string, SkillDefinition>> {
|
|
236
245
|
const definitions = new Map<string, SkillDefinition>();
|
|
237
246
|
const packageVersions = new Map<string, string | undefined>();
|
|
238
247
|
for (const { path, metadata } of (await resolveSkillResources(cwd)).skills) {
|
|
239
|
-
const read = await
|
|
248
|
+
const read = await readFile(path, {
|
|
240
249
|
maxBytes: DEFINITION_MAX_BYTES,
|
|
241
250
|
timeoutMs: DEFINITION_READ_TIMEOUT_MS,
|
|
242
251
|
});
|
|
@@ -247,7 +256,7 @@ export async function loadDefinitions(
|
|
|
247
256
|
const parsed = parseSkillDefinition(path, read.text);
|
|
248
257
|
if (parsed && metadata.origin === "package" && metadata.baseDir) {
|
|
249
258
|
if (!packageVersions.has(metadata.baseDir)) {
|
|
250
|
-
const manifest = await
|
|
259
|
+
const manifest = await readFile(join(metadata.baseDir, "package.json"), {
|
|
251
260
|
maxBytes: DEFINITION_MAX_BYTES,
|
|
252
261
|
timeoutMs: DEFINITION_READ_TIMEOUT_MS,
|
|
253
262
|
});
|
package/src/kernel/fanout.ts
CHANGED
|
@@ -1,35 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bounded synchronous fan-out — ADR-0015's option A′, and the cardinality bound ADR-0008 never had.
|
|
3
|
-
*
|
|
4
|
-
* **The gap this closes.** ADR-0008 bounds what each child may *hold* and says nothing about how many
|
|
5
|
-
* children exist. `resolve()` is a set operation; nowhere in `src/` was there a count. Blocking `delegate`
|
|
6
|
-
* bounded cardinality to one *by accident of being blocking*, so the invariant was never tested. Fan-out
|
|
7
|
-
* removes that accident: with `maxDepth: 2` and five children per call, five delegators each spawning five
|
|
8
|
-
* is thirty concurrent model sessions — **every ledger line individually correct and narrow, the aggregate
|
|
9
|
-
* a machine DoS and an uncapped bill.** The critic's verdict on this was that silence is itself a finding.
|
|
10
|
-
*
|
|
11
|
-
* **Why a budget rather than a per-call limit.** A per-call cap of K with depth D still permits K^D
|
|
12
|
-
* descendants, which is the same exponential wearing a smaller number. A *budget* is subtractive and
|
|
13
|
-
* therefore total: a session holding budget B may create at most B descendants in its whole subtree,
|
|
14
|
-
* because it spends from B to spawn and hands each child a share of what is left. That composes across
|
|
15
|
-
* process boundaries with **no shared state** — the same property that makes depth work — so it needs no
|
|
16
|
-
* registry, no lock and no counter file.
|
|
17
|
-
*
|
|
18
|
-
* It is deliberately NOT a concurrency limit. How many run at once is a resource question for the executor;
|
|
19
|
-
* how many may exist at all is a governance question, and this is the governance answer.
|
|
20
|
-
*/
|
|
21
|
-
|
|
1
|
+
/** Active descendant allocation arithmetic. Enforcement requires the shared owner allocator in capacity.ts. */
|
|
22
2
|
import { parseBound } from "./propagation.ts";
|
|
3
|
+
import { GovernanceRefusal, refusal } from "./refusals.ts";
|
|
23
4
|
|
|
24
|
-
/**
|
|
5
|
+
/** Active descendant allowance, excluding the owner session, when nothing is configured. */
|
|
25
6
|
export const DEFAULT_FANOUT_BUDGET = 8;
|
|
26
7
|
|
|
27
8
|
/**
|
|
28
9
|
* Hard ceiling on children in a single call, independent of budget.
|
|
29
10
|
*
|
|
30
11
|
* A budget alone would let one call spend all of it at once, and a hundred simultaneous `pi` processes is
|
|
31
|
-
* a different failure from a hundred spread over a session.
|
|
32
|
-
*
|
|
12
|
+
* a different failure from a hundred spread over a session. The owner allocator also conserves active
|
|
13
|
+
* capacity across overlapping calls.
|
|
33
14
|
*/
|
|
34
15
|
export const MAX_CHILDREN_PER_CALL = 8;
|
|
35
16
|
|
|
@@ -43,15 +24,16 @@ export const MAX_CHILDREN_PER_CALL = 8;
|
|
|
43
24
|
*/
|
|
44
25
|
export const MAX_CHAIN_STEPS = MAX_CHILDREN_PER_CALL;
|
|
45
26
|
|
|
46
|
-
/**
|
|
27
|
+
/** Absent uses the existing default; explicit zero stays exhausted and malformed input refuses. */
|
|
47
28
|
export function budgetFromEnv(raw: string | undefined): number {
|
|
29
|
+
if (raw === undefined) return DEFAULT_FANOUT_BUDGET;
|
|
48
30
|
const parsed = parseBound(raw);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
31
|
+
if (parsed === null || parsed === undefined)
|
|
32
|
+
throw new GovernanceRefusal(
|
|
33
|
+
refusal("FANOUT_EXCEEDED", "PI_DADDY_FANOUT must be a non-negative safe decimal integer"),
|
|
34
|
+
);
|
|
35
|
+
return parsed;
|
|
53
36
|
}
|
|
54
|
-
|
|
55
37
|
export interface BudgetSplit {
|
|
56
38
|
ok: boolean;
|
|
57
39
|
reason?: string;
|
|
@@ -60,7 +42,7 @@ export interface BudgetSplit {
|
|
|
60
42
|
}
|
|
61
43
|
|
|
62
44
|
/**
|
|
63
|
-
*
|
|
45
|
+
* Propose disjoint child allowances from currently available capacity; this does not reserve anything.
|
|
64
46
|
*
|
|
65
47
|
* The parent pays one unit per child it creates *before* sharing what is left, so a subtree can never
|
|
66
48
|
* exceed the budget it started with: spawning is itself an expenditure, not a free act that only its
|
|
@@ -68,7 +50,10 @@ export interface BudgetSplit {
|
|
|
68
50
|
* inventing it — the safe direction, and the reason a deep tree converges to zero instead of oscillating.
|
|
69
51
|
*/
|
|
70
52
|
export function splitBudget(budget: number, count: number): BudgetSplit {
|
|
71
|
-
if (
|
|
53
|
+
if (!Number.isSafeInteger(budget) || budget < 0)
|
|
54
|
+
return { ok: false, reason: "capacity must be a non-negative safe integer", perChild: 0 };
|
|
55
|
+
if (!Number.isSafeInteger(count) || count <= 0)
|
|
56
|
+
return { ok: false, reason: "a fan-out needs at least one child", perChild: 0 };
|
|
72
57
|
if (count > MAX_CHILDREN_PER_CALL) {
|
|
73
58
|
return {
|
|
74
59
|
ok: false,
|
package/src/kernel/grant-env.ts
CHANGED
|
@@ -140,8 +140,6 @@ export interface ProjectSettings {
|
|
|
140
140
|
ledger: string;
|
|
141
141
|
/** Gated by default (ADR-0012): closed under subsumption, so gating tool:write also gates tool:bash. */
|
|
142
142
|
gatedByDefault: Capability[];
|
|
143
|
-
/** Cumulative provider-reported USD cost at which the episode asks whether to continue. */
|
|
144
|
-
episodeCostCeiling: number;
|
|
145
143
|
}
|
|
146
144
|
|
|
147
145
|
export function buildProjectSettings(input: GrantEnvInput): ProjectSettings {
|
|
@@ -168,7 +166,6 @@ export function buildProjectSettings(input: GrantEnvInput): ProjectSettings {
|
|
|
168
166
|
cautions: [...input.cautions],
|
|
169
167
|
ledger: PROJECT_FILES.ledger,
|
|
170
168
|
gatedByDefault: ["tool:bash"],
|
|
171
|
-
episodeCostCeiling: 5,
|
|
172
169
|
};
|
|
173
170
|
}
|
|
174
171
|
|
|
@@ -16,6 +16,7 @@ export function supportedModelEfforts(model: {
|
|
|
16
16
|
|
|
17
17
|
export interface ModelCatalogue {
|
|
18
18
|
find(provider: string, modelId: string): unknown;
|
|
19
|
+
getProviderAuthStatus?(provider: string): { configured: boolean };
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/** Resolve an explicit provider/id against pi's own session catalogue without probing credentials or network. */
|
|
@@ -42,3 +43,31 @@ export function preflightModel(
|
|
|
42
43
|
{ model: model || "(empty)" },
|
|
43
44
|
);
|
|
44
45
|
}
|
|
46
|
+
|
|
47
|
+
/** Exact candidate only; never enumerate providers, resolve credentials, probe, or clamp effort. */
|
|
48
|
+
export function runtimePairUnavailable(
|
|
49
|
+
choice: { model?: string; thinking?: string },
|
|
50
|
+
catalogue: ModelCatalogue,
|
|
51
|
+
allowUnresolved = false,
|
|
52
|
+
): string | undefined {
|
|
53
|
+
if (!choice.model) return undefined; // No explicit/current choice: preserve Pi's ordinary configured default.
|
|
54
|
+
const slash = choice.model.indexOf("/");
|
|
55
|
+
const provider = choice.model.slice(0, slash),
|
|
56
|
+
id = choice.model.slice(slash + 1);
|
|
57
|
+
if (slash <= 0 || !id) return "runtime model needs provider/model";
|
|
58
|
+
const model = catalogue.find(provider, id);
|
|
59
|
+
if (!model) return allowUnresolved ? undefined : `model ${choice.model} is not in pi's session catalogue`;
|
|
60
|
+
if (
|
|
61
|
+
choice.thinking !== undefined &&
|
|
62
|
+
typeof model === "object" &&
|
|
63
|
+
model !== null &&
|
|
64
|
+
"reasoning" in model &&
|
|
65
|
+
!supportedModelEfforts(model as { reasoning: boolean }).includes(choice.thinking as never)
|
|
66
|
+
) {
|
|
67
|
+
return `model ${choice.model} does not support thinking ${choice.thinking}`;
|
|
68
|
+
}
|
|
69
|
+
// configured:false cannot distinguish keyless, missing and unknown public provider auth semantics.
|
|
70
|
+
// Reading the passive status is permitted evidence; never turn it into an authentication attempt.
|
|
71
|
+
catalogue.getProviderAuthStatus?.(provider);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|