IMPL-0001: Output Contracts Generated from the Pydantic Models¶
Status¶
Accepted (implemented, phase 1). Implements ADR-0028 rule 1, and rule 5 for the grammar mode. Date: 2026-10-03. Plan: Phases 0–1 of docs/plans/0028-model-capability-boundary-plan.md.
Context¶
Each operation's output had two definitions:
- Shown to the model: a static file,
src/pdl_taskmaster/controller/schemas/*.schema.json(11 files).context_compiler.py:94-116loads it into the prompt asoutput_schema. - Enforced by the provider: the Pydantic payload model (
wire_payloads.py:474-498), rewritten by_sanitize_schema_for_grammar(api_worker.py:615-698) and_strictify(api_worker.py:198-229). Lines 224-228 make every optional field required-nullable, for every provider. That form was written for Groq and Cerebras strict mode.
Nothing compared the two. Comparing them (tests/test_output_contracts.py: same properties, required sets and closed objects at every path) finds 7 of the 11 operations that send a grammar disagree:
| Operation | Disagreement |
|---|---|
EXECUTE, EMIT_RESULT_IR |
result_ir.witness (the last key of result_ir) required but not shown; evidence.section/observed required but optional in the prompt |
INTERPRET_PROMPT_REVIEW, INTERPRET_PLAN_REVIEW, INTERPRET_EXECUTION_INPUT |
confidence required but not shown; the two review operations are open objects in the prompt and closed in the grammar |
DRAFT_PROMPT |
task_entities required by the grammar, optional in the prompt |
DRAFT_EXECUTE |
execution_entities items shown with fields the grammar drops |
The semantic reads (BOOTSTRAP_ANALYSIS, INTERPRET_ACTIVATION) send no grammar. DRAFT_PLAN, REVISE_PLAN, REVISE_PROMPT and ANSWER_PROTOCOL_DISCUSSION agree.
Evidence¶
All from 2026-10-03, on nvidia/nemotron-3-super-120b-a12b, through OpenRouter. Captured bytes are under the session's scratch directory, and the regression fixture is tests/fixtures/nemotron-execute-whitespace-stall.json.
- Where the stall starts. Stalled
EXECUTEreplies wrote the complete answer, then emitted"\n "until the 16,384-token cap. The run always began right after"open_defects": []. HTTP 200; nothing was rejected. - What follows
open_defects. - Every unconstrained reply closed
result_irwith}there (14/14). - Every constrained reply that completed wrote
, "witness": nullfirst. - With
witnessremoved from the grammar'srequired, replies closed with}}}(8/8). - The whitespace condition. On a host whose grammar allows free whitespace (DekaLLM), the unchanged request stalled 5/5, all pretty-printed. With
witnessoptional, 2/2 pretty-printed replies completed. Nvidia's free endpoint later forced compact output (0/6 pretty-printed even when asked), which is why its stalls stopped. - The prompt format is ruled out. The API worker renders the projection compact (
repl.py:470-479); the stalled input was one unindented line. - The descriptions do work. Showing the full grammar schema without descriptions in the prompt switched ⅝ replies to
REQUEST_INPUT. The static files' descriptions are what steer the model, so they must survive the move.
Decision¶
- Descriptions move into the models. Each description in the static files moves verbatim onto the matching Pydantic field or model. A test asserts that every original description appears unchanged, at the same path, in the generated prompt schema.
- One generator, two views.
wire_payloadsgainsprompt_schema(operation, form)andgrammar_schema(operation, form): - the grammar is the prompt schema with descriptions removed;
- both share every structural transform:
$refinlining,consttoenum,oneOftoanyOf, discriminator tag required, theoutcomeunion wrapper. - Optional stays optional. The default form keeps Pydantic's
requiredand closes objects. The all-required form (strict_all_required) is used only when the serving provider declares it (IMPL-0003), and then for both views. - The prompt is generated.
context_compilerrendersoutput_schemafromprompt_schema;output_schema_sha256becomes the hash of the rendered schema. The static files and their contract paths are retired. - The invariant test. For every operation and both forms, the prompt schema and the grammar have the same object paths,
requiredsets andadditionalProperties. EXECUTEgoes in JSON mode. It is sent in JSON mode (no schema). Grammar is a per-stage profile setting (IMPL-0003), and returningEXECUTEtoschemaneeds evidence from the model's own serving endpoint. Without the schema the identical request completed 14/14, and the host validates every reply against the Pydantic model either way.
As implemented¶
- Generator.
runtime/output_contracts.py. contract_schema(op)is the Pydantic schema with$refinlined, discriminator tags required in every variant, objects closed unless declared open, Pydantic's own titles, defaults and docstrings removed, and thex-contractannotations applied.prompt_schema(op, form)andgrammar_schema(op, form)apply the same structural form. The grammar additionally drops descriptions and the keywords decoding engines reject.ContractFormisgrammar(schema/json/none),strict_all_requiredandfree_form_objects.- Plumbing.
ApiWorker.contract_form(op)decides the form from the configured providers.- The host hands it to the bridge (
engine.bridge.contract_form), and the compiler rendersoutput_schemain that form. - The worker sends the matching grammar: a JSON schema,
json_object, or nothing. - Annotations moved into the models (
contract(...)inruntime/wire_payloads.py): - the 39 descriptions;
minLength1 where the host already rejects empty strings, anduniqueItemswhere it deduplicates;- the
requiredsets the contract has always shown where Pydantic is deliberately more lenient (result_iron a RESULT;files,reconciliation,open_defects;approach_handoff;execution_entitiesuntil LEDGER L93, which made every field of the brief required in Pydantic too); - closed result-record objects;
DRAFT_EXECUTE's typed entity items.- Hidden from the contract, still validated (
SkipJsonSchema): confidence, System 1's channel. The old grammar forced System 2 to emit it, and a low value silently made a review UNRESOLVED.- the witness's host-set
provisional. - Shown exactly when enforced.
- The
{"outcome": ...}wrapper strict providers need is shown to the model whenever the grammar carries it. The union's own description stays at the top. - In JSON mode the plain union is shown.
- Rule 5.
EXECUTEis sent in JSON mode (JSON_MODE_OPERATIONS).- Groq gets
EXECUTEandEMIT_RESULT_IRin JSON mode instead of being routed past (_SCHEMA_REJECTED_OPERATIONS). - Groq and Cerebras get the strict all-required form (
_STRICT_ALL_REQUIRED_PROVIDERS). - These tables move to the per-stage profile (IMPL-0003).
- Static schema files retired.
- Deleted:
src/pdl_taskmaster/controller/schemas/*.schema.json. - The 13
output_schemapaths were removed from both copies ofEXECUTION_CONTRACT.json. This is a contract change, soCONTRACT_MANIFEST.jsonrecords the new hash in both copies (GUARD-05). - The projection manifest names
wire_payloads:<OPERATION>and hashes the rendered schema. - The replay fixture (
tests/fixtures/recorded-cases.json) was re-keyed: prompts only, responses unchanged.
Model-facing changes, each deliberate¶
- New keys shown. Keys the grammar already enforced, or the host accepts, are now shown:
witnessunder EXECUTE, with the reviewed witness description;evidence.sectionandevidence.observed;task_entities(DRAFT_PROMPT) anddescription(REQUEST_INPUT) shown as optional, as the host has always treated them.- One wording change.
approach_notessays "SEM-05/TASK-03 split" instead of "partition". The benchmark-contamination scan flags that word in harness source, and the description now lives in Python. - Dropped. The REVIEW_FACTS "at least one change or a progression" rule (
minItemsplusanyOf) is no longer shown. Neither the host nor the grammar ever enforced it. - Variant order kept. Order is what the model reads first; parsing is by
kind. - Unions keep the order the model has always seen: RESULT before REQUEST_INPUT (EXECUTE), RESULT before BLOCKED (DRAFT_EXECUTE).
- In the first live run, with REQUEST_INPUT listed first, Nemotron returned an input request. With the order restored, it returned a RESULT. One run each, so not attributable to the order alone. Session 20261004-025445 later asked for input with RESULT listed first, so the order does not explain it.
Live verification (2026-10-03, dev-mode REPL, three-gods prompt, --fast)¶
| Model, providers | EXECUTE |
Outcome |
|---|---|---|
Nemotron 3 Super :free, --api-providers Nvidia |
json_object, 6.8 s, 1,589 output tokens (840 reasoning) |
CLOSED_SUCCESS; complete result_ir including witness; the answer is essentially the canonical solution |
| gpt-oss-120b, default (Baseten, Crusoe) | json_object, 1.7 s, 383 output tokens |
CLOSED_SUCCESS; the default-form schemas for prompt and plan drafting were accepted; the answer is wrong (fixed questions), as before |
Host modes (2026-10-04)¶
- The contract follows the host's mode. The host reads
result_ironly in Result IR mode (verified execution orPDLT_RESULT_IR=1, RS-10). Outside it, EXECUTE showedresult_iras required and ten RS clauses, and the host discarded the field. - A property declared
contract(when=RESULT_IR_MODE)exists only in that mode: not shown, required or enforced otherwise. - The contract's
mode_requirementslists the clauses that apply only in a mode (EXECUTE: RS-01, RS-04 to RS-10). - The engine passes the modes per call (
_call(modes=...),OperationBridge.request,ContextCompiler.compile); the manifest records them. - The grammar is the schema the projection showed. The worker builds it from the projection's
output_schema(grammar_view), so it holds in every mode as well as every form. A request without a projection falls back to the operation's default. - Retired clauses removed. RS-02 and RS-03 were still listed and shown; they are off the requirement lists and the registry index. The standard keeps them as prose, like PLAN-09.
- Empty inputs not shown.
REQUIRED_TASK_INPUTSmoved tooptional_include(EXECUTE, DRAFT_EXECUTE). It was shown asnull, which reads as required inputs that are missing.
REQUEST_INPUT probe (2026-10-04, Nemotron :free, Nvidia only, dev + fast, 10 runs per arm)¶
Session 20261004-025445 asked for "the gods' responses" at EXECUTE. Same model and code as 20261004-021028, which answered. The probe uses that session's exact request, without the line asking for the questions.
| Arm | Reached EXECUTE | First EXECUTE asked for input | Stopped at plan review (PLAN-02 finding) |
|---|---|---|---|
| A: host modes applied | 7/10 | 0/7 | 3/10 |
| B: A plus the REQUEST_INPUT sentence and the entity-definition wording | 6/10 | 0/6 | 4/10 |
- No measurable effect on input requests. The rate was 0 before the wording change. 0/7 bounds it only loosely (the 95% upper bound is about 40%).
- Interactive plans did not cause it. In arm A, 3 of the 7 plans had interactive steps (pose, receive, observe), and all 3 answered.
- The wording change: REQUEST_INPUT adds "People or events described in the task are part of the task, not a source of input." Kept as a protocol statement (GUARD-03), not solving advice.
- The plan-review stops are the host's PLAN-02 restatement finding; under fast mode a finding needs a review, and with stdin closed the run exits 2. They are unrelated to these changes.
Consequences¶
- What the model sees changes. It now sees
witness,confidenceandevidence.sectionas optional keys. Behaviour is checked on both named models before acceptance. - The replay fixture changes.
tests/fixtures/recorded-cases.jsonis re-keyed, because the prompts change; the responses don't. - Editing an output contract means editing the Pydantic model.
Verification¶
- The invariant and description-preservation tests.
- The full offline suite and the integrity gate.
- A live three-gods turn on Nemotron reaching
CLOSED_SUCCESS. - An unchanged gpt-oss turn.