Skip to content

StructuredOutput: accept a validated result file path instead of re-emitted JSON #140

Closed

Description

@byapparov

Context

In schema mode the model writes and lints its result file (e.g. explain-change's $RESULT), then has to re-emit the WHOLE object as StructuredOutput tool arguments (prompt.ts:1120-1157), and run.ts:590 writes those args to --output-result. The linted file is never read.

Measured on 10 aictrl prod runs (2026-10-10):

  • the re-emission is about 5–9k output tokens per run (12–23% of output), or about 16k when a corrective retry re-emits everything (40–46%);
  • about $0.02 per Gemini run, plus estimated generation time of 30 s to 2 min;
  • it creates a second, unvalidated-by-lint copy that can drift from the linted file. On Gemini it silently lost every map field (see the companion issue).

Proposal

StructuredOutput also accepts {"path": "<file>"} (or a flag like --output-result-from <file>). The CLI reads the file, validates it with Ajv and writes --output-result. A rejection asks for a small edit to the file instead of a full re-emission.

Acceptance Criteria

  • Path form supported: validated, size-capped, confined to the work dir, with the same exit codes.
  • The corrective turn on the path form tells the model to edit the file.
  • Backward compatible with the inline-args form.

References

aictrl-dev/aictrl#6172; aictrl skill explain-change SKILL.md "call StructuredOutput with the contents of $RESULT", output contract pr-explanation-output-contract.ts:45,48.

Activity

  1. byapparov commented on Oct 10, 2026

    @byapparov
    MemberAuthor

    More evidence (2026-10-10, offline eval via agy with Gemini 3.8 Flash, 13 runs): re-emitting the linted result as a schema-enforced object drifted in 7 of 13 Gemini runs, and in the 1 Sonnet run checked. Keys that aren't plain identifiers were quoted, renamed or dropped ('a->b', 'T.col' → T_col, "\"1\""); optional fields (userSees, canDo, related) were emptied; one result shrank from 8 elements to 1. A probe asking the model to copy a lint-clean file verbatim still produced 19 lint errors. Prompt text didn't fix it. This is agy's own loop, not aictrl run, so it needs confirming on our CLI, but it supports the path form here: publish the validated file from disk rather than a re-emitted copy.

  2. byapparov commented on Oct 10, 2026

    @byapparov
    MemberAuthor

    Confirmed on aictrl run 0.4.7: the published result is the re-emitted copy, not the validated file

    The minimal reproduction and matrix are on #139: #139

    The same runs prove this issue:

    • In all 12 Gemini and GLM map-schema runs, the model's own ws/result.json matched the expected object exactly (6/6 keys, including queued->running, [*]->queued, TASKS.org_id, "1").
    • The CLI published only what the model re-emitted as StructuredOutput arguments. On Gemini that was {} every time (7/7): silently accepted when the schema has no minProperties (exit 0), a failed run when it does (exit 3).
    • GLM re-emitted it correctly (3/3), so the design isn't lossy by itself. It turns any provider-side schema loss, or any copy drift, into the published result.

    In prod (aictrl, 2026-10-10) this emptied every diagram's elements in all 5 Gemini PR Explanation runs. It also costs about 5–9k output tokens per run to re-emit (~16k when a corrective turn re-emits again).

    Fix options are on hold until we've chosen; the reproduction comes first, as requested.

  3. added a commit that references this issue on Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions