fix(review): result_schema gadfly findings + go mod tidy
executus CI / test (pull_request) Successful in 50s

- Extracted applyResultSchema (panic-isolated like safeFinalNudge — a
  validator bug must never convert a successful run into a panic error)
- Blank corrective-round reply no longer clobbers the prior candidate
- result_schema_valid now fires on first-try conforming answers too
- Multi-phase runs log WARN + result_schema_skipped instead of a
  silent drop
- errors.As instead of the hand-rolled assertion; redundant guard
  removed; go.mod/go.sum tidied (the CI failure)

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-07-17 13:30:04 -04:00
co-authored by Claude Fable 5
parent b4080a8a6e
commit a6b185985a
4 changed files with 124 additions and 75 deletions
+14 -64
View File
@@ -498,70 +498,11 @@ func (e *Executor) Run(ctx context.Context, ra RunnableAgent, inv tool.Invocatio
// actually be returned. Fail-open on compile problems; best-effort on
// persistent violations (the host's fallback validator is authoritative).
if runErr == nil && runRes != nil && len(inv.ResultSchema) > 0 {
if sch, cerr := compileResultSchema(inv.ResultSchema); cerr != nil {
slog.Warn("run: result_schema failed to compile; skipping kernel validation",
"run_id", inv.RunID, "error", cerr)
if rec != nil {
rec.LogEvent("result_schema_skipped", map[string]any{"error": cerr.Error()})
}
} else {
for round := 0; ; round++ {
var errs []string
doc, ok := extractResultJSON(runRes.Output)
if ok {
errs = validateResultJSON(sch, doc)
if errs == nil {
// Conforming: the bare validated document IS the answer.
runRes.Output = string(doc)
if rec != nil && round > 0 {
rec.LogEvent("result_schema_valid", map[string]any{"rounds": round})
}
break
}
} else {
errs = []string{"no JSON document found in output"}
}
if round >= resultSchemaMaxRounds {
// Best effort exhausted — keep the last candidate; the
// host's own validator decides what to do with it.
if rec != nil {
rec.LogEvent("result_schema_unmet", map[string]any{
"rounds": round, "validation_errors": errs,
})
}
break
}
correction := resultSchemaCorrection(inv.ResultSchema, errs)
if rec != nil {
rec.LogEvent("result_schema_round", map[string]any{
"round": round + 1, "validation_errors": errs,
})
}
if noteCheckpoint != nil {
noteCheckpoint(llm.UserText(correction))
}
roundOpts := append([]agent.Option{
agent.WithToolbox(toolbox),
agent.WithMaxSteps(resultSchemaRoundMaxSteps),
agent.WithStepObserver(obs),
}, sharedOpts...)
roundAg := agent.New(model, e.systemPrompt(ra), roundOpts...)
roundRes, roundErr := roundAg.Run(runCtx, correction,
agent.WithSteer(steer), agent.WithHistory(runRes.Messages))
if roundRes != nil {
runRes.Usage.Add(roundRes.Usage)
}
if roundErr != nil || roundRes == nil {
slog.Warn("run: result_schema corrective round failed; keeping last output",
"run_id", inv.RunID, "error", roundErr)
break
}
// The round's text is the NEW candidate (contrast FinalGuard,
// whose extra-round text is discarded meta-commentary).
runRes.Output = roundRes.Output
runRes.Messages = roundRes.Messages
}
}
e.applyResultSchema(runCtx, resultSchemaDeps{
inv: inv, ra: ra, runRes: runRes, rec: rec,
model: model, toolbox: toolbox, sharedOpts: sharedOpts,
obs: obs, steer: steer, noteCheckpoint: noteCheckpoint,
})
}
// FinalGuard (optional): the run finished successfully — before the result
@@ -635,6 +576,15 @@ func (e *Executor) Run(ctx context.Context, ra RunnableAgent, inv tool.Invocatio
// shared step observer (audit/steps/critic) is wired per phase by the phase
// runner; checkpointing is phase-boundary granular (completed phases are
// recorded so a resumed run skips them).
//
// ResultSchema is single-loop-only (FinalGuard's scope) — make the drop
// observable rather than silent.
if len(inv.ResultSchema) > 0 {
slog.Warn("run: result_schema ignored on multi-phase run", "run_id", inv.RunID)
if rec != nil {
rec.LogEvent("result_schema_skipped", map[string]any{"error": "multi-phase runs are not schema-validated"})
}
}
runRes, runErr = e.runPhases(runCtx, ra, phaseDeps{
baseModel: model,
baseToolbox: toolbox,