@abelxiaoxing/cadence 1.0.8 → 1.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abelxiaoxing/cadence",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Prompts, skills, and a private orchestration extension exposing the Abel four-stage workflow (/abel-init, /abel-design, /abel-implement, /abel-diagnose) with package-owned professional Agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,8 +53,8 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "@biomejs/biome": "^2.4.16",
56
- "@earendil-works/pi-coding-agent": "0.84.1",
57
- "@earendil-works/pi-tui": "0.84.2",
56
+ "@earendil-works/pi-coding-agent": "0.84.3",
57
+ "@earendil-works/pi-tui": "0.84.3",
58
58
  "@types/node": "^22.15.3",
59
59
  "rumdl": "^0.2.10",
60
60
  "typescript": "^6.0.3",
@@ -157,6 +157,39 @@ function isSingleStructuralSubmit(message: AssistantMessage | undefined) {
157
157
  );
158
158
  }
159
159
 
160
+ function safeChildError(failure: ChildFailure): string {
161
+ switch (failure.kind) {
162
+ case "environment":
163
+ return failure.code === "child-session-create-failed"
164
+ ? "child session creation failed"
165
+ : "child environment unavailable";
166
+ case "transport":
167
+ switch (failure.code) {
168
+ case "child-timeout":
169
+ case "timeout":
170
+ return "child phase timeout";
171
+ case "child-no-final-assistant":
172
+ return "child produced no final assistant";
173
+ case "child-provider-stream-aborted":
174
+ return "child provider stream aborted";
175
+ case "child-provider-stream-error":
176
+ case "transport-failure":
177
+ return "child provider stream failed";
178
+ }
179
+ return "child provider stream failed";
180
+ case "artifact":
181
+ return failure.code === "child-no-structural-submit"
182
+ ? "child produced no structural submission"
183
+ : "child structural submission is invalid";
184
+ case "cancelled":
185
+ return "child phase cancelled";
186
+ case "stale":
187
+ case "approval-boundary":
188
+ case "result-limit":
189
+ return "child result rejected";
190
+ }
191
+ }
192
+
160
193
  export type ChildSessionResult =
161
194
  | {
162
195
  ok: true;
@@ -192,6 +225,7 @@ export async function runChildSession(input: {
192
225
  allowedPaths?: string[];
193
226
  timeoutMs: number;
194
227
  signal?: AbortSignal;
228
+ failureOverride?: () => ChildFailure | undefined;
195
229
  }): Promise<ChildSessionResult> {
196
230
  const submit = createSubmitTool({
197
231
  requestId: input.requestId,
@@ -255,23 +289,46 @@ export async function runChildSession(input: {
255
289
  identity: submit.getIdentity(),
256
290
  };
257
291
  };
258
- const transportFailed = (): boolean => {
292
+ const transportFailure = ():
293
+ | Extract<ChildFailure, { kind: "transport" }>
294
+ | undefined => {
259
295
  const final = session?.messages
260
296
  .filter((message) => message.role === "assistant")
261
297
  .at(-1);
262
- if (final === undefined) return true;
298
+ if (final === undefined) {
299
+ return {
300
+ kind: "transport",
301
+ code: "child-no-final-assistant",
302
+ stage: "child-finalization",
303
+ };
304
+ }
263
305
  if (final.stopReason === "error") {
264
- return (
265
- submit.getAttempts() === 0 &&
306
+ return submit.getAttempts() === 0 &&
266
307
  !final.content.some(
267
308
  (content) =>
268
309
  content.type === "toolCall" &&
269
310
  content.name === "abel_submit_result",
270
311
  )
271
- );
312
+ ? {
313
+ kind: "transport",
314
+ code: "child-provider-stream-error",
315
+ stage: "child-provider-stream",
316
+ }
317
+ : undefined;
272
318
  }
273
- return final.stopReason === "aborted";
319
+ if (final.stopReason === "aborted") {
320
+ return {
321
+ kind: "transport",
322
+ code: "child-provider-stream-aborted",
323
+ stage: "child-provider-stream",
324
+ };
325
+ }
326
+ return undefined;
274
327
  };
328
+ const noStructuralSubmit = (): ChildFailure =>
329
+ submit.getAttempts() === 0
330
+ ? { kind: "artifact", code: "child-no-structural-submit" }
331
+ : { kind: "artifact", code: "invalid-structural-result" };
275
332
  try {
276
333
  abort.signal.throwIfAborted();
277
334
  const creation = createAgentSession({
@@ -331,22 +388,19 @@ export async function runChildSession(input: {
331
388
  const attempts = submit.getAttempts();
332
389
  const classification = classifySession();
333
390
  if (!result || attempts !== 1) {
334
- const transportFailure = transportFailed();
335
391
  const failure =
336
392
  submit.getFailure() ??
337
- (transportFailure
338
- ? ({ kind: "transport", code: "transport-failure" } as const)
339
- : ({
340
- kind: "artifact",
341
- code: "invalid-structural-result",
342
- } as const));
393
+ input.failureOverride?.() ??
394
+ transportFailure() ??
395
+ noStructuralSubmit();
396
+ const isTransport = failure.kind === "transport";
343
397
  disposeOnce();
344
398
  return {
345
399
  ok: false,
346
- error: "child did not retain exactly one structural submission",
400
+ error: safeChildError(failure),
347
401
  failure,
348
402
  failureKind: "failed",
349
- transportFailure,
403
+ transportFailure: isTransport,
350
404
  disposeCount,
351
405
  usage: usage.total(),
352
406
  classification,
@@ -355,11 +409,15 @@ export async function runChildSession(input: {
355
409
  const assistants = session.messages.filter((m) => m.role === "assistant");
356
410
  const final = assistants.at(-1);
357
411
  if (!isSingleStructuralSubmit(final)) {
412
+ const failure = {
413
+ kind: "artifact",
414
+ code: "invalid-structural-result",
415
+ } as const;
358
416
  disposeOnce();
359
417
  return {
360
418
  ok: false,
361
- error: "final assistant message is not one structural submit",
362
- failure: { kind: "artifact", code: "invalid-structural-result" },
419
+ error: safeChildError(failure),
420
+ failure,
363
421
  failureKind: "failed",
364
422
  transportFailure: false,
365
423
  disposeCount,
@@ -390,22 +448,35 @@ export async function runChildSession(input: {
390
448
  ? "cancelled"
391
449
  : "failed";
392
450
  const classification = classifySession();
393
- const transportFailure = transportFailed();
394
451
  const failure: ChildFailure = timedOut
395
- ? { kind: "transport", code: "timeout" }
452
+ ? {
453
+ kind: "transport",
454
+ code: "child-timeout",
455
+ stage: "child-timeout",
456
+ }
396
457
  : failureKind === "cancelled"
397
458
  ? { kind: "cancelled", code: "cancelled" }
398
- : (submit.getFailure() ??
399
- (transportFailure
400
- ? { kind: "transport", code: "transport-failure" }
401
- : { kind: "artifact", code: "invalid-structural-result" }));
459
+ : session === undefined
460
+ ? {
461
+ kind: "environment",
462
+ code: "child-session-create-failed",
463
+ stage: "child-session-create",
464
+ }
465
+ : (submit.getFailure() ??
466
+ input.failureOverride?.() ??
467
+ transportFailure() ??
468
+ noStructuralSubmit());
469
+ const isTransport = failure.kind === "transport";
402
470
  disposeOnce();
403
471
  return {
404
472
  ok: false,
405
- error: (error as Error).message,
473
+ error:
474
+ failure.kind === "cancelled" && error instanceof Error
475
+ ? error.message
476
+ : safeChildError(failure),
406
477
  failure,
407
478
  failureKind,
408
- transportFailure,
479
+ transportFailure: isTransport,
409
480
  disposeCount,
410
481
  usage: usage.total(),
411
482
  classification,
package/src/contracts.ts CHANGED
@@ -74,6 +74,7 @@ export const ARTIFACT_FAILURE_CODES = [
74
74
  "invalid-diff-bytes",
75
75
  "invalid-snapshot",
76
76
  "invalid-structural-result",
77
+ "child-no-structural-submit",
77
78
  "invalid-verification-contract",
78
79
  "lockfile-bound-mismatch",
79
80
  "noncanonical-path",
@@ -108,6 +109,7 @@ export const ENVIRONMENT_FAILURE_CODES = [
108
109
  "checkout-failed",
109
110
  "clone-alternates",
110
111
  "clone-failed",
112
+ "child-session-create-failed",
111
113
  "dependency-path-unsafe",
112
114
  "git-apply-check-unavailable",
113
115
  "git-apply-failed",
@@ -115,6 +117,11 @@ export const ENVIRONMENT_FAILURE_CODES = [
115
117
  "git-ignore-check-failed",
116
118
  "git-index-unavailable",
117
119
  "invalid-subagent-endpoint",
120
+ "parent-bridge-capture-not-ready",
121
+ "parent-bridge-generation-invalidated",
122
+ "parent-bridge-model-key-mismatch",
123
+ "parent-bridge-provider-not-installed",
124
+ "parent-bridge-session-unavailable",
118
125
  "root-unavailable",
119
126
  "sandbox-runtime-unavailable",
120
127
  ] as const;
@@ -126,18 +133,39 @@ export const APPROVAL_BOUNDARY_CODES = [
126
133
  "verification-contract-insufficient",
127
134
  "agents-contract-insufficient",
128
135
  ] as const;
129
- export const CHILD_TRANSPORT_CODES = ["timeout", "transport-failure"] as const;
136
+ export const CHILD_TRANSPORT_CODES = [
137
+ "child-no-final-assistant",
138
+ "child-provider-stream-aborted",
139
+ "child-provider-stream-error",
140
+ "child-timeout",
141
+ "timeout",
142
+ "transport-failure",
143
+ ] as const;
144
+ export const FAILURE_STAGES = [
145
+ "phase-runtime",
146
+ "child-session-create",
147
+ "child-provider-stream",
148
+ "child-finalization",
149
+ "child-timeout",
150
+ ] as const;
130
151
 
131
152
  export type ArtifactFailureCode = (typeof ARTIFACT_FAILURE_CODES)[number];
132
153
  export type StaleFailureCode = (typeof STALE_FAILURE_CODES)[number];
133
154
  export type EnvironmentFailureCode = (typeof ENVIRONMENT_FAILURE_CODES)[number];
134
155
  export type ApprovalBoundaryCode = (typeof APPROVAL_BOUNDARY_CODES)[number];
135
156
  export type ChildTransportCode = (typeof CHILD_TRANSPORT_CODES)[number];
157
+ export type FailureStage = (typeof FAILURE_STAGES)[number];
158
+
159
+ export interface SafeFailureDiagnostic {
160
+ code: ChildTransportCode;
161
+ stage: FailureStage;
162
+ }
136
163
 
137
164
  export type EnvironmentFailure = {
138
165
  kind: "environment";
139
166
  code: EnvironmentFailureCode;
140
167
  message?: string;
168
+ stage?: FailureStage;
141
169
  };
142
170
 
143
171
  export type CandidateFailure =
@@ -150,7 +178,11 @@ export type CandidateFailure =
150
178
 
151
179
  export type ChildFailure =
152
180
  | CandidateFailure
153
- | { kind: "transport"; code: ChildTransportCode };
181
+ | {
182
+ kind: "transport";
183
+ code: ChildTransportCode;
184
+ stage?: FailureStage;
185
+ };
154
186
 
155
187
  export type CandidateRejection =
156
188
  | {
@@ -165,6 +197,7 @@ export type TaskFailure =
165
197
  | {
166
198
  kind: "attempts-exhausted";
167
199
  cause: "artifact" | "stale" | "transport";
200
+ lastFailure?: SafeFailureDiagnostic;
168
201
  }
169
202
  | {
170
203
  kind: "checkpoint-attempts-exhausted";
@@ -27,6 +27,23 @@ export interface ParentPayloadCapture {
27
27
  readonly onPayload: ParentPayloadCallback;
28
28
  }
29
29
 
30
+ export type ParentBridgeFailureCode =
31
+ | "parent-bridge-session-unavailable"
32
+ | "parent-bridge-provider-not-installed"
33
+ | "parent-bridge-capture-not-ready"
34
+ | "parent-bridge-model-key-mismatch"
35
+ | "parent-bridge-generation-invalidated";
36
+
37
+ export class ParentPayloadBridgeError extends Error {
38
+ readonly code: ParentBridgeFailureCode;
39
+
40
+ constructor(code: ParentBridgeFailureCode) {
41
+ super(code);
42
+ this.name = "ParentPayloadBridgeError";
43
+ this.code = code;
44
+ }
45
+ }
46
+
30
47
  export interface ParentProviderRegistry {
31
48
  getProvider(provider: string): Provider | undefined;
32
49
  registerProvider(provider: Provider): void;
@@ -217,6 +234,32 @@ export class ParentPayloadBridge {
217
234
  return capture;
218
235
  }
219
236
 
237
+ diagnoseCapture(
238
+ modelKey: ParentModelKey,
239
+ registry: Pick<ParentProviderRegistry, "getProvider">,
240
+ ): ParentBridgeFailureCode | undefined {
241
+ const state = this.state;
242
+ if (!state?.active) return "parent-bridge-session-unavailable";
243
+ const current = registry.getProvider(modelKey.provider);
244
+ if (!current || !this.isWrappedProvider(current)) {
245
+ return "parent-bridge-provider-not-installed";
246
+ }
247
+ const capture = state.ready.get(modelKeyId(modelKey));
248
+ if (capture && sameModelKey(capture.modelKey, modelKey)) {
249
+ return this.unwrapProvider(current) === capture.delegate
250
+ ? undefined
251
+ : "parent-bridge-provider-not-installed";
252
+ }
253
+ if (
254
+ [...state.ready.values()].some(
255
+ (ready) => ready.modelKey.provider === modelKey.provider,
256
+ )
257
+ ) {
258
+ return "parent-bridge-model-key-mismatch";
259
+ }
260
+ return "parent-bridge-capture-not-ready";
261
+ }
262
+
220
263
  wrapProvider(delegate: Provider): Provider {
221
264
  const original = this.unwrapProvider(delegate);
222
265
  const stream = (
@@ -265,7 +308,9 @@ export class ParentPayloadBridge {
265
308
  childOnPayload?: ParentPayloadCallback,
266
309
  signal?: AbortSignal,
267
310
  ): Promise<Record<string, unknown>> {
268
- if (!capture) throw new Error("parent payload bridge is unavailable");
311
+ if (!capture) {
312
+ throw new ParentPayloadBridgeError("parent-bridge-capture-not-ready");
313
+ }
269
314
  const requestedKey = modelKeyFor(model);
270
315
  const state = this.requireCurrentCapture(capture, requestedKey);
271
316
  signal?.throwIfAborted();
@@ -360,13 +405,13 @@ export class ParentPayloadBridge {
360
405
  model: Model<string>,
361
406
  provisional: ProvisionalCapture | undefined,
362
407
  ): Promise<void> {
363
- let successful = false;
364
408
  try {
365
409
  for await (const event of source) {
366
- if (event.type === "done") successful = true;
410
+ if (event.type === "done") {
411
+ if (provisional) this.commitCapture(provisional);
412
+ }
367
413
  output.push(event);
368
414
  }
369
- if (successful && provisional) this.commitCapture(provisional);
370
415
  } catch {
371
416
  const error = streamFailure(model);
372
417
  output.push({ type: "error", reason: "error", error });
@@ -401,20 +446,32 @@ export class ParentPayloadBridge {
401
446
  modelKey: ParentModelKey,
402
447
  ): GenerationState {
403
448
  const state = this.state;
449
+ if (!capture) {
450
+ throw new ParentPayloadBridgeError("parent-bridge-capture-not-ready");
451
+ }
404
452
  if (
405
- !capture ||
406
453
  !state?.active ||
407
454
  this.captureStates.get(capture) !== state ||
408
455
  capture.generation !== state.generation ||
409
- capture.sessionId !== state.sessionId ||
410
- !sameModelKey(capture.modelKey, modelKey) ||
411
- state.ready.get(modelKeyId(modelKey)) !== capture
456
+ capture.sessionId !== state.sessionId
412
457
  ) {
413
- throw new Error("parent payload bridge is unavailable");
458
+ throw new ParentPayloadBridgeError(
459
+ "parent-bridge-generation-invalidated",
460
+ );
461
+ }
462
+ if (!sameModelKey(capture.modelKey, modelKey)) {
463
+ throw new ParentPayloadBridgeError("parent-bridge-model-key-mismatch");
464
+ }
465
+ if (state.ready.get(modelKeyId(modelKey)) !== capture) {
466
+ throw new ParentPayloadBridgeError("parent-bridge-capture-not-ready");
414
467
  }
415
468
  return state;
416
469
  }
417
470
 
471
+ private isWrappedProvider(provider: Provider): boolean {
472
+ return Reflect.get(provider, PROVIDER_WRAPPER) !== undefined;
473
+ }
474
+
418
475
  private unwrapProvider(provider: Provider): Provider {
419
476
  let current = provider;
420
477
  const seen = new Set<Provider>();
@@ -14,10 +14,11 @@ import {
14
14
  ModelRuntime,
15
15
  } from "@earendil-works/pi-coding-agent";
16
16
  import type { ChildFailure } from "./contracts.ts";
17
- import type {
18
- ParentPayloadBridge,
19
- ParentPayloadCallback,
20
- ParentPayloadCapture,
17
+ import {
18
+ type ParentPayloadBridge,
19
+ ParentPayloadBridgeError,
20
+ type ParentPayloadCallback,
21
+ type ParentPayloadCapture,
21
22
  } from "./parent-payload-bridge.ts";
22
23
  import type { SubagentEndpoint } from "./subagent-endpoint.ts";
23
24
 
@@ -145,7 +146,12 @@ type PhaseRuntimeFailure = Extract<
145
146
  >;
146
147
 
147
148
  export type PhaseRuntimeResult =
148
- | { ok: true; modelRuntime: ModelRuntime; model: Model<string> }
149
+ | {
150
+ ok: true;
151
+ modelRuntime: ModelRuntime;
152
+ model: Model<string>;
153
+ failureOverride?: () => PhaseRuntimeFailure | undefined;
154
+ }
149
155
  | { ok: false; error: string; failure: PhaseRuntimeFailure };
150
156
 
151
157
  function phaseRuntimeFailure(
@@ -170,15 +176,28 @@ function phasePayloadCallback(
170
176
  payloadBridge: PhasePayloadBridge,
171
177
  childOnPayload: ParentPayloadCallback | undefined,
172
178
  signal: AbortSignal | undefined,
179
+ diagnostic?: { failure?: PhaseRuntimeFailure },
173
180
  ): ParentPayloadCallback {
174
- return (payload, model) =>
175
- payloadBridge.bridge.composePayload(
176
- payloadBridge.capture,
177
- payload,
178
- model,
179
- childOnPayload,
180
- signal,
181
- );
181
+ return async (payload, model) => {
182
+ try {
183
+ return await payloadBridge.bridge.composePayload(
184
+ payloadBridge.capture,
185
+ payload,
186
+ model,
187
+ childOnPayload,
188
+ signal,
189
+ );
190
+ } catch (error) {
191
+ if (error instanceof ParentPayloadBridgeError && diagnostic) {
192
+ diagnostic.failure = {
193
+ kind: "environment",
194
+ code: error.code,
195
+ stage: "child-provider-stream",
196
+ };
197
+ }
198
+ throw error;
199
+ }
200
+ };
182
201
  }
183
202
 
184
203
  export async function runtimeForProvider(
@@ -209,6 +228,7 @@ export function phaseProvider(
209
228
  env?: Record<string, string>;
210
229
  },
211
230
  payloadBridge: PhasePayloadBridge,
231
+ diagnostic?: { failure?: PhaseRuntimeFailure },
212
232
  ): Provider {
213
233
  return {
214
234
  ...parent,
@@ -232,6 +252,7 @@ export function phaseProvider(
232
252
  payloadBridge,
233
253
  options?.onPayload as ParentPayloadCallback | undefined,
234
254
  options?.signal,
255
+ diagnostic,
235
256
  );
236
257
  return parent.stream(model, context, {
237
258
  ...options,
@@ -247,6 +268,7 @@ export function phaseProvider(
247
268
  payloadBridge,
248
269
  options?.onPayload as ParentPayloadCallback | undefined,
249
270
  options?.signal,
271
+ diagnostic,
250
272
  );
251
273
  return parent.streamSimple(model, context, {
252
274
  ...options,
@@ -321,31 +343,55 @@ export async function runtimeFromContext(
321
343
  const effectiveModelKey = modelKeyFor(model);
322
344
  const capture = payloadBridge.capture(effectiveModelKey, ctx.modelRegistry);
323
345
  if (!capture) {
346
+ const code =
347
+ payloadBridge.diagnoseCapture(effectiveModelKey, ctx.modelRegistry) ??
348
+ "parent-bridge-capture-not-ready";
324
349
  return phaseRuntimeFailure(
325
- { kind: "transport", code: "transport-failure" },
326
- "parent payload bridge is unavailable",
350
+ { kind: "environment", code, stage: "phase-runtime" },
351
+ code,
327
352
  );
328
353
  }
329
- const captureIsReady = () => {
354
+ const captureFailure = () => {
330
355
  const currentModel = ctx.model;
331
- return (
332
- currentModel !== undefined &&
333
- sameSelectedModel(currentModel, selectedModelKey) &&
356
+ if (
357
+ currentModel === undefined ||
358
+ !sameSelectedModel(currentModel, selectedModelKey)
359
+ ) {
360
+ return "parent-bridge-model-key-mismatch" as const;
361
+ }
362
+ if (
334
363
  payloadBridge.capture(effectiveModelKey, ctx.modelRegistry) === capture
364
+ ) {
365
+ return undefined;
366
+ }
367
+ return (
368
+ payloadBridge.diagnoseCapture(effectiveModelKey, ctx.modelRegistry) ??
369
+ "parent-bridge-capture-not-ready"
335
370
  );
336
371
  };
337
- if (!captureIsReady()) {
372
+ const initialCaptureFailure = captureFailure();
373
+ if (initialCaptureFailure) {
338
374
  return phaseRuntimeFailure(
339
- { kind: "transport", code: "transport-failure" },
340
- "parent payload bridge is unavailable",
375
+ {
376
+ kind: "environment",
377
+ code: initialCaptureFailure,
378
+ stage: "phase-runtime",
379
+ },
380
+ initialCaptureFailure,
341
381
  );
342
382
  }
343
383
  const delegate = capture.delegate;
344
384
  if (!delegate) throw new Error("parent Provider is unavailable");
345
- const provider = phaseProvider(delegate, resolved, {
346
- bridge: payloadBridge,
347
- capture,
348
- });
385
+ const diagnostic: { failure?: PhaseRuntimeFailure } = {};
386
+ const provider = phaseProvider(
387
+ delegate,
388
+ resolved,
389
+ {
390
+ bridge: payloadBridge,
391
+ capture,
392
+ },
393
+ diagnostic,
394
+ );
349
395
  let modelRuntime: ModelRuntime;
350
396
  try {
351
397
  modelRuntime = await runtimeForProvider(provider, signal);
@@ -354,13 +400,23 @@ export async function runtimeFromContext(
354
400
  throw error;
355
401
  }
356
402
  if (signal?.aborted) return cancelledPhaseRuntime();
357
- if (!captureIsReady()) {
403
+ const finalCaptureFailure = captureFailure();
404
+ if (finalCaptureFailure) {
358
405
  return phaseRuntimeFailure(
359
- { kind: "transport", code: "transport-failure" },
360
- "parent payload bridge is unavailable",
406
+ {
407
+ kind: "environment",
408
+ code: finalCaptureFailure,
409
+ stage: "phase-runtime",
410
+ },
411
+ finalCaptureFailure,
361
412
  );
362
413
  }
363
- return { ok: true, modelRuntime, model };
414
+ return {
415
+ ok: true,
416
+ modelRuntime,
417
+ model,
418
+ failureOverride: () => diagnostic.failure,
419
+ };
364
420
  }
365
421
 
366
422
  function abortable<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
package/src/runtime.ts CHANGED
@@ -521,6 +521,25 @@ function attemptFailureCause(
521
521
  }
522
522
  }
523
523
 
524
+ function attemptsExhaustedFailure(failure: ChildFailure): TaskFailure {
525
+ const cause = attemptFailureCause(failure);
526
+ if (failure.kind !== "transport") {
527
+ return { kind: "attempts-exhausted", cause };
528
+ }
529
+ return {
530
+ kind: "attempts-exhausted",
531
+ cause,
532
+ lastFailure: {
533
+ code: failure.code,
534
+ stage:
535
+ failure.stage ??
536
+ (failure.code === "child-timeout" || failure.code === "timeout"
537
+ ? "child-timeout"
538
+ : "child-provider-stream"),
539
+ },
540
+ };
541
+ }
542
+
524
543
  function assertNever(value: never): never {
525
544
  throw new Error(`unhandled candidate failure: ${JSON.stringify(value)}`);
526
545
  }
@@ -1397,6 +1416,7 @@ export class Runtime {
1397
1416
  ],
1398
1417
  timeoutMs: LIMITS.phaseTimeoutMs,
1399
1418
  signal,
1419
+ failureOverride: phase.failureOverride,
1400
1420
  });
1401
1421
  if (signal.aborted)
1402
1422
  return {
@@ -1828,7 +1848,8 @@ export class Runtime {
1828
1848
  envelope.stage === "abel-design" &&
1829
1849
  !first.ok &&
1830
1850
  first.failure?.kind === "artifact" &&
1831
- first.failure.code === "invalid-structural-result";
1851
+ (first.failure.code === "invalid-structural-result" ||
1852
+ first.failure.code === "child-no-structural-submit");
1832
1853
  if (!retryInvalidDesignResult) return this.withUsage(context, first);
1833
1854
 
1834
1855
  const second = await this.dispatchChild(agent, envelope, ctx, signal);
@@ -1901,12 +1922,7 @@ export class Runtime {
1901
1922
  const firstTerminal = terminalTaskFailure(firstFailure);
1902
1923
  if (firstTerminal) return finish(block(firstTerminal));
1903
1924
  if (launchIndex === 1) {
1904
- return finish(
1905
- block({
1906
- kind: "attempts-exhausted",
1907
- cause: attemptFailureCause(firstFailure),
1908
- }),
1909
- );
1925
+ return finish(block(attemptsExhaustedFailure(firstFailure)));
1910
1926
  }
1911
1927
  if (firstFailure.kind === "artifact" || firstFailure.kind === "stale") {
1912
1928
  task.state = {
@@ -1940,12 +1956,7 @@ export class Runtime {
1940
1956
  }
1941
1957
  const secondTerminal = terminalTaskFailure(secondFailure);
1942
1958
  if (secondTerminal) return finish(block(secondTerminal));
1943
- return finish(
1944
- block({
1945
- kind: "attempts-exhausted",
1946
- cause: attemptFailureCause(secondFailure),
1947
- }),
1948
- );
1959
+ return finish(block(attemptsExhaustedFailure(secondFailure)));
1949
1960
  }
1950
1961
 
1951
1962
  private captureUsage(