@akagilnc/pi-workflow-roles 0.1.1883 → 0.1.1893
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/dist/evidence-child-executor.js +153 -16
- package/dist/navigator-attendance.js +64 -29
- package/dist/public-cli/main.js +418 -204
- package/dist/reviewer-child-executor.js +1 -1
- package/dist/typed-provider-http.js +60 -0
- package/dist/upstream-error-testimony.js +19 -0
- package/package.json +1 -1
- package/scripts/build-package.mjs +2 -0
- package/src/evidence-child-executor.ts +177 -24
- package/src/navigator-attendance.ts +86 -33
- package/src/public-cli/coder-run.ts +25 -7
- package/src/public-cli/collector-run.ts +1 -0
- package/src/public-cli/doctor-run.ts +1 -0
- package/src/public-cli/explicit-internal.ts +61 -19
- package/src/public-cli/fixer-run.ts +25 -7
- package/src/public-cli/judge-run.ts +28 -7
- package/src/public-cli/merger-run.ts +25 -7
- package/src/public-cli/reviewer-run.ts +24 -7
- package/src/public-cli/run-lifecycle.ts +14 -76
- package/src/public-cli/settlement.ts +256 -66
- package/src/reviewer-child-executor.ts +1 -1
- package/src/role-runtime.ts +18 -8
- package/src/typed-provider-http.ts +99 -0
- package/src/upstream-error-testimony.ts +45 -0
|
@@ -6,7 +6,7 @@ import { executeEvidenceChild } from "./evidence-child-executor.js";
|
|
|
6
6
|
export function projectSharedChildFailure(error) {
|
|
7
7
|
if (typeof error === "object" && error !== null && "evidenceChildFailure" in error) {
|
|
8
8
|
const classification = error.evidenceChildFailure;
|
|
9
|
-
if (classification === "provider" || classification === "child") {
|
|
9
|
+
if (classification === "provider" || classification === "child" || classification === "unknown") {
|
|
10
10
|
Object.assign(error, { reviewerFailure: classification });
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const TYPED_HTTP_FILE = "typed-provider-http.json";
|
|
4
|
+
function typedProviderHttpPath(runDirectory) {
|
|
5
|
+
return join(runDirectory, TYPED_HTTP_FILE);
|
|
6
|
+
}
|
|
7
|
+
async function clearTypedProviderHttpObservation(runDirectory) {
|
|
8
|
+
try {
|
|
9
|
+
await unlink(typedProviderHttpPath(runDirectory));
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function recordTypedProviderHttpStatus(runDirectory, observation) {
|
|
18
|
+
if (observation.httpStatus >= 200 && observation.httpStatus < 300) {
|
|
19
|
+
await clearTypedProviderHttpObservation(runDirectory);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const body = {
|
|
23
|
+
httpStatus: observation.httpStatus,
|
|
24
|
+
provider: observation.provider
|
|
25
|
+
};
|
|
26
|
+
await writeFile(
|
|
27
|
+
typedProviderHttpPath(runDirectory),
|
|
28
|
+
`${JSON.stringify(body)}
|
|
29
|
+
`,
|
|
30
|
+
"utf8"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
async function readLatestTypedProviderHttpObservation(runDirectory) {
|
|
34
|
+
let text;
|
|
35
|
+
try {
|
|
36
|
+
text = await readFile(typedProviderHttpPath(runDirectory), "utf8");
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
const raw = JSON.parse(text);
|
|
44
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
45
|
+
throw new Error("typed provider HTTP observation must be a JSON object");
|
|
46
|
+
}
|
|
47
|
+
const record = raw;
|
|
48
|
+
if (typeof record.httpStatus !== "number") {
|
|
49
|
+
throw new Error("typed provider HTTP observation missing numeric httpStatus");
|
|
50
|
+
}
|
|
51
|
+
if (typeof record.provider !== "string" || record.provider.trim() === "") {
|
|
52
|
+
throw new Error("typed provider HTTP observation missing non-empty provider");
|
|
53
|
+
}
|
|
54
|
+
return { httpStatus: record.httpStatus, provider: record.provider };
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
clearTypedProviderHttpObservation,
|
|
58
|
+
readLatestTypedProviderHttpObservation,
|
|
59
|
+
recordTypedProviderHttpStatus
|
|
60
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function isNonSuccessHttpStatus(status) {
|
|
2
|
+
return typeof status === "number" && (status < 200 || status >= 300);
|
|
3
|
+
}
|
|
4
|
+
function hasUpstreamErrorTestimony(input) {
|
|
5
|
+
if (isNonSuccessHttpStatus(input.httpStatus)) return true;
|
|
6
|
+
return Array.isArray(input.diagnostics) && input.diagnostics.length > 0;
|
|
7
|
+
}
|
|
8
|
+
function projectConfirmedRemotePayload(input) {
|
|
9
|
+
return {
|
|
10
|
+
...input.body === void 0 ? {} : { body: input.body },
|
|
11
|
+
...input.code === void 0 ? {} : { code: input.code },
|
|
12
|
+
...input.errno === void 0 ? {} : { errno: input.errno }
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
hasUpstreamErrorTestimony,
|
|
17
|
+
isNonSuccessHttpStatus,
|
|
18
|
+
projectConfirmedRemotePayload
|
|
19
|
+
};
|
package/package.json
CHANGED
|
@@ -33,6 +33,11 @@ import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
|
|
|
33
33
|
import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
|
|
34
34
|
import { createStreamIdleGuard, isStreamIdleTimeoutError } from "./stream-idle-guard.ts";
|
|
35
35
|
import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
|
|
36
|
+
import {
|
|
37
|
+
hasUpstreamErrorTestimony,
|
|
38
|
+
isNonSuccessHttpStatus,
|
|
39
|
+
projectConfirmedRemotePayload,
|
|
40
|
+
} from "./upstream-error-testimony.ts";
|
|
36
41
|
|
|
37
42
|
// ── shared constants / types ──────────────────────────────────────────────
|
|
38
43
|
|
|
@@ -186,15 +191,25 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
|
|
|
186
191
|
const idle = createStreamIdleGuard(
|
|
187
192
|
options.signal === undefined ? {} : { parentSignal: options.signal },
|
|
188
193
|
);
|
|
194
|
+
// Typed HTTP status observed via onResponse — never inferred from prose.
|
|
195
|
+
let observedHttpStatus: number | undefined;
|
|
189
196
|
try {
|
|
190
197
|
const requestSignal = request?.signal;
|
|
191
198
|
const streamSignal = requestSignal === undefined
|
|
192
199
|
? idle.signal
|
|
193
200
|
: AbortSignal.any([idle.signal, requestSignal]);
|
|
201
|
+
const priorOnResponse = request?.onResponse;
|
|
194
202
|
const inheritedRequest = {
|
|
195
203
|
...(request ?? {}),
|
|
196
204
|
...(dispatch.auth.env === undefined ? {} : { env: dispatch.auth.env }),
|
|
197
205
|
signal: streamSignal,
|
|
206
|
+
onResponse: async (
|
|
207
|
+
response: { status: number; headers: Record<string, string> },
|
|
208
|
+
model: Model<Api>,
|
|
209
|
+
) => {
|
|
210
|
+
if (typeof response?.status === "number") observedHttpStatus = response.status;
|
|
211
|
+
await priorOnResponse?.(response, model);
|
|
212
|
+
},
|
|
198
213
|
} as ProviderStreamOptions;
|
|
199
214
|
if (options.runCompletion !== undefined) {
|
|
200
215
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
@@ -209,12 +224,12 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
|
|
|
209
224
|
),
|
|
210
225
|
streamSignal,
|
|
211
226
|
);
|
|
212
|
-
const response: AssistantMessage = {
|
|
227
|
+
const response: AssistantMessage = attachObservedHttpStatus({
|
|
213
228
|
...completed,
|
|
214
229
|
api: model.api,
|
|
215
230
|
provider: model.provider,
|
|
216
231
|
model: model.id,
|
|
217
|
-
};
|
|
232
|
+
}, observedHttpStatus);
|
|
218
233
|
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
219
234
|
wrapped.push({ type: "error", reason: response.stopReason, error: response });
|
|
220
235
|
} else {
|
|
@@ -232,9 +247,12 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
|
|
|
232
247
|
if (next.done) break;
|
|
233
248
|
sawEvent = true;
|
|
234
249
|
idle.poke();
|
|
235
|
-
wrapped.push(next.value as any);
|
|
250
|
+
wrapped.push(enrichStreamEvent(next.value, observedHttpStatus) as any);
|
|
236
251
|
}
|
|
237
|
-
const response =
|
|
252
|
+
const response = attachObservedHttpStatus(
|
|
253
|
+
await waitForStream(source.result(), idle.signal),
|
|
254
|
+
observedHttpStatus,
|
|
255
|
+
);
|
|
238
256
|
if (!sawEvent) wrapped.end(response);
|
|
239
257
|
return;
|
|
240
258
|
} catch (error) {
|
|
@@ -264,17 +282,26 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
|
|
|
264
282
|
continue;
|
|
265
283
|
}
|
|
266
284
|
state.streamFailure = failure;
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
285
|
+
const projected = projectStructuredRemote(failure);
|
|
286
|
+
// Prefer structured fields on the thrown failure; fall back to onResponse observation.
|
|
287
|
+
const httpStatus = projected.httpStatus ?? numericHttpStatus(observedHttpStatus);
|
|
288
|
+
const response = {
|
|
289
|
+
role: "assistant" as const,
|
|
290
|
+
content: [] as [],
|
|
270
291
|
api: model.api,
|
|
271
292
|
provider: model.provider,
|
|
272
293
|
model: model.id,
|
|
273
294
|
usage: emptyUsage(),
|
|
274
|
-
stopReason: "error",
|
|
295
|
+
stopReason: "error" as const,
|
|
296
|
+
// Preserve the held failure message bytes — do not rewrite.
|
|
275
297
|
errorMessage: failure instanceof Error ? failure.message : String(failure),
|
|
276
298
|
timestamp: Date.now(),
|
|
277
|
-
|
|
299
|
+
...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
|
|
300
|
+
...(httpStatus === undefined ? {} : { status: httpStatus, statusCode: httpStatus }),
|
|
301
|
+
...(projected.body === undefined ? {} : { body: projected.body }),
|
|
302
|
+
...(projected.code === undefined ? {} : { code: projected.code }),
|
|
303
|
+
...(projected.errno === undefined ? {} : { errno: projected.errno }),
|
|
304
|
+
} as unknown as AssistantMessage;
|
|
278
305
|
wrapped.push({ type: "error", reason: "error", error: response });
|
|
279
306
|
wrapped.end(response);
|
|
280
307
|
return;
|
|
@@ -346,11 +373,114 @@ export async function createInheritedRuntime(options: InheritedRuntimeOptions):
|
|
|
346
373
|
return state;
|
|
347
374
|
}
|
|
348
375
|
|
|
376
|
+
type EvidenceChildFailureClassification = "provider" | "child" | "unknown";
|
|
349
377
|
type ClassifiedReviewerError = Error & Readonly<{
|
|
350
|
-
evidenceChildFailure:
|
|
378
|
+
evidenceChildFailure: EvidenceChildFailureClassification;
|
|
351
379
|
evidenceChildOriginal?: unknown;
|
|
352
380
|
}>;
|
|
353
|
-
function
|
|
381
|
+
function numericHttpStatus(value: unknown): number | undefined {
|
|
382
|
+
return isNonSuccessHttpStatus(value) ? value : undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
type StructuredRemoteProjection = {
|
|
386
|
+
readonly hasTestimony: boolean;
|
|
387
|
+
readonly httpStatus?: number;
|
|
388
|
+
readonly diagnostics?: unknown;
|
|
389
|
+
readonly body?: unknown;
|
|
390
|
+
readonly code?: unknown;
|
|
391
|
+
readonly errno?: unknown;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Cause-chain reader over the shared upstream-testimony authority.
|
|
396
|
+
* Shape walking stays here; testimony + confirmed-remote payload rules are shared.
|
|
397
|
+
*/
|
|
398
|
+
function projectStructuredRemote(error: unknown): StructuredRemoteProjection {
|
|
399
|
+
let httpStatus: number | undefined;
|
|
400
|
+
let diagnostics: unknown;
|
|
401
|
+
let body: unknown;
|
|
402
|
+
let code: unknown;
|
|
403
|
+
let errno: unknown;
|
|
404
|
+
let cursor: unknown = error;
|
|
405
|
+
const seen = new Set<unknown>();
|
|
406
|
+
while (typeof cursor === "object" && cursor !== null && !seen.has(cursor)) {
|
|
407
|
+
seen.add(cursor);
|
|
408
|
+
const record = cursor as Record<string, unknown>;
|
|
409
|
+
const nodeStatus = numericHttpStatus(record.statusCode)
|
|
410
|
+
?? numericHttpStatus(record.status)
|
|
411
|
+
?? numericHttpStatus(record.httpStatus);
|
|
412
|
+
const nodeDiagnostics = Array.isArray(record.diagnostics) && record.diagnostics.length > 0
|
|
413
|
+
? record.diagnostics
|
|
414
|
+
: undefined;
|
|
415
|
+
const nodeHasTestimony = hasUpstreamErrorTestimony({
|
|
416
|
+
...(nodeStatus === undefined ? {} : { httpStatus: nodeStatus }),
|
|
417
|
+
...(nodeDiagnostics === undefined ? {} : { diagnostics: nodeDiagnostics }),
|
|
418
|
+
});
|
|
419
|
+
if (httpStatus === undefined && nodeStatus !== undefined) httpStatus = nodeStatus;
|
|
420
|
+
if (diagnostics === undefined && nodeDiagnostics !== undefined) diagnostics = nodeDiagnostics;
|
|
421
|
+
// Payload only from confirmed-remote nodes — never arbitrary local Error.code.
|
|
422
|
+
if (nodeHasTestimony) {
|
|
423
|
+
const payload = projectConfirmedRemotePayload(record);
|
|
424
|
+
if (body === undefined && payload.body !== undefined) body = payload.body;
|
|
425
|
+
if (code === undefined && payload.code !== undefined) code = payload.code;
|
|
426
|
+
if (errno === undefined && payload.errno !== undefined) errno = payload.errno;
|
|
427
|
+
}
|
|
428
|
+
cursor = record.cause;
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
hasTestimony: hasUpstreamErrorTestimony({
|
|
432
|
+
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
433
|
+
...(diagnostics === undefined ? {} : { diagnostics }),
|
|
434
|
+
}),
|
|
435
|
+
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
436
|
+
...(diagnostics === undefined ? {} : { diagnostics }),
|
|
437
|
+
...(body === undefined ? {} : { body }),
|
|
438
|
+
...(code === undefined ? {} : { code }),
|
|
439
|
+
...(errno === undefined ? {} : { errno }),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Attach a directly observed HTTP status onto an error/aborted assistant message.
|
|
445
|
+
* Does not invent status from errorMessage prose; skips when already held.
|
|
446
|
+
*/
|
|
447
|
+
function attachObservedHttpStatus<T extends AssistantMessage>(
|
|
448
|
+
message: T,
|
|
449
|
+
observedHttpStatus: number | undefined,
|
|
450
|
+
): T {
|
|
451
|
+
if (observedHttpStatus === undefined) return message;
|
|
452
|
+
if (message.stopReason !== "error" && message.stopReason !== "aborted") return message;
|
|
453
|
+
if (numericHttpStatus(observedHttpStatus) === undefined) return message;
|
|
454
|
+
if (projectStructuredRemote(message).httpStatus !== undefined) return message;
|
|
455
|
+
return Object.assign(message, {
|
|
456
|
+
status: observedHttpStatus,
|
|
457
|
+
statusCode: observedHttpStatus,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
function enrichStreamEvent(event: unknown, observedHttpStatus: number | undefined): unknown {
|
|
461
|
+
if (observedHttpStatus === undefined || event === null || typeof event !== "object") return event;
|
|
462
|
+
const record = event as Record<string, unknown>;
|
|
463
|
+
if (record.type === "error" && record.error !== null && typeof record.error === "object") {
|
|
464
|
+
return {
|
|
465
|
+
...record,
|
|
466
|
+
error: attachObservedHttpStatus(record.error as AssistantMessage, observedHttpStatus),
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (record.type === "done" && record.message !== null && typeof record.message === "object") {
|
|
470
|
+
return {
|
|
471
|
+
...record,
|
|
472
|
+
message: attachObservedHttpStatus(record.message as AssistantMessage, observedHttpStatus),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (record.partial !== null && typeof record.partial === "object") {
|
|
476
|
+
return {
|
|
477
|
+
...record,
|
|
478
|
+
partial: attachObservedHttpStatus(record.partial as AssistantMessage, observedHttpStatus),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return event;
|
|
482
|
+
}
|
|
483
|
+
function classifiedError(error: unknown, evidenceChildFailure: EvidenceChildFailureClassification): ClassifiedReviewerError {
|
|
354
484
|
const diagnostic = typeof error === "object" && error !== null && typeof (error as { errorMessage?: unknown }).errorMessage === "string"
|
|
355
485
|
? (error as { errorMessage: string }).errorMessage
|
|
356
486
|
: error === undefined ? "" : String(error);
|
|
@@ -359,7 +489,9 @@ function classifiedError(error: unknown, evidenceChildFailure: "provider" | "chi
|
|
|
359
489
|
: Object.assign(new Error(diagnostic, { cause: error }), { evidenceChildOriginal: error });
|
|
360
490
|
const classification = "evidenceChildFailure" in wrapped
|
|
361
491
|
? (wrapped as ClassifiedReviewerError).evidenceChildFailure
|
|
362
|
-
: evidenceChildFailure
|
|
492
|
+
: evidenceChildFailure === "provider" && !projectStructuredRemote(error).hasTestimony
|
|
493
|
+
? "unknown"
|
|
494
|
+
: evidenceChildFailure;
|
|
363
495
|
return Object.assign(wrapped, { evidenceChildFailure: classification });
|
|
364
496
|
}
|
|
365
497
|
|
|
@@ -461,13 +593,19 @@ export async function executeEvidenceChild(
|
|
|
461
593
|
const lastAssistant = [...session.messages]
|
|
462
594
|
.reverse()
|
|
463
595
|
.find((message) => message.role === "assistant");
|
|
464
|
-
|
|
596
|
+
// error|aborted assistant stops share the upstream-testimony rule: provider only
|
|
597
|
+
// with direct HTTP/SDK testimony, otherwise existing unknown. child is reserved
|
|
598
|
+
// for real local child/report failures (no assistant / blank report / cleanup).
|
|
599
|
+
if (
|
|
600
|
+
lastAssistant?.role === "assistant"
|
|
601
|
+
&& (lastAssistant.stopReason === "error" || lastAssistant.stopReason === "aborted")
|
|
602
|
+
) {
|
|
465
603
|
throw classifiedError(
|
|
466
604
|
new Error(lastAssistant.errorMessage ?? "", { cause: lastAssistant }),
|
|
467
|
-
"provider",
|
|
605
|
+
projectStructuredRemote(lastAssistant).hasTestimony ? "provider" : "unknown",
|
|
468
606
|
);
|
|
469
607
|
}
|
|
470
|
-
if (lastAssistant?.role !== "assistant"
|
|
608
|
+
if (lastAssistant?.role !== "assistant") {
|
|
471
609
|
throw classifiedError(
|
|
472
610
|
new Error("Evidence child child terminated without a report", {
|
|
473
611
|
cause: lastAssistant ?? session.messages,
|
|
@@ -820,22 +958,37 @@ export async function executeAuditorChild(
|
|
|
820
958
|
else if (retentionFailure !== undefined) throw retentionFailure;
|
|
821
959
|
} catch (retentionFailure) {
|
|
822
960
|
if (response.stopReason !== "error") throw retentionFailure;
|
|
961
|
+
// Do not trim/rewrite the held errorMessage bytes.
|
|
962
|
+
const diagnostic = typeof response.errorMessage === "string" && response.errorMessage.trim() !== ""
|
|
963
|
+
? response.errorMessage
|
|
964
|
+
: undefined;
|
|
965
|
+
const projected = projectStructuredRemote(response);
|
|
823
966
|
const failure = new Error(
|
|
824
|
-
|
|
967
|
+
diagnostic ?? "",
|
|
825
968
|
{ cause: retentionFailure },
|
|
826
969
|
) as Error & {
|
|
827
|
-
knownCause: "provider";
|
|
970
|
+
knownCause: "provider" | "unrecognized";
|
|
828
971
|
failureCode?: string;
|
|
829
972
|
details: Record<string, unknown>;
|
|
830
973
|
};
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
974
|
+
if (projected.hasTestimony && (response.model || response.provider)) {
|
|
975
|
+
failure.name = response.model || response.provider || "Error";
|
|
976
|
+
failure.failureCode = response.provider || response.model;
|
|
977
|
+
}
|
|
978
|
+
failure.knownCause = projected.hasTestimony ? "provider" : "unrecognized";
|
|
834
979
|
const retentionError = retentionFailure instanceof Error ? retentionFailure : undefined;
|
|
835
980
|
const retentionCause = retentionError?.cause;
|
|
836
981
|
failure.details = {
|
|
837
|
-
...(
|
|
838
|
-
...(response.
|
|
982
|
+
...(diagnostic === undefined ? {} : { errorMessage: diagnostic }),
|
|
983
|
+
...(projected.hasTestimony && response.provider ? { provider: response.provider } : {}),
|
|
984
|
+
...(projected.hasTestimony && response.model ? { model: response.model } : {}),
|
|
985
|
+
...(response.api ? { api: response.api } : {}),
|
|
986
|
+
...(response.rawStopReason ? { rawStopReason: response.rawStopReason } : {}),
|
|
987
|
+
...(projected.httpStatus === undefined ? {} : { httpStatus: projected.httpStatus }),
|
|
988
|
+
...(projected.diagnostics === undefined ? {} : { diagnostics: projected.diagnostics }),
|
|
989
|
+
...(projected.body === undefined ? {} : { body: projected.body }),
|
|
990
|
+
...(projected.code === undefined ? {} : { code: projected.code }),
|
|
991
|
+
...(projected.errno === undefined ? {} : { errno: projected.errno }),
|
|
839
992
|
retentionFailure: {
|
|
840
993
|
name: retentionError?.name ?? typeof retentionFailure,
|
|
841
994
|
message: retentionError?.message ?? String(retentionFailure),
|
|
@@ -862,8 +1015,8 @@ export async function executeAuditorChild(
|
|
|
862
1015
|
parent: binding.parent,
|
|
863
1016
|
failure: {
|
|
864
1017
|
cause: failure.knownCause,
|
|
865
|
-
identity: { name: failure.name, code: failure.failureCode },
|
|
866
|
-
diagnostic: failure.message,
|
|
1018
|
+
...(failure.failureCode === undefined ? {} : { identity: { name: failure.name, code: failure.failureCode } }),
|
|
1019
|
+
...(failure.message === "" ? {} : { diagnostic: failure.message }),
|
|
867
1020
|
details: failure.details,
|
|
868
1021
|
},
|
|
869
1022
|
});
|
|
@@ -18,6 +18,12 @@ import { renderPublicAkRoleCommand } from "./public-command-renderer.ts";
|
|
|
18
18
|
import { issueRoot, subjectPath } from "./work-subject-identity.ts";
|
|
19
19
|
import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
|
|
20
20
|
import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
|
|
21
|
+
import { recordTypedProviderHttpStatus } from "./typed-provider-http.ts";
|
|
22
|
+
import {
|
|
23
|
+
hasUpstreamErrorTestimony,
|
|
24
|
+
isNonSuccessHttpStatus,
|
|
25
|
+
projectConfirmedRemotePayload,
|
|
26
|
+
} from "./upstream-error-testimony.ts";
|
|
21
27
|
|
|
22
28
|
export const NAVIGATOR_EVENT_TYPE = "ak-navigator-attendance" as const;
|
|
23
29
|
export const NAVIGATOR_PREPARE_TOOL_NAME = "ak_navigator_prepare" as const;
|
|
@@ -1092,19 +1098,37 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
|
|
|
1092
1098
|
}
|
|
1093
1099
|
assignProviderFailure(navigatorProviderFailureFromStatus(status));
|
|
1094
1100
|
};
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
+
/** Project only fields actually held on the call surface — never forge upstream payload. */
|
|
1102
|
+
const projectHeldUpstream = (error: unknown): Record<string, unknown> => {
|
|
1103
|
+
if (!exactRecord(error)) return {};
|
|
1104
|
+
// Shape reading stays local; testimony + confirmed-remote payload use shared authority.
|
|
1105
|
+
const status = typeof error.statusCode === "number"
|
|
1106
|
+
? error.statusCode
|
|
1107
|
+
: typeof error.status === "number"
|
|
1108
|
+
? error.status
|
|
1109
|
+
: typeof error.httpStatus === "number"
|
|
1110
|
+
? error.httpStatus
|
|
1111
|
+
: undefined;
|
|
1112
|
+
const httpStatus = isNonSuccessHttpStatus(status) ? status : undefined;
|
|
1113
|
+
const diagnostics = Array.isArray(error.diagnostics) && error.diagnostics.length > 0
|
|
1114
|
+
? error.diagnostics
|
|
1115
|
+
: undefined;
|
|
1116
|
+
const testimony = hasUpstreamErrorTestimony({
|
|
1117
|
+
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
1118
|
+
...(diagnostics === undefined ? {} : { diagnostics }),
|
|
1119
|
+
});
|
|
1120
|
+
return {
|
|
1121
|
+
...(httpStatus === undefined ? {} : { statusCode: httpStatus, status: httpStatus }),
|
|
1122
|
+
...(diagnostics === undefined ? {} : { diagnostics }),
|
|
1123
|
+
...(testimony ? projectConfirmedRemotePayload(error) : {}),
|
|
1124
|
+
};
|
|
1101
1125
|
};
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
return
|
|
1126
|
+
/** Keep held upstream fields; strip only the local navigatorFailure classification marker. */
|
|
1127
|
+
const retainUpstreamMessage = <T extends Record<string, unknown>>(error: T): T => {
|
|
1128
|
+
if (!("navigatorFailure" in error)) return error;
|
|
1129
|
+
const copy = { ...error };
|
|
1130
|
+
delete copy.navigatorFailure;
|
|
1131
|
+
return copy;
|
|
1108
1132
|
};
|
|
1109
1133
|
const setupFailureMessage = (error: unknown) => ({
|
|
1110
1134
|
role: "assistant" as const,
|
|
@@ -1115,15 +1139,27 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
|
|
|
1115
1139
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
1116
1140
|
stopReason: "error" as const,
|
|
1117
1141
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
1118
|
-
navigatorFailureEvidenceId: retainProviderFailure(error),
|
|
1119
1142
|
timestamp: Date.now(),
|
|
1143
|
+
...projectHeldUpstream(error),
|
|
1120
1144
|
});
|
|
1145
|
+
const persistNavigatorHttpObservation = async (
|
|
1146
|
+
status: number,
|
|
1147
|
+
model: unknown,
|
|
1148
|
+
): Promise<void> => {
|
|
1149
|
+
const runDir = process.env.AK_ROLE_RUN_DIR;
|
|
1150
|
+
if (typeof runDir !== "string" || runDir.trim() === "") return;
|
|
1151
|
+
const provider = exactRecord(model) && typeof model.provider === "string" && model.provider.trim() !== ""
|
|
1152
|
+
? model.provider
|
|
1153
|
+
: undefined;
|
|
1154
|
+
if (provider === undefined) return;
|
|
1155
|
+
await recordTypedProviderHttpStatus(runDir, { httpStatus: status, provider });
|
|
1156
|
+
};
|
|
1121
1157
|
const instrumentProvider = <TProvider extends NonNullable<typeof provider>>(sourceProvider: TProvider): TProvider => {
|
|
1122
1158
|
type StreamFn = TProvider["stream"];
|
|
1123
1159
|
type StreamSimpleFn = TProvider["streamSimple"];
|
|
1124
1160
|
type StreamOptionsArg = Parameters<StreamFn>[2];
|
|
1125
1161
|
type StreamSimpleOptionsArg = Parameters<StreamSimpleFn>[2];
|
|
1126
|
-
const instrumentStreamOptions = <TOptions>(options: TOptions): TOptions => {
|
|
1162
|
+
const instrumentStreamOptions = <TOptions>(options: TOptions, observedStatus: { value?: number }): TOptions => {
|
|
1127
1163
|
const record = (exactRecord(options) ? options : {}) as Record<string, unknown>;
|
|
1128
1164
|
const previous = typeof record.onResponse === "function"
|
|
1129
1165
|
? record.onResponse as (response: { status: number; headers: Record<string, string> }, model: unknown) => void | Promise<void>
|
|
@@ -1131,12 +1167,23 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
|
|
|
1131
1167
|
return {
|
|
1132
1168
|
...record,
|
|
1133
1169
|
onResponse: async (response: { status: number; headers: Record<string, string> }, model: unknown) => {
|
|
1170
|
+
observedStatus.value = response.status;
|
|
1134
1171
|
classifyProviderResponseStatus(response.status);
|
|
1172
|
+
// Durable run-dossier sink for typed non-success HTTP (2xx clears).
|
|
1173
|
+
await persistNavigatorHttpObservation(response.status, model);
|
|
1135
1174
|
await previous?.(response, model);
|
|
1136
1175
|
},
|
|
1137
1176
|
} as TOptions;
|
|
1138
1177
|
};
|
|
1139
|
-
|
|
1178
|
+
/** Attach a directly observed non-success HTTP status when the terminal message lacks one. */
|
|
1179
|
+
const withObservedStatus = <T extends Record<string, unknown>>(message: T, observedStatus: number | undefined): T => {
|
|
1180
|
+
if (observedStatus === undefined || observedStatus >= 200 && observedStatus < 300) return message;
|
|
1181
|
+
if (typeof message.statusCode === "number" || typeof message.status === "number" || typeof message.httpStatus === "number") {
|
|
1182
|
+
return message;
|
|
1183
|
+
}
|
|
1184
|
+
return { ...message, statusCode: observedStatus, status: observedStatus };
|
|
1185
|
+
};
|
|
1186
|
+
const wrapProviderStream = (source: ReturnType<StreamFn>, observedStatus: { value?: number }): ReturnType<StreamFn> => {
|
|
1140
1187
|
const wrapped = createAssistantMessageEventStream();
|
|
1141
1188
|
void (async () => {
|
|
1142
1189
|
let result: Awaited<ReturnType<typeof source.result>> | undefined;
|
|
@@ -1147,13 +1194,14 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
|
|
|
1147
1194
|
sawTerminal = true;
|
|
1148
1195
|
if (event.type === "done" && exactRecord(event.message)) {
|
|
1149
1196
|
assignProviderFailure(navigatorProviderFailureFromDiagnostics(event.message.diagnostics));
|
|
1150
|
-
|
|
1197
|
+
// Keep held message/status/body/code/errno/diagnostics on the session surface.
|
|
1198
|
+
result = withObservedStatus(retainUpstreamMessage(event.message), observedStatus.value) as typeof event.message;
|
|
1151
1199
|
wrapped.push({ ...event, message: result });
|
|
1152
1200
|
continue;
|
|
1153
1201
|
}
|
|
1154
1202
|
if (event.type === "error" && exactRecord(event.error)) {
|
|
1155
1203
|
classifyProviderStreamError(event.error);
|
|
1156
|
-
result =
|
|
1204
|
+
result = withObservedStatus(retainUpstreamMessage(event.error), observedStatus.value) as typeof event.error;
|
|
1157
1205
|
wrapped.push({ ...event, error: result });
|
|
1158
1206
|
continue;
|
|
1159
1207
|
}
|
|
@@ -1163,24 +1211,24 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
|
|
|
1163
1211
|
// Only await result after a terminal done/error event. end(undefined) leaves result() unresolved forever.
|
|
1164
1212
|
if (sawTerminal) {
|
|
1165
1213
|
const terminal = await source.result();
|
|
1166
|
-
if (result === undefined && exactRecord(terminal))
|
|
1167
|
-
|
|
1214
|
+
if (result === undefined && exactRecord(terminal)) {
|
|
1215
|
+
result = withObservedStatus(retainUpstreamMessage(terminal), observedStatus.value) as typeof terminal;
|
|
1216
|
+
} else if (result === undefined) result = terminal;
|
|
1168
1217
|
}
|
|
1169
1218
|
} catch (error) {
|
|
1170
|
-
//
|
|
1219
|
+
// Stream throw: project held upstream fields onto the synthetic terminal message (session is durable).
|
|
1171
1220
|
classifyProviderStreamError(error);
|
|
1172
|
-
if (providerFailure === undefined) providerFailure = { source: "
|
|
1221
|
+
if (providerFailure === undefined) providerFailure = { source: "unknown", cause: "unknown" };
|
|
1173
1222
|
if (!sawTerminal) {
|
|
1174
|
-
const message = setupFailureMessage(error);
|
|
1223
|
+
const message = withObservedStatus(setupFailureMessage(error), observedStatus.value);
|
|
1175
1224
|
wrapped.push({ type: "error", reason: "error", error: message });
|
|
1176
1225
|
result = message;
|
|
1177
1226
|
sawTerminal = true;
|
|
1178
1227
|
}
|
|
1179
1228
|
} finally {
|
|
1180
|
-
// No terminal stream event
|
|
1181
|
-
// Emit a synthetic error so callers do not hang waiting for done/error, and never await an unresolved source.result().
|
|
1229
|
+
// No terminal stream event: unknown only — do not forge upstream payload.
|
|
1182
1230
|
if (!sawTerminal) {
|
|
1183
|
-
if (providerFailure === undefined) providerFailure = { source: "
|
|
1231
|
+
if (providerFailure === undefined) providerFailure = { source: "unknown", cause: "unknown" };
|
|
1184
1232
|
const message = setupFailureMessage(new Error("Navigator provider produced no response"));
|
|
1185
1233
|
wrapped.push({ type: "error", reason: "error", error: message });
|
|
1186
1234
|
result = message;
|
|
@@ -1191,15 +1239,16 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
|
|
|
1191
1239
|
})();
|
|
1192
1240
|
return wrapped as ReturnType<StreamFn>;
|
|
1193
1241
|
};
|
|
1194
|
-
const invokeInstrumentedStream = (invoke: () => ReturnType<StreamFn>): ReturnType<StreamFn> => {
|
|
1242
|
+
const invokeInstrumentedStream = (invoke: (observedStatus: { value?: number }) => ReturnType<StreamFn>): ReturnType<StreamFn> => {
|
|
1195
1243
|
// Reset before invoking the selected provider so setup throws cannot leave a prior call's fact.
|
|
1196
1244
|
providerFailure = undefined;
|
|
1245
|
+
const observedStatus: { value?: number } = {};
|
|
1197
1246
|
try {
|
|
1198
|
-
return wrapProviderStream(invoke());
|
|
1247
|
+
return wrapProviderStream(invoke(observedStatus), observedStatus);
|
|
1199
1248
|
} catch (error) {
|
|
1200
|
-
//
|
|
1249
|
+
// Setup failures become terminal synthetic errors with held upstream fields projected when present.
|
|
1201
1250
|
classifyProviderStreamError(error);
|
|
1202
|
-
if (providerFailure === undefined) providerFailure = { source: "
|
|
1251
|
+
if (providerFailure === undefined) providerFailure = { source: "unknown", cause: "unknown" };
|
|
1203
1252
|
const wrapped = createAssistantMessageEventStream();
|
|
1204
1253
|
const message = setupFailureMessage(error);
|
|
1205
1254
|
queueMicrotask(() => {
|
|
@@ -1212,12 +1261,16 @@ export function createNativeNavigatorSessionFactory(defaultModelSettingPath = na
|
|
|
1212
1261
|
return {
|
|
1213
1262
|
...sourceProvider,
|
|
1214
1263
|
stream(model: Parameters<StreamFn>[0], streamContext: Parameters<StreamFn>[1], options: StreamOptionsArg) {
|
|
1215
|
-
|
|
1216
|
-
|
|
1264
|
+
return invokeInstrumentedStream((observedStatus) => {
|
|
1265
|
+
const instrumented = instrumentStreamOptions(options, observedStatus);
|
|
1266
|
+
return sourceProvider.stream(model, streamContext, instrumented) as ReturnType<StreamFn>;
|
|
1267
|
+
}) as ReturnType<StreamFn>;
|
|
1217
1268
|
},
|
|
1218
1269
|
streamSimple(model: Parameters<StreamSimpleFn>[0], streamContext: Parameters<StreamSimpleFn>[1], options: StreamSimpleOptionsArg) {
|
|
1219
|
-
|
|
1220
|
-
|
|
1270
|
+
return invokeInstrumentedStream((observedStatus) => {
|
|
1271
|
+
const instrumented = instrumentStreamOptions(options, observedStatus);
|
|
1272
|
+
return sourceProvider.streamSimple(model, streamContext, instrumented) as ReturnType<StreamFn>;
|
|
1273
|
+
}) as ReturnType<StreamSimpleFn>;
|
|
1221
1274
|
},
|
|
1222
1275
|
} as TProvider;
|
|
1223
1276
|
};
|