@akagilnc/pi-workflow-roles 0.1.5204 → 0.1.5212
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/acp-host/production-host.js +133 -289
- package/dist/headless-host/production-host.js +109 -265
- package/dist/public-cli/main.js +0 -1
- package/dist/sitian-appender.js +0 -1
- package/dist/submission-ledger.js +8 -29
- package/package.json +2 -3
- package/scripts/build-package.mjs +1 -3
- package/souls/audit-law.md +1 -1
- package/souls/judge-auditor.md +1 -1
- package/src/host-contracts.ts +0 -100
- package/src/role-runtime.ts +4 -57
- package/src/sitian-appender.ts +0 -1
- package/src/sitian-contracts.ts +0 -3
- package/src/submission-ledger.ts +10 -31
- package/dist/pi/in-process-session.js +0 -660
- package/dist/stream-idle-guard.js +0 -67
- package/schemas/tool-execution-observation.schema.json +0 -107
- package/scripts/generate-tool-execution-observation-schema.ts +0 -7
- package/src/in-process-session.ts +0 -1
- package/src/pi/in-process-session.ts +0 -861
- package/src/stream-idle-guard.ts +0 -98
- package/src/tool-execution-observation.ts +0 -168
|
@@ -1,861 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pi Institutional Sub-Session Open Seam (#518 / #233).
|
|
3
|
-
* Adapts Pi AgentSession / ModelRuntime / ModelRegistry into the host-neutral
|
|
4
|
-
* HostInstitutionalSessionHandle contract.
|
|
5
|
-
* Zero Pi type leakage out of this module.
|
|
6
|
-
*/
|
|
7
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
createAgentSession,
|
|
13
|
-
DefaultResourceLoader,
|
|
14
|
-
ModelRegistry,
|
|
15
|
-
ModelRuntime,
|
|
16
|
-
SettingsManager,
|
|
17
|
-
type AgentToolResult,
|
|
18
|
-
type SessionManager,
|
|
19
|
-
type ToolDefinition,
|
|
20
|
-
} from "@earendil-works/pi-coding-agent";
|
|
21
|
-
import {
|
|
22
|
-
createAssistantMessageEventStream,
|
|
23
|
-
InMemoryCredentialStore,
|
|
24
|
-
type Api,
|
|
25
|
-
type AssistantMessage,
|
|
26
|
-
type Model,
|
|
27
|
-
type Provider,
|
|
28
|
-
type ProviderStreamOptions,
|
|
29
|
-
type TranscriptContext,
|
|
30
|
-
type Usage,
|
|
31
|
-
} from "@earendil-works/pi-ai";
|
|
32
|
-
|
|
33
|
-
import { createRecordSession } from "../archivist-record-entry.ts";
|
|
34
|
-
import type {
|
|
35
|
-
HostAssistantTurnResult,
|
|
36
|
-
HostInstitutionalSessionEvent,
|
|
37
|
-
HostInstitutionalSessionHandle,
|
|
38
|
-
HostInstitutionalSessionOptions,
|
|
39
|
-
HostSessionUsage,
|
|
40
|
-
} from "../host-contracts.ts";
|
|
41
|
-
import {
|
|
42
|
-
createStreamIdleGuard,
|
|
43
|
-
isStreamIdleTimeoutError,
|
|
44
|
-
StreamIdleTimeoutError,
|
|
45
|
-
} from "../stream-idle-guard.ts";
|
|
46
|
-
import {
|
|
47
|
-
hasUpstreamErrorTestimony,
|
|
48
|
-
isNonSuccessHttpStatus,
|
|
49
|
-
projectConfirmedRemotePayload,
|
|
50
|
-
} from "../upstream-error-testimony.ts";
|
|
51
|
-
|
|
52
|
-
export const DEFAULT_COMPLIANCE_IDLE_MAX_RETRIES = 2;
|
|
53
|
-
|
|
54
|
-
/** Recover a typed idle timeout from a thrown error or an error-stop message. */
|
|
55
|
-
function streamIdleTimeoutFromUnknown(value: unknown): StreamIdleTimeoutError | undefined {
|
|
56
|
-
if (isStreamIdleTimeoutError(value)) return value;
|
|
57
|
-
const message = value instanceof Error ? value.message : typeof value === "string" ? value : undefined;
|
|
58
|
-
if (message === undefined) return undefined;
|
|
59
|
-
const match = /stream idle timeout after (\d+)ms/i.exec(message);
|
|
60
|
-
return match !== null && match[1] !== undefined
|
|
61
|
-
? new StreamIdleTimeoutError(Number(match[1]))
|
|
62
|
-
: undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// ── Stream / remote error projection helpers ───────────────────────────────
|
|
67
|
-
|
|
68
|
-
function emptyUsage(): Usage {
|
|
69
|
-
return {
|
|
70
|
-
input: 0,
|
|
71
|
-
output: 0,
|
|
72
|
-
cacheRead: 0,
|
|
73
|
-
cacheWrite: 0,
|
|
74
|
-
totalTokens: 0,
|
|
75
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function addUsage(total: Usage, next?: Usage): void {
|
|
80
|
-
if (!next) return;
|
|
81
|
-
total.input += next.input;
|
|
82
|
-
total.output += next.output;
|
|
83
|
-
total.cacheRead += next.cacheRead;
|
|
84
|
-
total.cacheWrite += next.cacheWrite;
|
|
85
|
-
total.totalTokens += next.totalTokens;
|
|
86
|
-
total.cost.input += next.cost?.input ?? 0;
|
|
87
|
-
total.cost.output += next.cost?.output ?? 0;
|
|
88
|
-
total.cost.cacheRead += next.cost?.cacheRead ?? 0;
|
|
89
|
-
total.cost.cacheWrite += next.cost?.cacheWrite ?? 0;
|
|
90
|
-
total.cost.total += next.cost?.total ?? 0;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function numericHttpStatus(value: unknown): number | undefined {
|
|
94
|
-
return isNonSuccessHttpStatus(value) ? value : undefined;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
type StructuredRemoteProjection = {
|
|
98
|
-
readonly hasTestimony: boolean;
|
|
99
|
-
readonly httpStatus?: number;
|
|
100
|
-
readonly diagnostics?: unknown;
|
|
101
|
-
readonly body?: unknown;
|
|
102
|
-
readonly code?: unknown;
|
|
103
|
-
readonly errno?: unknown;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
function projectStructuredRemote(error: unknown): StructuredRemoteProjection {
|
|
107
|
-
let httpStatus: number | undefined;
|
|
108
|
-
let diagnostics: unknown;
|
|
109
|
-
let body: unknown;
|
|
110
|
-
let code: unknown;
|
|
111
|
-
let errno: unknown;
|
|
112
|
-
let cursor: unknown = error;
|
|
113
|
-
const seen = new Set<unknown>();
|
|
114
|
-
while (typeof cursor === "object" && cursor !== null && !seen.has(cursor)) {
|
|
115
|
-
seen.add(cursor);
|
|
116
|
-
const record = cursor as Record<string, unknown>;
|
|
117
|
-
const nodeStatus = numericHttpStatus(record.statusCode)
|
|
118
|
-
?? numericHttpStatus(record.status)
|
|
119
|
-
?? numericHttpStatus(record.httpStatus);
|
|
120
|
-
const nodeDiagnostics = Array.isArray(record.diagnostics) && record.diagnostics.length > 0
|
|
121
|
-
? record.diagnostics
|
|
122
|
-
: undefined;
|
|
123
|
-
const nodeHasTestimony = hasUpstreamErrorTestimony({
|
|
124
|
-
...(nodeStatus === undefined ? {} : { httpStatus: nodeStatus }),
|
|
125
|
-
...(nodeDiagnostics === undefined ? {} : { diagnostics: nodeDiagnostics }),
|
|
126
|
-
});
|
|
127
|
-
if (httpStatus === undefined && nodeStatus !== undefined) httpStatus = nodeStatus;
|
|
128
|
-
if (diagnostics === undefined && nodeDiagnostics !== undefined) diagnostics = nodeDiagnostics;
|
|
129
|
-
if (nodeHasTestimony) {
|
|
130
|
-
const payload = projectConfirmedRemotePayload(record);
|
|
131
|
-
if (body === undefined && payload.body !== undefined) body = payload.body;
|
|
132
|
-
if (code === undefined && payload.code !== undefined) code = payload.code;
|
|
133
|
-
if (errno === undefined && payload.errno !== undefined) errno = payload.errno;
|
|
134
|
-
}
|
|
135
|
-
cursor = record.cause;
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
hasTestimony: hasUpstreamErrorTestimony({
|
|
139
|
-
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
140
|
-
...(diagnostics === undefined ? {} : { diagnostics }),
|
|
141
|
-
}),
|
|
142
|
-
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
143
|
-
...(diagnostics === undefined ? {} : { diagnostics }),
|
|
144
|
-
...(body === undefined ? {} : { body }),
|
|
145
|
-
...(code === undefined ? {} : { code }),
|
|
146
|
-
...(errno === undefined ? {} : { errno }),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function attachObservedHttpStatus<T extends AssistantMessage>(
|
|
151
|
-
message: T,
|
|
152
|
-
observedHttpStatus: number | undefined,
|
|
153
|
-
): T {
|
|
154
|
-
if (observedHttpStatus === undefined) return message;
|
|
155
|
-
if (message.stopReason !== "error" && message.stopReason !== "aborted") return message;
|
|
156
|
-
if (numericHttpStatus(observedHttpStatus) === undefined) return message;
|
|
157
|
-
if (projectStructuredRemote(message).httpStatus !== undefined) return message;
|
|
158
|
-
return Object.assign(message, {
|
|
159
|
-
status: observedHttpStatus,
|
|
160
|
-
statusCode: observedHttpStatus,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function enrichStreamEvent(event: unknown, observedHttpStatus: number | undefined): unknown {
|
|
165
|
-
if (observedHttpStatus === undefined || event === null || typeof event !== "object") return event;
|
|
166
|
-
const record = event as Record<string, unknown>;
|
|
167
|
-
if (record.type === "error" && record.error !== null && typeof record.error === "object") {
|
|
168
|
-
return {
|
|
169
|
-
...record,
|
|
170
|
-
error: attachObservedHttpStatus(record.error as AssistantMessage, observedHttpStatus),
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
if (record.type === "done" && record.message !== null && typeof record.message === "object") {
|
|
174
|
-
return {
|
|
175
|
-
...record,
|
|
176
|
-
message: attachObservedHttpStatus(record.message as AssistantMessage, observedHttpStatus),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
if (record.partial !== null && typeof record.partial === "object") {
|
|
180
|
-
return {
|
|
181
|
-
...record,
|
|
182
|
-
partial: attachObservedHttpStatus(record.partial as AssistantMessage, observedHttpStatus),
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
return event;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ── S3 Institutional Session Open Seam ─────────────────────────────────────
|
|
189
|
-
|
|
190
|
-
export type OpenPiInstitutionalSessionOptions = HostInstitutionalSessionOptions & {
|
|
191
|
-
readonly label?: string;
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Result of the Pi institutional open seam (#518 §1②).
|
|
196
|
-
* The host-neutral handle is the open-turn-close surface and does not leak
|
|
197
|
-
* AgentSession, ModelRuntime, or Provider objects out of the adapter.
|
|
198
|
-
*/
|
|
199
|
-
export type OpenPiInstitutionalSessionResult = {
|
|
200
|
-
readonly handle: HostInstitutionalSessionHandle;
|
|
201
|
-
/**
|
|
202
|
-
* Primary provider-stream failure held at the adapter boundary when a
|
|
203
|
-
* provider stream errors after idle-only retries are exhausted. Consumers
|
|
204
|
-
* that need to distinguish a step machine "error" response from a real
|
|
205
|
-
* transport failure read this to preserve primary-cause fidelity
|
|
206
|
-
* (ADR 0018 / 失败诚实宪法). Undefined while the session stays healthy.
|
|
207
|
-
*/
|
|
208
|
-
readonly streamFailure: unknown;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
export async function openPiInProcessSession(
|
|
212
|
-
options: OpenPiInstitutionalSessionOptions,
|
|
213
|
-
): Promise<OpenPiInstitutionalSessionResult> {
|
|
214
|
-
const label = options.label ?? "Institutional sub-session";
|
|
215
|
-
const selection = options.selection;
|
|
216
|
-
|
|
217
|
-
// 1. Scratch management
|
|
218
|
-
let scratchDir: string | undefined;
|
|
219
|
-
let resolvedAgentDir = options.agentDir;
|
|
220
|
-
if (resolvedAgentDir === undefined) {
|
|
221
|
-
scratchDir = await mkdtemp(join(options.credentialScratchParent ?? tmpdir(), "ak-institutional-"));
|
|
222
|
-
resolvedAgentDir = scratchDir;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
// 2. Auth resolution in Pi layer via explicit selection (Hop 3)
|
|
227
|
-
// spec-2: adapter creates its OWN child-local ModelRuntime and ModelRegistry.
|
|
228
|
-
// Auth is resolved strictly by explicit selection on the child registry — never
|
|
229
|
-
// ambiently inherited from the parent ExtensionContext/modelRegistry.
|
|
230
|
-
const childRuntime = await ModelRuntime.create();
|
|
231
|
-
const childRegistry = new ModelRegistry(childRuntime);
|
|
232
|
-
|
|
233
|
-
const childProvider: Provider | undefined = typeof childRegistry.getProvider === "function"
|
|
234
|
-
? childRegistry.getProvider(selection.provider)
|
|
235
|
-
: undefined;
|
|
236
|
-
|
|
237
|
-
const foundModel = typeof childRegistry.find === "function"
|
|
238
|
-
? childRegistry.find(selection.provider, selection.model)
|
|
239
|
-
: undefined;
|
|
240
|
-
|
|
241
|
-
const withTypedReason = (error: Error, reason: "auth" | "model" | "thinking"): Error =>
|
|
242
|
-
Object.assign(error, { reason });
|
|
243
|
-
|
|
244
|
-
// Existing Navigator/model contract (pre-#590): unknown provider or model is
|
|
245
|
-
// typed unavailable/model — never auth. Auth runs only after the selection
|
|
246
|
-
// resolves to a known provider surface.
|
|
247
|
-
if (childProvider === undefined) {
|
|
248
|
-
throw withTypedReason(
|
|
249
|
-
new Error(`${label} model is unavailable: ${selection.provider}/${selection.model}`),
|
|
250
|
-
"model",
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const providerDefaultModel = childProvider.getModels?.()[0];
|
|
255
|
-
const fallbackApi = providerDefaultModel?.api
|
|
256
|
-
?? (childProvider as any)?.api
|
|
257
|
-
?? (selection.provider === "openai-codex" ? "openai-codex-responses" : "openai-completions");
|
|
258
|
-
// Fallback model facts come from the provider surface only — never derive
|
|
259
|
-
// reasoning (or any capability) from the thinking string (#683 pass-through).
|
|
260
|
-
const modelToUse = foundModel ?? {
|
|
261
|
-
id: selection.model,
|
|
262
|
-
name: selection.model,
|
|
263
|
-
api: fallbackApi as any,
|
|
264
|
-
provider: selection.provider,
|
|
265
|
-
baseUrl: providerDefaultModel?.baseUrl ?? "",
|
|
266
|
-
reasoning: providerDefaultModel?.reasoning ?? false,
|
|
267
|
-
input: ["text"],
|
|
268
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
269
|
-
contextWindow: 128000,
|
|
270
|
-
maxTokens: 16384,
|
|
271
|
-
};
|
|
272
|
-
|
|
273
|
-
let resolution: { auth: { baseUrl?: string; apiKey?: string; headers?: Record<string, string | null> }; env?: Record<string, string> } | undefined;
|
|
274
|
-
if (typeof childRegistry.getProviderAuth === "function") {
|
|
275
|
-
resolution = await childRegistry.getProviderAuth(selection.provider).catch((error: unknown) => {
|
|
276
|
-
throw withTypedReason(
|
|
277
|
-
new Error(`${label} authentication failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error }),
|
|
278
|
-
"auth",
|
|
279
|
-
);
|
|
280
|
-
});
|
|
281
|
-
if (resolution === undefined) {
|
|
282
|
-
throw withTypedReason(
|
|
283
|
-
new Error(`${label} authentication failed: provider is not configured: ${selection.provider}`),
|
|
284
|
-
"auth",
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
let authResult: { ok: boolean; error?: string; apiKey?: string; headers?: Record<string, string | null>; env?: Record<string, string> } | undefined;
|
|
290
|
-
if (typeof childRegistry.getApiKeyAndHeaders === "function") {
|
|
291
|
-
authResult = await childRegistry.getApiKeyAndHeaders(modelToUse as any);
|
|
292
|
-
if (authResult && !authResult.ok) {
|
|
293
|
-
throw withTypedReason(
|
|
294
|
-
new Error(`${label} authentication failed: ${authResult.error}`),
|
|
295
|
-
"auth",
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const resolvedApiKey = authResult?.apiKey ?? resolution?.auth?.apiKey;
|
|
301
|
-
const resolvedHeaders = authResult?.headers ?? resolution?.auth?.headers;
|
|
302
|
-
const resolvedEnv = authResult?.env ?? resolution?.env;
|
|
303
|
-
const effectiveBaseUrl = resolution?.auth?.baseUrl ?? modelToUse.baseUrl;
|
|
304
|
-
|
|
305
|
-
const effectiveModel: Model<Api> = {
|
|
306
|
-
...modelToUse,
|
|
307
|
-
baseUrl: effectiveBaseUrl,
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
// 3. Standalone ModelRuntime + Provider with idle-only retry.
|
|
311
|
-
// spec-2: the adapter constructs its OWN runtime/provider registration.
|
|
312
|
-
// Auth is resolved only by explicit selection above and seeded into the
|
|
313
|
-
// child runtime's own credential store — never inherited ambiently from
|
|
314
|
-
// the parent ExtensionContext/Provider.
|
|
315
|
-
const credentials = new InMemoryCredentialStore();
|
|
316
|
-
const runtime = await ModelRuntime.create({
|
|
317
|
-
credentials,
|
|
318
|
-
modelsPath: null,
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// Seed the child runtime's own credential store with the explicitly
|
|
322
|
-
// resolved auth so stream auth resolution is self-contained. No ambient
|
|
323
|
-
// fallback: absence of a resolved apiKey is not silently supplied.
|
|
324
|
-
if (resolvedApiKey !== undefined) {
|
|
325
|
-
await credentials.modify(selection.provider, async () => ({
|
|
326
|
-
type: "api_key",
|
|
327
|
-
key: resolvedApiKey,
|
|
328
|
-
...(resolvedEnv === undefined ? {} : { env: resolvedEnv }),
|
|
329
|
-
}));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const abortReason = (signal: AbortSignal): unknown =>
|
|
333
|
-
signal.reason ?? new Error(`${label} provider stream aborted`);
|
|
334
|
-
|
|
335
|
-
let streamFailureValue: unknown;
|
|
336
|
-
|
|
337
|
-
async function waitForStream<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
338
|
-
if (signal.aborted) throw abortReason(signal);
|
|
339
|
-
let onAbort: (() => void) | undefined;
|
|
340
|
-
try {
|
|
341
|
-
return await Promise.race([
|
|
342
|
-
promise,
|
|
343
|
-
new Promise<never>((_resolve, reject) => {
|
|
344
|
-
onAbort = () => reject(abortReason(signal));
|
|
345
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
346
|
-
}),
|
|
347
|
-
]);
|
|
348
|
-
} finally {
|
|
349
|
-
if (onAbort !== undefined) signal.removeEventListener("abort", onAbort);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const createRetriedStream = (
|
|
354
|
-
simple: boolean,
|
|
355
|
-
model: Model<Api>,
|
|
356
|
-
context: TranscriptContext,
|
|
357
|
-
request?: ProviderStreamOptions,
|
|
358
|
-
): ReturnType<Provider["stream"]> => {
|
|
359
|
-
const wrapped = createAssistantMessageEventStream();
|
|
360
|
-
void (async () => {
|
|
361
|
-
for (let attempt = 0; ; attempt += 1) {
|
|
362
|
-
const idle = createStreamIdleGuard(
|
|
363
|
-
options.signal === undefined ? {} : { parentSignal: options.signal },
|
|
364
|
-
);
|
|
365
|
-
let observedHttpStatus: number | undefined;
|
|
366
|
-
let observedHttpPayload: ReturnType<typeof projectConfirmedRemotePayload> | undefined;
|
|
367
|
-
try {
|
|
368
|
-
const requestSignal = request?.signal;
|
|
369
|
-
const streamSignal = requestSignal === undefined
|
|
370
|
-
? idle.signal
|
|
371
|
-
: AbortSignal.any([idle.signal, requestSignal]);
|
|
372
|
-
const priorOnResponse = request?.onResponse;
|
|
373
|
-
const priorFetch = request?.fetch;
|
|
374
|
-
const baseFetch = priorFetch ?? globalThis.fetch.bind(globalThis);
|
|
375
|
-
// Capture wire status from the real Response before the provider SDK
|
|
376
|
-
// folds non-2xx into errorMessage-only assistant stops (no free-text parse).
|
|
377
|
-
// A structured Response is testimony — including 5xx. Local/unrecognized
|
|
378
|
-
// failures never produce a Response, so they stay unlabelled.
|
|
379
|
-
const statusAwareFetch: typeof fetch = async (input, init) => {
|
|
380
|
-
const response = await baseFetch(input, init);
|
|
381
|
-
if (isNonSuccessHttpStatus(response?.status)) {
|
|
382
|
-
observedHttpStatus = response.status;
|
|
383
|
-
try {
|
|
384
|
-
const parsed: unknown = JSON.parse(await response.clone().text());
|
|
385
|
-
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
386
|
-
observedHttpPayload = projectConfirmedRemotePayload(parsed);
|
|
387
|
-
}
|
|
388
|
-
} catch {
|
|
389
|
-
// Payload observation must not break the provider stream.
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return response;
|
|
393
|
-
};
|
|
394
|
-
const retriedRequest: ProviderStreamOptions = {
|
|
395
|
-
...(request ?? {}),
|
|
396
|
-
...(resolvedEnv === undefined ? {} : { env: resolvedEnv }),
|
|
397
|
-
signal: streamSignal,
|
|
398
|
-
maxRetries: 0,
|
|
399
|
-
fetch: statusAwareFetch,
|
|
400
|
-
onResponse: async (
|
|
401
|
-
response: { status: number; headers: Record<string, string> },
|
|
402
|
-
resModel: Model<Api>,
|
|
403
|
-
) => {
|
|
404
|
-
if (typeof response?.status === "number") observedHttpStatus = response.status;
|
|
405
|
-
await priorOnResponse?.(response, resModel);
|
|
406
|
-
},
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
if (childProvider === undefined) {
|
|
410
|
-
throw withTypedReason(
|
|
411
|
-
new Error(`${label} provider not found: ${model.provider}`),
|
|
412
|
-
"model",
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const source = simple
|
|
417
|
-
? childProvider.streamSimple(model, context, retriedRequest as any)
|
|
418
|
-
: childProvider.stream(model, context, retriedRequest as any);
|
|
419
|
-
let sawEvent = false;
|
|
420
|
-
const attemptEvents: any[] = [];
|
|
421
|
-
const iterator = source[Symbol.asyncIterator]();
|
|
422
|
-
while (true) {
|
|
423
|
-
const next = await waitForStream(iterator.next(), idle.signal);
|
|
424
|
-
if (next.done) break;
|
|
425
|
-
sawEvent = true;
|
|
426
|
-
idle.poke();
|
|
427
|
-
attemptEvents.push(enrichStreamEvent(next.value, observedHttpStatus));
|
|
428
|
-
}
|
|
429
|
-
const response = attachObservedHttpStatus(
|
|
430
|
-
await waitForStream(source.result(), idle.signal),
|
|
431
|
-
observedHttpStatus,
|
|
432
|
-
);
|
|
433
|
-
// Some stream APIs (e.g. OpenAI-completions over an HTTP error)
|
|
434
|
-
// surface the failure as an error-stop message rather than throwing.
|
|
435
|
-
// Hold that primary provider failure at the adapter boundary so
|
|
436
|
-
// consumers surface a real transport failure, never a projected
|
|
437
|
-
// step-machine "error" response (ADR 0018 / #518 §3).
|
|
438
|
-
if (response.stopReason === "error") {
|
|
439
|
-
const errorMessage = response.errorMessage ?? `${label} provider stream failed`;
|
|
440
|
-
const idleFailure = streamIdleTimeoutFromUnknown(errorMessage);
|
|
441
|
-
if (
|
|
442
|
-
options.idleRetry !== false
|
|
443
|
-
&& idleFailure !== undefined
|
|
444
|
-
&& attempt < DEFAULT_COMPLIANCE_IDLE_MAX_RETRIES
|
|
445
|
-
&& options.signal?.aborted !== true
|
|
446
|
-
) {
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
const failure = idleFailure ?? new Error(errorMessage, { cause: response });
|
|
450
|
-
// Only structured status: onResponse observation or fields already on the
|
|
451
|
-
// assistant message. Never parse errorMessage prose for HTTP status.
|
|
452
|
-
const httpStatus = numericHttpStatus(observedHttpStatus)
|
|
453
|
-
?? numericHttpStatus((response as { statusCode?: unknown }).statusCode)
|
|
454
|
-
?? numericHttpStatus((response as { status?: unknown }).status);
|
|
455
|
-
if (httpStatus !== undefined) {
|
|
456
|
-
Object.assign(failure, {
|
|
457
|
-
statusCode: httpStatus,
|
|
458
|
-
status: httpStatus,
|
|
459
|
-
...(observedHttpPayload ?? {}),
|
|
460
|
-
});
|
|
461
|
-
Object.assign(response, {
|
|
462
|
-
statusCode: httpStatus,
|
|
463
|
-
status: httpStatus,
|
|
464
|
-
...(observedHttpPayload ?? {}),
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
streamFailureValue = failure;
|
|
468
|
-
}
|
|
469
|
-
for (const ev of attemptEvents) {
|
|
470
|
-
wrapped.push(ev as any);
|
|
471
|
-
}
|
|
472
|
-
wrapped.end(response);
|
|
473
|
-
return;
|
|
474
|
-
} catch (error) {
|
|
475
|
-
if (request?.signal?.aborted) {
|
|
476
|
-
const response: AssistantMessage = {
|
|
477
|
-
role: "assistant",
|
|
478
|
-
content: [],
|
|
479
|
-
api: model.api,
|
|
480
|
-
provider: model.provider,
|
|
481
|
-
model: model.id,
|
|
482
|
-
usage: emptyUsage(),
|
|
483
|
-
stopReason: "aborted",
|
|
484
|
-
errorMessage: `${label} session aborted`,
|
|
485
|
-
timestamp: Date.now(),
|
|
486
|
-
};
|
|
487
|
-
wrapped.push({ type: "error", reason: "aborted", error: response });
|
|
488
|
-
wrapped.end(response);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
const failure = isStreamIdleTimeoutError(idle.signal.reason) ? idle.signal.reason : error;
|
|
492
|
-
const idleFailure = streamIdleTimeoutFromUnknown(failure);
|
|
493
|
-
if (
|
|
494
|
-
options.idleRetry !== false
|
|
495
|
-
&& idleFailure !== undefined
|
|
496
|
-
&& attempt < DEFAULT_COMPLIANCE_IDLE_MAX_RETRIES
|
|
497
|
-
&& options.signal?.aborted !== true
|
|
498
|
-
) {
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
const typedFailure = idleFailure ?? failure;
|
|
502
|
-
const projected = projectStructuredRemote(failure);
|
|
503
|
-
const httpStatus = projected.httpStatus ?? numericHttpStatus(observedHttpStatus);
|
|
504
|
-
const payload = projectConfirmedRemotePayload({
|
|
505
|
-
body: projected.body ?? observedHttpPayload?.body,
|
|
506
|
-
code: projected.code ?? observedHttpPayload?.code,
|
|
507
|
-
errno: projected.errno ?? observedHttpPayload?.errno,
|
|
508
|
-
});
|
|
509
|
-
// Hold the primary provider failure at the adapter boundary (ADR
|
|
510
|
-
// 0018 / 失败诚实宪法). Attach observed HTTP status when held so
|
|
511
|
-
// host-neutral consumers (navigator/auditor) can classify auth/quota
|
|
512
|
-
// without reading free-text errorMessage.
|
|
513
|
-
if (
|
|
514
|
-
httpStatus !== undefined
|
|
515
|
-
&& typeof typedFailure === "object"
|
|
516
|
-
&& typedFailure !== null
|
|
517
|
-
) {
|
|
518
|
-
Object.assign(typedFailure, { statusCode: httpStatus, status: httpStatus, ...payload });
|
|
519
|
-
}
|
|
520
|
-
streamFailureValue = typedFailure;
|
|
521
|
-
const response = {
|
|
522
|
-
role: "assistant" as const,
|
|
523
|
-
content: [] as [],
|
|
524
|
-
api: model.api,
|
|
525
|
-
provider: model.provider,
|
|
526
|
-
model: model.id,
|
|
527
|
-
usage: emptyUsage(),
|
|
528
|
-
stopReason: "error" as const,
|
|
529
|
-
errorMessage: failure instanceof Error ? failure.message : String(failure),
|
|
530
|
-
timestamp: Date.now(),
|
|
531
|
-
...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
|
|
532
|
-
...(httpStatus === undefined ? {} : { status: httpStatus, statusCode: httpStatus }),
|
|
533
|
-
...payload,
|
|
534
|
-
} as unknown as AssistantMessage;
|
|
535
|
-
wrapped.push({ type: "error", reason: "error", error: response });
|
|
536
|
-
wrapped.end(response);
|
|
537
|
-
return;
|
|
538
|
-
} finally {
|
|
539
|
-
idle.dispose();
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
})();
|
|
543
|
-
return wrapped as ReturnType<Provider["stream"]>;
|
|
544
|
-
};
|
|
545
|
-
|
|
546
|
-
const provider: Provider = {
|
|
547
|
-
id: selection.provider,
|
|
548
|
-
name: childProvider?.name ?? label,
|
|
549
|
-
...(effectiveBaseUrl ? { baseUrl: effectiveBaseUrl } : {}),
|
|
550
|
-
...(resolvedHeaders ? { headers: resolvedHeaders as any } : {}),
|
|
551
|
-
auth: {
|
|
552
|
-
apiKey: {
|
|
553
|
-
name: `${label} authentication`,
|
|
554
|
-
async resolve() {
|
|
555
|
-
return {
|
|
556
|
-
auth: {
|
|
557
|
-
...(resolvedApiKey === undefined ? {} : { apiKey: resolvedApiKey }),
|
|
558
|
-
...(resolvedHeaders === undefined ? {} : { headers: resolvedHeaders }),
|
|
559
|
-
...(effectiveBaseUrl === undefined ? {} : { baseUrl: effectiveBaseUrl }),
|
|
560
|
-
},
|
|
561
|
-
...(resolvedEnv === undefined ? {} : { env: resolvedEnv }),
|
|
562
|
-
};
|
|
563
|
-
},
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
getModels() { return [effectiveModel]; },
|
|
567
|
-
stream(model, childContext, request) {
|
|
568
|
-
return createRetriedStream(false, model, childContext, request as ProviderStreamOptions | undefined);
|
|
569
|
-
},
|
|
570
|
-
streamSimple(model, childContext, request) {
|
|
571
|
-
return createRetriedStream(true, model, childContext, request as ProviderStreamOptions | undefined);
|
|
572
|
-
},
|
|
573
|
-
};
|
|
574
|
-
|
|
575
|
-
runtime.registerNativeProvider(provider);
|
|
576
|
-
await runtime.refresh({ allowNetwork: false });
|
|
577
|
-
|
|
578
|
-
// 4. Settings and ResourceLoader (fixed adapter policies: no extensions/skills/themes/templates/context-files)
|
|
579
|
-
const settings = SettingsManager.inMemory({ compaction: { enabled: false }, retry: { enabled: false } });
|
|
580
|
-
const loader = new DefaultResourceLoader({
|
|
581
|
-
cwd: options.cwd,
|
|
582
|
-
agentDir: resolvedAgentDir,
|
|
583
|
-
settingsManager: settings,
|
|
584
|
-
noExtensions: true,
|
|
585
|
-
noSkills: true,
|
|
586
|
-
noPromptTemplates: true,
|
|
587
|
-
noThemes: true,
|
|
588
|
-
noContextFiles: true,
|
|
589
|
-
systemPrompt: options.systemPrompt,
|
|
590
|
-
});
|
|
591
|
-
await loader.reload();
|
|
592
|
-
|
|
593
|
-
// 5. SessionManager resolution
|
|
594
|
-
let sessionManager: SessionManager;
|
|
595
|
-
if (options.sessionManager !== undefined) {
|
|
596
|
-
sessionManager = options.sessionManager as SessionManager;
|
|
597
|
-
} else if (options.sessionIdentity !== undefined) {
|
|
598
|
-
sessionManager = createRecordSession({
|
|
599
|
-
cwd: options.cwd,
|
|
600
|
-
kind: options.sessionIdentity.kind,
|
|
601
|
-
...(options.sessionIdentity.subject === undefined ? {} : { subject: options.sessionIdentity.subject }),
|
|
602
|
-
...(options.sessionIdentity.parent === undefined ? {} : { parent: options.sessionIdentity.parent }),
|
|
603
|
-
}) as SessionManager;
|
|
604
|
-
} else {
|
|
605
|
-
sessionManager = createRecordSession({
|
|
606
|
-
cwd: options.cwd,
|
|
607
|
-
kind: "institutional",
|
|
608
|
-
}) as SessionManager;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// 6. Tools mapping
|
|
612
|
-
const customTools: ToolDefinition[] = [];
|
|
613
|
-
if (options.customTools !== undefined) {
|
|
614
|
-
customTools.push(...(options.customTools as ToolDefinition[]));
|
|
615
|
-
}
|
|
616
|
-
if (options.tools !== undefined) {
|
|
617
|
-
for (const hostTool of options.tools) {
|
|
618
|
-
customTools.push({
|
|
619
|
-
name: hostTool.name,
|
|
620
|
-
label: hostTool.label,
|
|
621
|
-
description: hostTool.description,
|
|
622
|
-
parameters: hostTool.parameters,
|
|
623
|
-
execute: async (toolCallId, params, signal, update, _ctx) => {
|
|
624
|
-
const res = await hostTool.execute(
|
|
625
|
-
toolCallId,
|
|
626
|
-
params as any,
|
|
627
|
-
signal,
|
|
628
|
-
update === undefined ? undefined : (u) => update(u as AgentToolResult<unknown>),
|
|
629
|
-
undefined as any,
|
|
630
|
-
);
|
|
631
|
-
return res as AgentToolResult<unknown>;
|
|
632
|
-
},
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// 7. Create AgentSession
|
|
638
|
-
// Thinking is opaque pass-through (#683 / #675 ⑥). Absent selection omits
|
|
639
|
-
// thinkingLevel (Pi owns default). Pi clamps unsupported levels itself;
|
|
640
|
-
// we do not re-check or invent package defaults. When reusing a session file,
|
|
641
|
-
// re-apply seat model after open so Pi cannot restore a stale model over
|
|
642
|
-
// selection (#675 ⑤ / #697).
|
|
643
|
-
const requestedThinking = selection.thinking;
|
|
644
|
-
const priorEntries =
|
|
645
|
-
typeof (sessionManager as { getEntries?: () => readonly unknown[] }).getEntries === "function"
|
|
646
|
-
? (sessionManager as { getEntries: () => readonly unknown[] }).getEntries()
|
|
647
|
-
: [];
|
|
648
|
-
const reusedSession = priorEntries.some((entry) => {
|
|
649
|
-
if (typeof entry !== "object" || entry === null) return false;
|
|
650
|
-
const type = (entry as { type?: unknown }).type;
|
|
651
|
-
return type === "message" || type === "model_change" || type === "thinking_level_change";
|
|
652
|
-
});
|
|
653
|
-
const { session } = await createAgentSession({
|
|
654
|
-
cwd: options.cwd,
|
|
655
|
-
model: effectiveModel,
|
|
656
|
-
...(requestedThinking === undefined ? {} : { thinkingLevel: requestedThinking as any }),
|
|
657
|
-
modelRuntime: runtime,
|
|
658
|
-
sessionManager,
|
|
659
|
-
settingsManager: settings,
|
|
660
|
-
agentDir: resolvedAgentDir,
|
|
661
|
-
resourceLoader: loader,
|
|
662
|
-
...(options.noTools === undefined ? {} : { noTools: options.noTools }),
|
|
663
|
-
...(options.toolsAllowlist === undefined ? {} : { tools: options.toolsAllowlist as string[] }),
|
|
664
|
-
...(customTools.length === 0 ? {} : { customTools }),
|
|
665
|
-
});
|
|
666
|
-
if (reusedSession) {
|
|
667
|
-
try {
|
|
668
|
-
await session.setModel(effectiveModel);
|
|
669
|
-
if (requestedThinking !== undefined) {
|
|
670
|
-
session.setThinkingLevel(requestedThinking as any);
|
|
671
|
-
}
|
|
672
|
-
} catch (error) {
|
|
673
|
-
session.dispose();
|
|
674
|
-
throw withTypedReason(
|
|
675
|
-
error instanceof Error
|
|
676
|
-
? error
|
|
677
|
-
: new Error(
|
|
678
|
-
`${label} failed to apply seat model/thinking for ${selection.provider}/${selection.model}`,
|
|
679
|
-
),
|
|
680
|
-
requestedThinking !== undefined ? "thinking" : "model",
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// 8. Event subscriptions
|
|
686
|
-
const listeners = new Set<(event: HostInstitutionalSessionEvent) => void>();
|
|
687
|
-
const accumulatedUsage = emptyUsage();
|
|
688
|
-
|
|
689
|
-
let lastEmittedAssistant: AssistantMessage | undefined;
|
|
690
|
-
const unsubscribeSession = session.subscribe((event) => {
|
|
691
|
-
if (event.type === "message_end") {
|
|
692
|
-
const msg = event.message as AssistantMessage;
|
|
693
|
-
if (msg.role === "assistant") {
|
|
694
|
-
lastEmittedAssistant = msg;
|
|
695
|
-
if (msg.usage) {
|
|
696
|
-
addUsage(accumulatedUsage, msg.usage);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
for (const listener of listeners) {
|
|
700
|
-
listener({
|
|
701
|
-
type: "message_end",
|
|
702
|
-
role: msg.role,
|
|
703
|
-
message: msg,
|
|
704
|
-
...(msg.usage === undefined ? {} : { usage: msg.usage as HostSessionUsage }),
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
} else if (event.type === "turn_end") {
|
|
708
|
-
const msg = event.message as AssistantMessage;
|
|
709
|
-
for (const listener of listeners) {
|
|
710
|
-
listener({
|
|
711
|
-
type: "turn_end",
|
|
712
|
-
...(msg.stopReason === undefined ? {} : { stopReason: msg.stopReason }),
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
} else if (event.type === "tool_execution_start") {
|
|
716
|
-
for (const listener of listeners) {
|
|
717
|
-
listener({
|
|
718
|
-
type: "tool_call",
|
|
719
|
-
toolCallId: event.toolCallId,
|
|
720
|
-
toolName: event.toolName,
|
|
721
|
-
args: (event as any).args ?? (event as any).input,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
} else if (event.type === "tool_execution_end") {
|
|
725
|
-
for (const listener of listeners) {
|
|
726
|
-
listener({
|
|
727
|
-
type: "tool_result",
|
|
728
|
-
toolCallId: event.toolCallId,
|
|
729
|
-
toolName: event.toolName,
|
|
730
|
-
isError: event.isError,
|
|
731
|
-
details: (event as any).result ?? (event as any).details,
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
let closed = false;
|
|
738
|
-
const sessionFile = sessionManager.getSessionFile();
|
|
739
|
-
const sessionId = (sessionManager as any).getHeader?.()?.id;
|
|
740
|
-
|
|
741
|
-
const handle: HostInstitutionalSessionHandle = {
|
|
742
|
-
...(sessionFile === undefined ? {} : { sessionFile }),
|
|
743
|
-
...(sessionId === undefined ? {} : { sessionId }),
|
|
744
|
-
|
|
745
|
-
async prompt(text: string): Promise<HostAssistantTurnResult> {
|
|
746
|
-
const abortSession = () => { void session.abort(); };
|
|
747
|
-
if (options.signal?.aborted) abortSession();
|
|
748
|
-
else options.signal?.addEventListener("abort", abortSession, { once: true });
|
|
749
|
-
|
|
750
|
-
const assistantCountBefore = session.messages.filter(
|
|
751
|
-
(message) => message.role === "assistant",
|
|
752
|
-
).length;
|
|
753
|
-
let promptError: unknown;
|
|
754
|
-
try {
|
|
755
|
-
await session.prompt(text);
|
|
756
|
-
} catch (error) {
|
|
757
|
-
promptError = error;
|
|
758
|
-
} finally {
|
|
759
|
-
options.signal?.removeEventListener("abort", abortSession);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const lastAssistant = [...session.messages]
|
|
763
|
-
.reverse()
|
|
764
|
-
.find((message) => message.role === "assistant") as AssistantMessage | undefined;
|
|
765
|
-
|
|
766
|
-
if (lastAssistant !== undefined && lastAssistant !== lastEmittedAssistant) {
|
|
767
|
-
lastEmittedAssistant = lastAssistant;
|
|
768
|
-
for (const listener of listeners) {
|
|
769
|
-
listener({
|
|
770
|
-
type: "message_end",
|
|
771
|
-
role: lastAssistant.role,
|
|
772
|
-
message: lastAssistant,
|
|
773
|
-
...(lastAssistant.usage === undefined ? {} : { usage: lastAssistant.usage as HostSessionUsage }),
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const producedAssistant = session.messages.filter(
|
|
779
|
-
(message) => message.role === "assistant",
|
|
780
|
-
).length > assistantCountBefore;
|
|
781
|
-
if (promptError !== undefined && !producedAssistant) {
|
|
782
|
-
throw promptError;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
return {
|
|
786
|
-
text: session.getLastAssistantText() ?? "",
|
|
787
|
-
...(lastAssistant?.stopReason === undefined ? {} : { stopReason: lastAssistant.stopReason }),
|
|
788
|
-
...(lastAssistant?.errorMessage === undefined ? {} : { errorMessage: lastAssistant.errorMessage }),
|
|
789
|
-
usage: accumulatedUsage as HostSessionUsage,
|
|
790
|
-
messages: session.messages,
|
|
791
|
-
};
|
|
792
|
-
},
|
|
793
|
-
|
|
794
|
-
subscribe(listener: (event: HostInstitutionalSessionEvent) => void): () => void {
|
|
795
|
-
listeners.add(listener);
|
|
796
|
-
return () => {
|
|
797
|
-
listeners.delete(listener);
|
|
798
|
-
};
|
|
799
|
-
},
|
|
800
|
-
|
|
801
|
-
abort(): void {
|
|
802
|
-
void session.abort();
|
|
803
|
-
},
|
|
804
|
-
|
|
805
|
-
async close(): Promise<void> {
|
|
806
|
-
if (closed) return;
|
|
807
|
-
closed = true;
|
|
808
|
-
listeners.clear();
|
|
809
|
-
const cleanupFailures: unknown[] = [];
|
|
810
|
-
try {
|
|
811
|
-
unsubscribeSession();
|
|
812
|
-
} catch (error) {
|
|
813
|
-
cleanupFailures.push(error);
|
|
814
|
-
}
|
|
815
|
-
try {
|
|
816
|
-
session.dispose();
|
|
817
|
-
} catch (error) {
|
|
818
|
-
cleanupFailures.push(error);
|
|
819
|
-
}
|
|
820
|
-
if (scratchDir !== undefined) {
|
|
821
|
-
try {
|
|
822
|
-
await rm(scratchDir, { recursive: true, force: true });
|
|
823
|
-
} catch (error) {
|
|
824
|
-
cleanupFailures.push(error);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
if (cleanupFailures.length === 1) throw cleanupFailures[0];
|
|
828
|
-
if (cleanupFailures.length > 1) {
|
|
829
|
-
throw new AggregateError(cleanupFailures, `${label} close cleanup failures`, {
|
|
830
|
-
cause: cleanupFailures[0],
|
|
831
|
-
});
|
|
832
|
-
}
|
|
833
|
-
},
|
|
834
|
-
};
|
|
835
|
-
|
|
836
|
-
return {
|
|
837
|
-
handle,
|
|
838
|
-
// Read lazily: the provider stream runs during session.prompt, so the
|
|
839
|
-
// primary failure is only known after that turn completes. A getter keeps
|
|
840
|
-
// this reflecting the latest value instead of freezing it at open time.
|
|
841
|
-
get streamFailure() {
|
|
842
|
-
return streamFailureValue;
|
|
843
|
-
},
|
|
844
|
-
};
|
|
845
|
-
} catch (openError) {
|
|
846
|
-
if (scratchDir !== undefined) {
|
|
847
|
-
try {
|
|
848
|
-
await rm(scratchDir, { recursive: true, force: true });
|
|
849
|
-
} catch (cleanupFailure) {
|
|
850
|
-
// ADR 0018 / #518 §1④: primary failure and cleanup failure stay
|
|
851
|
-
// separated — cleanup must not mask the original open cause.
|
|
852
|
-
throw new AggregateError(
|
|
853
|
-
[openError, cleanupFailure],
|
|
854
|
-
`${label} open failed and its scratch cleanup also failed`,
|
|
855
|
-
{ cause: openError },
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
throw openError;
|
|
860
|
-
}
|
|
861
|
-
}
|