@skill-harness/core 0.23.0 → 0.24.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/dist/adapters/types.d.ts +8 -2
- package/dist/capture-trace-types.d.ts +3 -0
- package/dist/execution-trace.d.ts +21 -0
- package/dist/execution-trace.js +91 -10
- package/dist/provider-failure.d.ts +4 -0
- package/dist/provider-failure.js +13 -2
- package/dist/regrade.js +4 -3
- package/dist/run.js +18 -4
- package/package.json +1 -1
package/dist/adapters/types.d.ts
CHANGED
|
@@ -78,6 +78,8 @@ export interface StructuredRun {
|
|
|
78
78
|
* answering badly. `run.ts` turns this into ERROR — never a model verdict.
|
|
79
79
|
*/
|
|
80
80
|
providerFailure?: string;
|
|
81
|
+
/** Terminal error/abort or missing settlement: unavailable outcome, never a judgeable success. */
|
|
82
|
+
executionFailure?: string;
|
|
81
83
|
}
|
|
82
84
|
export interface HarnessAdapter {
|
|
83
85
|
name: string;
|
|
@@ -104,7 +106,11 @@ export interface HarnessAdapter {
|
|
|
104
106
|
* field is to identify which CLI produced a transcript.
|
|
105
107
|
*
|
|
106
108
|
* Optional so a test double or a future adapter need not implement it; callers
|
|
107
|
-
* treat a missing method exactly like a null answer.
|
|
109
|
+
* treat a missing method exactly like a null answer. A qualification probe must
|
|
110
|
+
* use the same cwd and environment as the execution it qualifies.
|
|
108
111
|
*/
|
|
109
|
-
version?(
|
|
112
|
+
version?(options?: {
|
|
113
|
+
cwd?: string;
|
|
114
|
+
env?: NodeJS.ProcessEnv;
|
|
115
|
+
}): Promise<string | null>;
|
|
110
116
|
}
|
|
@@ -92,6 +92,7 @@ export interface TraceMetrics {
|
|
|
92
92
|
/** Date of the repository price table used for cost_usd; null when unpriced. */
|
|
93
93
|
price_as_of: string | null;
|
|
94
94
|
tool_calls: number;
|
|
95
|
+
/** Legacy field: requested Agent arguments, not verified child launches. */
|
|
95
96
|
delegated_children: number;
|
|
96
97
|
/** Maximum simultaneously outstanding tool calls observed in the pi stream. */
|
|
97
98
|
max_concurrency: number;
|
|
@@ -118,6 +119,8 @@ export interface ExecutionTraceV1 {
|
|
|
118
119
|
* never contained, changing grades on scenarios nobody edited.
|
|
119
120
|
*/
|
|
120
121
|
final_text: string;
|
|
122
|
+
/** Observed terminal state; unknown versions are unqualified, historical Pi 0.83.0 has no settlement field. */
|
|
123
|
+
final_status?: "complete" | "error" | "aborted" | "incomplete" | "truncated" | "unavailable" | "unqualified";
|
|
121
124
|
/** Every tool call in this invocation, in issue order. */
|
|
122
125
|
tool_calls: TraceToolCall[];
|
|
123
126
|
/**
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import type { ExecutionTraceV1 } from "./capture-trace-types.js";
|
|
2
2
|
import type { ModelRef, RunMode } from "./adapters/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parse pi's `--mode json` event stream into an `ExecutionTraceV1`.
|
|
5
|
+
*
|
|
6
|
+
* Pinned by the fixtures in `packages/adapters/test/fixtures/pi-json/`. Three
|
|
7
|
+
* event-shape constraints are baked in here and each one is a bug if removed:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Line-at-a-time, never buffered.** `message_update` re-sends the entire
|
|
10
|
+
* accumulated message on every delta, so the stream is quadratic in output
|
|
11
|
+
* length — a trivial three-tool-call run emitted 52 MB wrapping 12 KB of
|
|
12
|
+
* terminal events. The parser consumes an iterable of lines and holds only
|
|
13
|
+
* what it keeps.
|
|
14
|
+
* 2. **The final assistant message only.** pi's print mode — what the judge has
|
|
15
|
+
* always been shown — emits exactly that. Concatenating every assistant text
|
|
16
|
+
* block would add interim narration the transcript has never contained and
|
|
17
|
+
* move verdicts on scenarios nobody edited.
|
|
18
|
+
* 3. **Correlate tool calls by `toolCallId`.** Batched calls execute
|
|
19
|
+
* concurrently and their `end` events arrive in completion order.
|
|
20
|
+
*/
|
|
21
|
+
/** Current execution is qualified only against this exact Pi release. */
|
|
22
|
+
export declare const QUALIFIED_PI_VERSION = "1.0.4";
|
|
23
|
+
export declare function piQualificationFailure(version: string | null): string | null;
|
|
3
24
|
export interface TraceMeta {
|
|
4
25
|
piVersion: string | null;
|
|
5
26
|
subject: ModelRef;
|
package/dist/execution-trace.js
CHANGED
|
@@ -20,6 +20,14 @@ import { redactArgs, redactText } from "./redaction.js";
|
|
|
20
20
|
* 3. **Correlate tool calls by `toolCallId`.** Batched calls execute
|
|
21
21
|
* concurrently and their `end` events arrive in completion order.
|
|
22
22
|
*/
|
|
23
|
+
/** Current execution is qualified only against this exact Pi release. */
|
|
24
|
+
export const QUALIFIED_PI_VERSION = "1.0.4";
|
|
25
|
+
/** Historical raw captures from this version remain readable; this is not fresh-run qualification. */
|
|
26
|
+
const LEGACY_PI_TRACE_VERSION = "0.83.0";
|
|
27
|
+
export function piQualificationFailure(version) {
|
|
28
|
+
return version === QUALIFIED_PI_VERSION ? null
|
|
29
|
+
: `Pi ${version ?? "(version unavailable)"} is unqualified; execution requires exact Pi ${QUALIFIED_PI_VERSION}`;
|
|
30
|
+
}
|
|
23
31
|
/** Events that carry no information a trace keeps, and are large. */
|
|
24
32
|
const SKIPPED = new Set(["message_update", "tool_execution_update"]);
|
|
25
33
|
/** `details` larger than this is dropped rather than persisted. */
|
|
@@ -40,6 +48,13 @@ export function parseTrace(lines, meta) {
|
|
|
40
48
|
let completionCounter = 0;
|
|
41
49
|
let malformedLines = 0;
|
|
42
50
|
let sawTerminal = false;
|
|
51
|
+
const settledContract = meta.piVersion !== LEGACY_PI_TRACE_VERSION;
|
|
52
|
+
const qualificationFailure = settledContract ? piQualificationFailure(meta.piVersion) : null;
|
|
53
|
+
let sawSettled = false;
|
|
54
|
+
let lastStopReason;
|
|
55
|
+
let currentAssistantText = "";
|
|
56
|
+
let eligibleFinal = false;
|
|
57
|
+
const captureErrors = [];
|
|
43
58
|
let finalText = "";
|
|
44
59
|
let lastAssistantText = "";
|
|
45
60
|
let inputTokens = null;
|
|
@@ -60,13 +75,35 @@ export function parseTrace(lines, meta) {
|
|
|
60
75
|
malformedLines++;
|
|
61
76
|
continue;
|
|
62
77
|
}
|
|
78
|
+
if (!ev || typeof ev !== "object" || Array.isArray(ev)) {
|
|
79
|
+
malformedLines++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
63
82
|
const type = ev.type;
|
|
64
|
-
if (typeof type !== "string"
|
|
83
|
+
if (typeof type !== "string") {
|
|
84
|
+
malformedLines++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (settledContract && ["agent_start", "turn_start", "message_start", "message_update", "message_end", "tool_execution_start", "tool_execution_end"].includes(type)) {
|
|
88
|
+
sawSettled = false;
|
|
89
|
+
if (type !== "tool_execution_end") {
|
|
90
|
+
currentAssistantText = "";
|
|
91
|
+
lastStopReason = undefined;
|
|
92
|
+
eligibleFinal = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (SKIPPED.has(type))
|
|
65
96
|
continue;
|
|
66
97
|
if (type === "tool_execution_start") {
|
|
67
98
|
const id = str(ev.toolCallId);
|
|
68
|
-
if (!id)
|
|
99
|
+
if (!id) {
|
|
100
|
+
captureErrors.push("tool start without an identity");
|
|
69
101
|
continue;
|
|
102
|
+
}
|
|
103
|
+
if (calls.has(id)) {
|
|
104
|
+
captureErrors.push("duplicate tool start identity");
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
70
107
|
calls.set(id, {
|
|
71
108
|
id,
|
|
72
109
|
name: str(ev.toolName) ?? "(unknown)",
|
|
@@ -83,11 +120,19 @@ export function parseTrace(lines, meta) {
|
|
|
83
120
|
}
|
|
84
121
|
if (type === "tool_execution_end") {
|
|
85
122
|
const id = str(ev.toolCallId);
|
|
86
|
-
if (!id)
|
|
123
|
+
if (!id) {
|
|
124
|
+
captureErrors.push("tool end without an identity");
|
|
87
125
|
continue;
|
|
126
|
+
}
|
|
88
127
|
const call = calls.get(id);
|
|
89
|
-
if (!call)
|
|
90
|
-
|
|
128
|
+
if (!call) {
|
|
129
|
+
captureErrors.push("tool end without a matching start");
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (call.completionIndex >= 0) {
|
|
133
|
+
captureErrors.push("duplicate tool completion identity");
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
91
136
|
if (call.completionIndex < 0)
|
|
92
137
|
activeCalls = Math.max(0, activeCalls - 1);
|
|
93
138
|
call.completionIndex = completionCounter++;
|
|
@@ -100,7 +145,20 @@ export function parseTrace(lines, meta) {
|
|
|
100
145
|
if (type === "message_end") {
|
|
101
146
|
sawTerminal = true;
|
|
102
147
|
const msg = ev.message;
|
|
103
|
-
|
|
148
|
+
if (!msg || typeof msg !== "object" || Array.isArray(msg) || typeof msg.role !== "string") {
|
|
149
|
+
captureErrors.push("malformed message_end message");
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// Pi also emits system/user messages; a system message has string content.
|
|
153
|
+
// They invalidate a previous final but are not assistant/tool-result payloads.
|
|
154
|
+
if (msg.role !== "assistant" && msg.role !== "toolResult")
|
|
155
|
+
continue;
|
|
156
|
+
if (!Array.isArray(msg.content) ||
|
|
157
|
+
msg.content.some((block) => !block || typeof block !== "object" || Array.isArray(block) || typeof block.type !== "string" || (block.type === "text" && typeof block.text !== "string"))) {
|
|
158
|
+
captureErrors.push("malformed message_end content");
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const at = isoTime(msg.timestamp);
|
|
104
162
|
if (msg?.role === "assistant" && at) {
|
|
105
163
|
for (const block of msg.content ?? []) {
|
|
106
164
|
if (block.type !== "toolCall" || typeof block.id !== "string")
|
|
@@ -120,6 +178,9 @@ export function parseTrace(lines, meta) {
|
|
|
120
178
|
if (msg?.role !== "assistant")
|
|
121
179
|
continue;
|
|
122
180
|
const text = assistantText(msg);
|
|
181
|
+
eligibleFinal = msg.content.every((block) => block.type === "text" || block.type === "thinking");
|
|
182
|
+
currentAssistantText = text;
|
|
183
|
+
lastStopReason = msg.stopReason;
|
|
123
184
|
if (text) {
|
|
124
185
|
lastAssistantText = text;
|
|
125
186
|
// `stop` marks the model's closing message; a `toolUse` message is
|
|
@@ -135,6 +196,8 @@ export function parseTrace(lines, meta) {
|
|
|
135
196
|
}
|
|
136
197
|
if (type === "turn_end" || type === "agent_end" || type === "agent_settled") {
|
|
137
198
|
sawTerminal = true;
|
|
199
|
+
if (type === "agent_settled")
|
|
200
|
+
sawSettled = true;
|
|
138
201
|
// Deliberately read NOTHING from these. They repeat the same assistant
|
|
139
202
|
// messages `message_end` already carried (`turn_end` and `agent_end` do;
|
|
140
203
|
// `agent_settled` carries no keys at all beyond `type`), and reading two
|
|
@@ -143,6 +206,23 @@ export function parseTrace(lines, meta) {
|
|
|
143
206
|
}
|
|
144
207
|
}
|
|
145
208
|
const toolCalls = [...calls.values()].sort((a, b) => a.issueIndex - b.issueIndex);
|
|
209
|
+
if (malformedLines > 0)
|
|
210
|
+
captureErrors.push(`pi JSONL contained ${malformedLines} malformed line(s); trace evidence is incomplete`);
|
|
211
|
+
const finalStatus = qualificationFailure ? "unqualified"
|
|
212
|
+
: !sawSettled || activeCalls > 0 ? "incomplete"
|
|
213
|
+
: captureErrors.length > 0 ? "unavailable"
|
|
214
|
+
: lastStopReason === "error" ? "error"
|
|
215
|
+
: lastStopReason === "aborted" ? "aborted"
|
|
216
|
+
: lastStopReason === "length" ? "truncated"
|
|
217
|
+
: lastStopReason === "stop" && eligibleFinal && currentAssistantText.length > 0 ? "complete" : "unavailable";
|
|
218
|
+
if (settledContract && finalStatus !== "complete") {
|
|
219
|
+
const reason = qualificationFailure
|
|
220
|
+
?? (finalStatus === "incomplete" ? "current settlement is missing or tool calls remain outstanding"
|
|
221
|
+
: finalStatus === "truncated" ? "settled assistant reached its output length limit"
|
|
222
|
+
: finalStatus === "error" || finalStatus === "aborted" ? `settled assistant stop reason is ${lastStopReason}`
|
|
223
|
+
: "current terminal assistant content is empty, ineligible, or captured evidence is malformed");
|
|
224
|
+
captureErrors.push(`Pi ${meta.piVersion ?? "(version unavailable)"} final delivery is ${finalStatus}; ${reason}`);
|
|
225
|
+
}
|
|
146
226
|
const metrics = {
|
|
147
227
|
input_tokens: inputTokens,
|
|
148
228
|
output_tokens: outputTokens,
|
|
@@ -172,7 +252,9 @@ export function parseTrace(lines, meta) {
|
|
|
172
252
|
// and `smoke-real-pi.sh` asserts no `/home/` survives into a persisted trace
|
|
173
253
|
// — an assertion that used to pass only because the smoke model happened not
|
|
174
254
|
// to echo one.
|
|
175
|
-
final_text: redactText(finalText || lastAssistantText, meta.homeDir),
|
|
255
|
+
final_text: redactText(settledContract ? currentAssistantText : finalText || lastAssistantText, meta.homeDir),
|
|
256
|
+
...(settledContract ? { final_status: finalStatus } : {}),
|
|
257
|
+
...(captureErrors.length ? { capture_errors: [...new Set(captureErrors)] } : {}),
|
|
176
258
|
tool_calls: toolCalls,
|
|
177
259
|
// `null`, not `[]`: the stream says nothing about the filesystem. The runner
|
|
178
260
|
// overwrites this after observing the workspace. Defaulting to `[]` claimed
|
|
@@ -184,7 +266,7 @@ export function parseTrace(lines, meta) {
|
|
|
184
266
|
metrics,
|
|
185
267
|
};
|
|
186
268
|
trace.trace_sha256 = traceSha256(trace);
|
|
187
|
-
return { trace, isComplete: sawTerminal, malformedLines };
|
|
269
|
+
return { trace, isComplete: settledContract ? !qualificationFailure && sawSettled && activeCalls === 0 : sawTerminal, malformedLines };
|
|
188
270
|
}
|
|
189
271
|
function addReported(current, value) {
|
|
190
272
|
return typeof value === "number" && value > 0 ? (current ?? 0) + value : current;
|
|
@@ -194,8 +276,7 @@ function assistantText(msg) {
|
|
|
194
276
|
return (msg.content ?? [])
|
|
195
277
|
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
196
278
|
.map((b) => b.text)
|
|
197
|
-
.join("
|
|
198
|
-
.trim();
|
|
279
|
+
.join("");
|
|
199
280
|
}
|
|
200
281
|
/**
|
|
201
282
|
* Bounded metadata about a tool result. The body is never kept: results carry
|
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
* convention cannot drift apart.
|
|
32
32
|
*/
|
|
33
33
|
export declare const PROVIDER_FAILURE_MARKER = "[skill-harness] provider failure:";
|
|
34
|
+
/** Failed or unproven terminal delivery, without assuming a provider-side cause. */
|
|
35
|
+
export declare const EXECUTION_FAILURE_MARKER = "[skill-harness] execution failure:";
|
|
36
|
+
export declare function withExecutionFailure(transcript: string, failure: string | null): string;
|
|
37
|
+
export declare function executionFailureFromTranscript(transcript: string): string | null;
|
|
34
38
|
/**
|
|
35
39
|
* Prefix `transcript` with the provider-failure marker, or return it unchanged.
|
|
36
40
|
*
|
package/dist/provider-failure.js
CHANGED
|
@@ -31,6 +31,14 @@
|
|
|
31
31
|
* convention cannot drift apart.
|
|
32
32
|
*/
|
|
33
33
|
export const PROVIDER_FAILURE_MARKER = "[skill-harness] provider failure:";
|
|
34
|
+
/** Failed or unproven terminal delivery, without assuming a provider-side cause. */
|
|
35
|
+
export const EXECUTION_FAILURE_MARKER = "[skill-harness] execution failure:";
|
|
36
|
+
export function withExecutionFailure(transcript, failure) {
|
|
37
|
+
return failure ? `${EXECUTION_FAILURE_MARKER} ${failure}\n\n${transcript}` : transcript;
|
|
38
|
+
}
|
|
39
|
+
export function executionFailureFromTranscript(transcript) {
|
|
40
|
+
return failureFromPreamble(transcript, EXECUTION_FAILURE_MARKER);
|
|
41
|
+
}
|
|
34
42
|
/**
|
|
35
43
|
* The adapters' shared turn-header prefix (`>>> USER:` / `>>> USER (turn n/m):`),
|
|
36
44
|
* which is where the preamble ends and model-influenced content begins.
|
|
@@ -87,11 +95,14 @@ export function providerFailureFromJsonLine(line) {
|
|
|
87
95
|
* provider failed — it is evidence the model typed the words.
|
|
88
96
|
*/
|
|
89
97
|
export function providerFailureFromTranscript(transcript) {
|
|
98
|
+
return failureFromPreamble(transcript, PROVIDER_FAILURE_MARKER);
|
|
99
|
+
}
|
|
100
|
+
function failureFromPreamble(transcript, marker) {
|
|
90
101
|
for (const line of transcript.split("\n")) {
|
|
91
102
|
if (line.startsWith(TURN_HEADER_PREFIX))
|
|
92
103
|
return null;
|
|
93
|
-
if (line.startsWith(
|
|
94
|
-
return line.slice(
|
|
104
|
+
if (line.startsWith(marker))
|
|
105
|
+
return line.slice(marker.length).trim();
|
|
95
106
|
}
|
|
96
107
|
return null;
|
|
97
108
|
}
|
package/dist/regrade.js
CHANGED
|
@@ -5,7 +5,7 @@ import { findTranscriptFiles, judgeRawPath, repIndexOf, readResults, writeResult
|
|
|
5
5
|
import { outcomesToResult, normalizeRepOutcome } from "./reps.js";
|
|
6
6
|
import { appendJournal } from "./journal.js";
|
|
7
7
|
import { rubricDigest, personaDigest, RUBRIC_PREFIX, PERSONA_KEY } from "./sources.js";
|
|
8
|
-
import { providerFailureFromTranscript } from "./provider-failure.js";
|
|
8
|
+
import { providerFailureFromTranscript, executionFailureFromTranscript } from "./provider-failure.js";
|
|
9
9
|
/**
|
|
10
10
|
* Carry a run's recorded hashes forward, refreshing only the `rubric:` keys this
|
|
11
11
|
* re-grade actually judged under (plus the persona, which applies to all of them).
|
|
@@ -47,8 +47,9 @@ export async function judgeOneRep(opts) {
|
|
|
47
47
|
// into a model verdict, spending a judge call to turn infrastructure noise into
|
|
48
48
|
// a false FAIL.
|
|
49
49
|
const providerFailure = providerFailureFromTranscript(transcript);
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const executionFailure = executionFailureFromTranscript(transcript);
|
|
51
|
+
if (providerFailure || executionFailure) {
|
|
52
|
+
const reason = providerFailure ? `provider failure — ${providerFailure}` : `execution failure — ${executionFailure}`;
|
|
52
53
|
appendJournal(runDir, { event: "judge-verdict", ts: now(), id: scenario.id, verdict: "ERROR", reason, suspect: false, ...repField });
|
|
53
54
|
return {
|
|
54
55
|
verdict: "ERROR", reason, suspect: false,
|
package/dist/run.js
CHANGED
|
@@ -16,7 +16,7 @@ import { judgeOneRep } from "./regrade.js";
|
|
|
16
16
|
import { runDeliveryCanary, canaryFailure } from "./canary.js";
|
|
17
17
|
import { boundaryCells, stabilityNote } from "./stability.js";
|
|
18
18
|
import { aggregateMetrics } from "./metrics.js";
|
|
19
|
-
import { providerFailureFromTranscript } from "./provider-failure.js";
|
|
19
|
+
import { providerFailureFromTranscript, executionFailureFromTranscript } from "./provider-failure.js";
|
|
20
20
|
/**
|
|
21
21
|
* The pi-daddy arm's delivery proof, by convention: `arms.yaml` points
|
|
22
22
|
* `PI_GRANTS_LEDGER` at exactly this filename inside the run directory. A fixed
|
|
@@ -303,6 +303,7 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
303
303
|
// and the artifact disagreeing, and a recovered measurement thrown away.
|
|
304
304
|
infrastructureFailure = null;
|
|
305
305
|
adapterFailure = null;
|
|
306
|
+
let executionUnavailable = false;
|
|
306
307
|
try {
|
|
307
308
|
if (scenario.mode === "seeded") {
|
|
308
309
|
const r = await runSeeded(scenario, {
|
|
@@ -348,8 +349,11 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
348
349
|
const structured = await ctx.adapter.runStructured({ ...req, scenarioId: scenario.id, rep });
|
|
349
350
|
transcript = structured.transcript;
|
|
350
351
|
traces = structured.traces;
|
|
352
|
+
executionUnavailable = Boolean(structured.executionFailure);
|
|
351
353
|
if (structured.providerFailure)
|
|
352
354
|
infrastructureFailure = `provider failure — ${structured.providerFailure}`;
|
|
355
|
+
else if (structured.executionFailure)
|
|
356
|
+
infrastructureFailure = `execution failure — ${structured.executionFailure}`;
|
|
353
357
|
}
|
|
354
358
|
else {
|
|
355
359
|
transcript = await ctx.adapter.run(req);
|
|
@@ -384,9 +388,19 @@ async function runRep(scenario, rep, repCount, ctx) {
|
|
|
384
388
|
const provider = providerFailureFromTranscript(transcript);
|
|
385
389
|
if (provider)
|
|
386
390
|
infrastructureFailure = `provider failure — ${provider}`;
|
|
391
|
+
else {
|
|
392
|
+
const execution = executionFailureFromTranscript(transcript);
|
|
393
|
+
if (execution)
|
|
394
|
+
infrastructureFailure = `execution failure — ${execution}`;
|
|
395
|
+
}
|
|
387
396
|
}
|
|
388
|
-
|
|
389
|
-
if
|
|
397
|
+
executionUnavailable ||= executionFailureFromTranscript(transcript) !== null;
|
|
398
|
+
// Qualified complete traces prove delivered bytes even if those bytes
|
|
399
|
+
// are only whitespace; the legacy text-only timeout heuristic cannot erase them.
|
|
400
|
+
const deliveredText = traces.length > 0 && traces.every((trace) => trace.final_status === "complete" && trace.final_text.length > 0 && !trace.capture_errors?.length);
|
|
401
|
+
noResponse = !deliveredText && hasEmptyAssistantTurn(transcript);
|
|
402
|
+
// Known unavailable execution is not an empty model answer to retry.
|
|
403
|
+
if (executionUnavailable || (!noResponse && !adapterFailure))
|
|
390
404
|
break;
|
|
391
405
|
}
|
|
392
406
|
// Survived the retry: ERROR for this rep, inside a run that still completes
|
|
@@ -578,7 +592,7 @@ export function formatScorecard(summary, lift, stability) {
|
|
|
578
592
|
const tokens = metrics.input_tokens === null
|
|
579
593
|
? "subject tokens unavailable"
|
|
580
594
|
: `subject tokens ${metrics.input_tokens} in / ${metrics.output_tokens ?? 0} out / ${metrics.cache_read_tokens ?? 0} cache-read`;
|
|
581
|
-
const tools = metrics.tool_calls === null ? "tool calls unavailable" : `${metrics.tool_calls} tool call(s), ${metrics.delegated_children ?? 0}
|
|
595
|
+
const tools = metrics.tool_calls === null ? "tool calls unavailable" : `${metrics.tool_calls} tool call(s), ${metrics.delegated_children ?? 0} requested Agent task(s), max outstanding tool calls ${metrics.max_concurrency ?? 0}; child launches/concurrency unobserved`;
|
|
582
596
|
const cost = metrics.cost_source === "subscription"
|
|
583
597
|
? " · subscription ($0 marginal cost recorded)"
|
|
584
598
|
: metrics.cost_source === "unreported"
|