Extraction tool calls: tolerate omitted-empty fields (weak-model trap)

Diagnosis from the real 25-file notes import (user-pasted session logs):
almost every run hit 'Validation failed for tool submit_findings/review:
edges must have required properties edges' — the model (glm flash, low
thinking) omits top-level fields when they're empty, and pi-ai's
structural JSON-Schema validation runs BEFORE the zod layer, so each
omission cost one of the extractor's maxTurns 4 (full payload
regeneration) and occasionally burned out the run entirely (the two
FAILED imports).

The model is technically wrong, but the schema provokes it:
required-but-usually-empty arrays are a known weak-model anti-pattern.

- lhTool's model-facing declaration now converts with io: "input"
  (the declaration describes what the MODEL sends): .default() fields
  drop out of required. No-op for teacher tools (none use defaults).
- findingsSchema.edges / auditSchema.dropped: .default([]) — omission
  means "none"; attribution: .default({quiz:{}}) for quiz-free sessions.
- Prompts hardened in all three agents: include every top-level field,
  never omit because empty.

Structural probe: validateToolArguments now passes the exact pasted
failure shape; zod re-parse in execute applies the defaults. 355/355
tests, typecheck, lint, build green. (lh-tool.ts is the shared engine
seam — flagged to master: io:"input" is the correct mode for every
model-facing declaration, teacher path verified unaffected.)
This commit is contained in:
amosblomqvist
2026-09-13 02:32:20 +03:00
parent 543cf33789
commit c3cc55c585
2 changed files with 25 additions and 6 deletions
+8 -2
View File
@@ -66,7 +66,10 @@ export function toPiTool(def: LhTool<any, any>): AgentTool<any, any> {
name: def.name,
label: def.name,
description: def.description,
parameters: z.toJSONSchema(def.schema as z.ZodType) as never,
// io: "input" — the declaration describes what the MODEL must send,
// so .default() fields are optional in the schema (omission = the
// default); required-empty arrays are a known weak-model trap.
parameters: z.toJSONSchema(def.schema as z.ZodType, { io: "input" }) as never,
// Question tools opt into SEQUENTIAL execution: pi runs the whole
// batch in call order when any called tool opts in, so the
// tool-input-available chunks arrive in the model's call order — the
@@ -110,6 +113,9 @@ export function piToolDeclarations(registry: Record<string, LhTool<any, any>>) {
name,
label: name,
description: def.description,
parameters: z.toJSONSchema(def.schema as z.ZodType) as never,
// io: "input" — the declaration describes what the MODEL must send,
// so .default() fields are optional in the schema (omission = the
// default); required-empty arrays are a known weak-model trap.
parameters: z.toJSONSchema(def.schema as z.ZodType, { io: "input" }) as never,
}));
}