@tea-agent/loop-agent 0.29.3 → 0.30.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.
@@ -126,7 +126,7 @@ export function buildNodePrompt(spec, task, upstream, options) {
126
126
  convergenceFeedback: options?.convergenceFeedback,
127
127
  });
128
128
  }
129
- function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths) {
129
+ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics) {
130
130
  if (attemptNumber <= 1)
131
131
  return basePrompt;
132
132
  if (previousFailureCategory === "protocol-invalid" &&
@@ -152,6 +152,7 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
152
152
  maxAttempts,
153
153
  reason: "T4",
154
154
  targetPaths: recoveryTargetPaths,
155
+ diagnostics: recoveryDiagnostics,
155
156
  }),
156
157
  ].join("\n");
157
158
  }
@@ -180,6 +181,7 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
180
181
  maxAttempts,
181
182
  reason: "T3_or_T5",
182
183
  targetPaths: recoveryTargetPaths,
184
+ diagnostics: recoveryDiagnostics,
183
185
  }),
184
186
  ].join("\n");
185
187
  }
@@ -636,12 +638,21 @@ export async function executeDagNode(input) {
636
638
  // or when no progress facts exist yet (no-op).
637
639
  const recoveryProgress = await loadBackendTestWriterProgressForRetry(runDir, task.id);
638
640
  const recoveryTargetPaths = recoveryProgress?.targetPaths;
641
+ // Fold per-path structural reasons (e.g. T5 "case heading at wrong
642
+ // level") into the retry prompt so the model fixes the exact defect
643
+ // instead of guessing from a generic hint.
644
+ const recoveryDiagnostics = recoveryProgress?.issues
645
+ ?.filter((i) => !!i.path && i.recoverable)
646
+ .reduce((acc, i) => {
647
+ (acc[i.path] ??= []).push(i.detail);
648
+ return acc;
649
+ }, {});
639
650
  result = await executeNode({
640
651
  task,
641
652
  cwd,
642
653
  model,
643
654
  ...(thinking ? { thinking } : {}),
644
- prompt: buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths),
655
+ prompt: buildAttemptPrompt(task, prompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics),
645
656
  attempt: attemptNumber,
646
657
  reportActivity,
647
658
  timeoutMs: livenessPolicy.absoluteMaxWallClockMs,
@@ -326,6 +326,20 @@ export const dagWritePolicySchema = z.enum([
326
326
  ]);
327
327
  /** Git/post-diff write guard intensity for exclusive Pi writers. */
328
328
  export const dagWriteGuardPolicySchema = z.enum(["full", "tools-only"]);
329
+ /**
330
+ * Fail-closed outcome/diff consistency contract for bounded Pi writers.
331
+ * The writer must begin with IMPLEMENTATION_OUTCOME: changed,
332
+ * already-satisfied, or blocked; the executor checks it against the
333
+ * run-attributed diff. Declared before the dynamic child schema so the
334
+ * child writerOutcomePolicy optional field can reuse it without a TDZ.
335
+ */
336
+ export const dagWriterOutcomePolicySchema = z
337
+ .object({
338
+ type: z.literal("implementation-outcome-v1"),
339
+ /** Generation writers may forbid already-satisfied/no-diff completion. */
340
+ requireChangedFiles: z.boolean().optional(),
341
+ })
342
+ .strict();
329
343
  export const contextPolicyIdSchema = z.enum([
330
344
  "baseline-v1",
331
345
  "role-specialized-v1",
@@ -512,7 +526,14 @@ export const dagDynamicExpansionChildTaskSchema = z.object({
512
526
  allowedPaths: z.array(z.string()).optional().default([]),
513
527
  forbiddenPaths: z.array(z.string()).optional().default([]),
514
528
  writeSet: z.array(z.string()).optional(),
529
+ /**
530
+ * Materialized-child pass-through fields for bounded Pi writer children
531
+ * (backend-test md/pytest case sharding). All optional so legacy
532
+ * read-only/frontend map children (which never set these) keep parsing.
533
+ */
534
+ writerOutcomePolicy: dagWriterOutcomePolicySchema.optional(),
515
535
  writeGuardPolicy: dagWriteGuardPolicySchema.optional(),
536
+ retryPolicy: dagRetryPolicySchema.optional(),
516
537
  });
517
538
  export const dagDynamicExpansionSchema = z.object({
518
539
  type: z.enum(["map_agent", "verify_agent"]),
@@ -603,13 +624,6 @@ export const dagDecisionGateSchema = z.object({
603
624
  schemaVersion: z.literal(1).optional(),
604
625
  mode: dagDecisionGateModeSchema.optional().default("record-only"),
605
626
  });
606
- export const dagWriterOutcomePolicySchema = z
607
- .object({
608
- type: z.literal("implementation-outcome-v1"),
609
- /** Generation writers may forbid already-satisfied/no-diff completion. */
610
- requireChangedFiles: z.boolean().optional(),
611
- })
612
- .strict();
613
627
  /**
614
628
  * The final supervised audit is the sole runtime write authority for its
615
629
  * writer. `allowedPaths` remains a ceiling; it is never a writer fallback.