diff --git a/CHANGELOG.md b/CHANGELOG.md index dd4501cbf..c9fa241e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,85 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### BREAKING — a template subtype's axis is DIRECTION (ADR-0052 / ADR-0053) + +`template.output` renders OUTBOUND — a document, an email, an export — and generates +**nothing that reads a model's reply**. The inbound half (the response record, the FR-010 +response-format fragment, the parser-on-receipt and the tolerant extractor) belongs to a +`template.prompt` carrying **`@responseRef`**, and the gate is that attribute's PRESENCE, +never a format value. All five ports; the rule lives in one predicate per port +(`FindInbound`) that every inbound generator and every api-docs builder calls through. + +**What was wrong.** The old tier had drifted three ways at once, and each way produced +generated code that could not work: + +- The **parser** applied NO format filter. An `@format: markdown` document template got a + generated `Schema.parse(JSON.parse(text))` over rendered prose — and this repository + shipped one, in `examples/advanced-modeling`. +- The **fragment emitter** and the **extractor** each applied their own + `@format ∈ {json,xml}` gate — against the OUTBOUND body's syntax, which says nothing + about the reply. A text-bodied prompt asking for a JSON answer, the common case, got a + strict parser and no tolerant extract, and no fragment at all. +- Nothing generated the inbound tier for a `template.prompt`, even though `@responseRef` + has been prompt-only vocabulary since it was introduced. + +**ADR-0053 supplies the missing fact:** `@responseFormat` (`json` | `xml`, default `json`) +is the syntax of the REPLY, distinct from `@format`, the syntax of the rendered prompt +BODY. The default reproduces the pre-ADR fallback exactly, so a model that never declares +it keeps its behaviour. The **strict tier is JSON-only**: an XML reply gets the tolerant +extract and nothing strict, because strict all-or-nothing semantics layered over a +REPAIRING parser would raise or accept based on how much repair happened. + +**Breaking, and each fails loudly:** + +- A `@promptStyle` left on a `template.output` now fails the LOAD + (`ERR_INVALID_TEMPLATE`) — it is prompt-only vocabulary, as is `@responseFormat`. +- A `template.output`'s parser / extractor / fragment files are no longer emitted; + `verify --codegen` names the committed ones as files a fresh regen would not emit. +- Emitted paths follow the direction: `.output.*` → `.response.*`, `.prompt.*` → + `.responseFormat.*`, and (Python) `_output_parser.py` → `_response_parser.py`, + `_output_prompt.py` → `_response_format.py`. +- **`@responseRef` now obeys the same target rule as `@payloadRef` in every port.** Only + TypeScript validated it; C#, Java and Python checked `@payloadRef` and never + `@responseRef`, so the same metadata failed one port's load and passed four — and in C# + the consequence was a parser returning a record nobody emitted (CS0246). + +Migration: [`docs/features/migrations/template-direction-outbound-vs-inbound.md`](docs/features/migrations/template-direction-outbound-vs-inbound.md). + +**The response RECORD differs by port, because the ports do not share a naming +convention.** C# names records after the resolved VALUE OBJECT, so the response record +simply IS that VO's record. Java, Kotlin and Python name them after the TEMPLATE, so a +responding prompt gets a SECOND record, `Response`, beside `Payload`; +Python puts it in its own `_response.py`, because the request payload emits +`extra="forbid"` (a mistyped render slot must fail at construction) while a reply record +must tolerate unknown fields, and a value-object reachable from both closures could carry +only one setting. TypeScript needs no new record — its payload types come from +`entityFile()`, which emits per `object.value` regardless of any template. + +**Also fixed, found while doing it:** + +- **The trace helper was a fifth inbound consumer nobody had listed.** TypeScript and + Python derived a REPLY's parse format from `@format`; Java called the 2-arg + `MetaObjectExtractor.extract` overload, which hardcodes `Format.JSON`, so an XML reply + was inexpressible there rather than merely mis-read. All three now read + `@responseFormat`. +- **A `template.prompt` got no model doc page.** The api-docs surface has always emitted + `api///.md` for a top-level prompt, and that page carries a + "Model / metadata" back-link — but `meta docs` wrote the neutral page for + `template.output` alone, so the link pointed at a page nothing generated, in every doc + tree containing a prompt. +- **A prompt's `@payloadRef` record was generated and documented nowhere** (C#, then + Python): api-docs walked `template.output` only. + +**The durable lesson is about the corpus, not the code.** `api-docs-cross-port` had +exactly one template, and one `@promptStyle` on it was the whole reason it exercised the +PROMPT and OUTPUT_PARSER paths in every port's api-docs builder. Removing that attribute — +required, since it is prompt-only now — silently deleted the last inbound coverage in the +corpus, and **all five ports stayed green**: a corpus that stops exercising a code path +emits no diagnostic, only assertions that quietly cover less. The corpus now carries a +README naming which case covers which path, so an edit that removes one has to remove its +stated purpose too. + ## [0.24.0] — npm `0.24.0` · PyPI `0.24.0` · NuGet `0.24.0` · Maven `7.24.0` A coordinated **MINOR** across all four registries. It is a MINOR rather than a PATCH @@ -673,8 +752,6 @@ ADR-0015 makes for schema migrations. Refusing warns rather than failing the rea because failing a Maven build over a file the user chose to own would punish exactly the person the guard protects. -||||||| constructed merge base - ### Added — Python port serves the shipped `ai` library (loader `libraries=[...]`) **No new vocabulary — this is port parity.** No type, subtype, or attribute is added, so diff --git a/agent-context/skills/metaobjects-prompts/SKILL.md b/agent-context/skills/metaobjects-prompts/SKILL.md index 99abcd3de..85826b37c 100644 --- a/agent-context/skills/metaobjects-prompts/SKILL.md +++ b/agent-context/skills/metaobjects-prompts/SKILL.md @@ -19,10 +19,13 @@ language lives in a reference fragment (pointed to at the bottom). A **template** is a typed pair: a logical reference to external text + a payload value-object declaring exactly what data the text expects. -| Subtype | Use | Extra attrs | -|---|---|---| -| `template.prompt` | LLM-targeted | `@maxTokens`, `@requiredSlots`, `@requiredTags`, `@model`, `@responseRef` | -| `template.output` | email / docs / config / export | `@kind: document \| email` (default `document`), `@promptStyle`, `@requiredTags`; `@kind: email` adds `@subjectRef` / `@htmlBodyRef` / `@textBodyRef` | +A template subtype's axis is **DIRECTION** — which way the text travels, not what it +is about (ADR-0052). + +| Subtype | Direction | Use | Extra attrs | +|---|---|---|---| +| `template.prompt` | outbound, and optionally inbound | LLM-targeted | `@maxTokens`, `@requiredSlots`, `@requiredTags`, `@model`, `@responseRef`, `@responseFormat`, `@promptStyle` | +| `template.output` | outbound ONLY | email / docs / config / export | `@kind: document \| email` (default `document`), `@requiredTags`; `@kind: email` adds `@subjectRef` / `@htmlBodyRef` / `@textBodyRef` | Both carry the generic attrs: @@ -36,13 +39,22 @@ Both carry the generic attrs: `template.output @kind: email` renders a structured `EmailDocument` (subject + HTML body + optional plain-text body) instead of one string — the TS render helper emits an `EmailDocument`-returning function for it (see the `render-example-email` -conformance fixture). `@promptStyle` (`guide` / `inline` / `exampleOnly`, FR-010) -selects how the output-format prompt fragment presents the payload shape to an LLM -(see "the output-format prompt fragment" below); `@requiredTags` names output tags -the rendered text must contain (`verify` checks it) on both subtypes. -`template.prompt` additionally carries `@responseRef` — naming the response -shape (an `object.value` or sourceless `object.projection`, #210) the prompt -expects, for typed LLM-call trace derivation. +conformance fixture). `@requiredTags` names output tags the rendered text must contain +(`verify` checks it) on both subtypes. + +**The INBOUND half belongs to `template.prompt` alone.** `@responseRef` names the +response shape (an `object.value` or sourceless `object.projection`, #210) a model's +reply is parsed into, and its PRESENCE is what asks for the whole inbound tier: the +response record, the response-format fragment, the parser-on-receipt and the tolerant +extractor. `@responseFormat` (`json` default / `xml`, ADR-0053) is the syntax of that +REPLY; `@promptStyle` (`guide` / `inline` / `exampleOnly`, FR-010) selects how the +fragment presents the shape. + +> **`@format` and `@responseFormat` are different facts.** `@format` is the syntax of +> the BODY you render; `@responseFormat` is the syntax of the answer you expect. A +> plain-text prompt asking for a JSON object is the common case. Putting `@promptStyle` +> or `@responseFormat` on a `template.output` is a LOAD ERROR — an output renders a +> document and nothing reads a reply to it. A third, structurally different subtype is also registered core vocabulary: **`template.toolcall`** (`@toolName` + `@payloadRef`, ADR-0011) — a vendor-agnostic @@ -192,33 +204,42 @@ For every template, the verify step resolves the text, parses each `{{...}}` reference, and checks it exists on the payload VO. If the text references `{{authorName}}` but the payload only has `displayName`, **the build fails.** This is the prompt-vs-payload drift gate — run it in CI. It walks both `template.prompt` -and `template.output` nodes the same way. +and `template.output` nodes the same way (both RENDER; only the direction of what comes +back differs). + +## A RESPONDING `template.prompt` generates a parser-on-receipt + +For every `template.prompt` declaring `@responseRef`, codegen emits a **typed parser** +that turns a model's reply back into that shape. It binds `@responseRef`, never +`@payloadRef` — `@payloadRef` types the request the prompt renders outbound, and the +question and the answer are usually different shapes. Each port emits the parser +idiomatically: a throw-on-invalid parse plus, where the language has the precedent, a +Result-style "safe" variant that doesn't throw. -## `template.output` also generates a parser-on-receipt +**A `template.output` gets no parser, ever.** Nothing reads a reply to a document. (Before +ADR-0052 it did, with no format filter at all — so an `@format: markdown` document got a +generated `JSON.parse` over rendered prose.) -For every `template.output`, codegen emits a **typed parser** that turns an LLM/raw -response back into the `@payloadRef` value-object — the reverse direction, reusing -the same payload VO (no new authoring). Each port emits it idiomatically: a -throw-on-invalid parse plus, where the language has the precedent, a Result-style -"safe" variant that doesn't throw. The parser file is a companion to the payload-VO -file; `verify` catches payload-VO ↔ parser drift at build time too. +The strict tier is JSON-only: an `@responseFormat: xml` reply gets the tolerant extract +and nothing strict, because strict all-or-nothing semantics layered over a REPAIRING XML +reader would raise or accept based on how much repair happened. The three-step consumer pattern is identical everywhere: render the prompt → call -your LLM client → parse the response with the generated parser. +your LLM client → parse the reply with the generated parser. -## `template.output` also generates the output-format prompt fragment (FR-010) +## A RESPONDING `template.prompt` generates the response-format fragment (FR-010) -For every **json/xml-format** `template.output` whose `@payloadRef` resolves to a -value-object, codegen additionally emits an `output-prompt` artifact: a -`renderFormat(...)`-shaped function backed by the render engine's -output-format renderer — the "produce your answer like this" instruction fragment -you splice into the prompt text so the model returns exactly the shape the parser -above expects. It's generated only for `json`/`xml` outputs (`text`/`html`/`csv`/ -`markdown`/`spreadsheet` don't get a fragment) and skipped under the same -unresolved-`@payloadRef` rule as the parser; the fragment and the parser's -`extract()` codegen agree on the same root name. +For every `template.prompt` whose `@responseRef` resolves, codegen additionally emits a +`renderFormat(...)`-shaped function backed by the render engine's output-format +renderer — the "produce your answer like this" instruction fragment you splice into the +prompt text so the model returns exactly the shape the parser above expects. The gate is +`@responseRef` PRESENCE, not a format value: the old `@format ∈ {json,xml}` gate read the +syntax of the OUTBOUND body to decide whether to instruct the model about the syntax of +its REPLY, so a text-bodied prompt asking for a JSON answer got no fragment at all. +`@responseFormat` selects which syntax the fragment teaches; the fragment and the +extractor agree on the same root name. -`@promptStyle` on the `template.output` controls the fragment's presentation +`@promptStyle` on the `template.prompt` controls the fragment's presentation (default `guide`): | `@promptStyle` | Presentation | diff --git a/agent-context/skills/metaobjects-prompts/references/csharp.md b/agent-context/skills/metaobjects-prompts/references/csharp.md index a594b3557..9a94b4b11 100644 --- a/agent-context/skills/metaobjects-prompts/references/csharp.md +++ b/agent-context/skills/metaobjects-prompts/references/csharp.md @@ -1,15 +1,18 @@ # C# parser-on-receipt -For every `template.output`, `MetaObjects.Codegen`'s `OutputParserGenerator` emits a -**typed parser** that validates an LLM/raw response against the template's -`@payloadRef` payload record. This is the receive side only — codegen emits **no** -provider/LLM-call layer; you compose the call yourself. The payload record comes from -the payload generator, so the parser and the payload VO can't silently drift. +For every RESPONDING `template.prompt` — one declaring `@responseRef` — +`MetaObjects.Codegen`'s `OutputParserGenerator` emits a **typed parser** that validates +a model's reply against that shape. ADR-0052: the tier binds `@responseRef`, never +`@payloadRef` (which types the request the prompt renders outbound), and a +`template.output` gets no parser at all. This is the receive side only — codegen emits +**no** provider/LLM-call layer; you compose the call yourself. C# names records after the +resolved VALUE OBJECT, so the response record simply IS that VO's record — no second +naming convention, and the parser and the record can't silently drift. ## Contents - Wire the generator - What it emits -- The output-format prompt fragment (FR-010) +- The response-format prompt fragment (FR-010) - The three-step consumer pattern - Recommended LLM caller (bring-your-own) - Consumer dependency @@ -27,12 +30,14 @@ dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog ## What it emits -Per `template.output`, `dotnet meta gen` writes one `.output.cs` with a -static `Parser` following the .NET BCL `Parse`/`TryParse` dual API — +Per responding `template.prompt`, `dotnet meta gen` writes one +`.response.cs` with a static `Parser` following the .NET BCL +`Parse`/`TryParse` dual API. The strict tier is JSON-only — an `@responseFormat: xml` +reply gets the tolerant extract and neither `Parse` nor `TryParse` — `Parse` throws, `TryParse` returns a bool plus an out-error: ```csharp -// generated .output.cs (shape) +// generated .response.cs (shape) public static class NpcResponseParser { /// malformed JSON or schema mismatch. @@ -53,25 +58,25 @@ generator also emits a tolerant `Extract(string[, ExtractOptions])` (self-contai components) returning an `ExtractionResult` with a nullable `Extracted` mirror — a classified per-field report rather than a throw. -## The output-format prompt fragment (FR-010) +## The response-format prompt fragment (FR-010) -For every json/xml-format `template.output`, `MetaObjects.Codegen`'s -`OutputPromptGenerator` (stable name `output-prompt-generator`) emits a -`.prompt.cs` declaring a static `Prompt` class with a -`RenderFormat()` / `RenderFormat(PromptOverrides)` pair, backed by the render -engine's `OutputFormatRenderer` — the "produce your answer like this" fragment for -the model. It runs as part of the same `dotnet meta gen` invocation as the payload +For every responding `template.prompt`, `MetaObjects.Codegen`'s `OutputPromptGenerator` +(stable name `output-prompt-generator`) emits a `.responseFormat.cs` +declaring a static `ResponseFormat` class with a `RenderFormat()` / +`RenderFormat(PromptOverrides)` pair, backed by the render engine's +`OutputFormatRenderer` — the "produce your answer like this" fragment for the model. It runs as part of the same `dotnet meta gen` invocation as the payload and parser generators: ```bash dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog ``` -`@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`) -controls the fragment's presentation; guidance is never emitted as comments. Skipped -for `template.prompt` nodes, non-json/xml `@format`, and an unresolved -`@payloadRef` — the same skip contract as the parser generator. The baked spec's -root name is the payload class name, agreeing with the parser's root. +`@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`) +controls the fragment's presentation; guidance is never emitted as comments. Skipped for +`template.output` nodes and an unresolved `@responseRef` — the same skip contract as the +parser generator. There is NO format gate: the old `@format ∈ {json,xml}` test read the +syntax of the outbound body to decide whether to describe the reply. The baked spec's +root name is the response record's, agreeing with the parser's root. ## The three-step consumer pattern diff --git a/agent-context/skills/metaobjects-prompts/references/java.md b/agent-context/skills/metaobjects-prompts/references/java.md index f86c24d6b..832f752ad 100644 --- a/agent-context/skills/metaobjects-prompts/references/java.md +++ b/agent-context/skills/metaobjects-prompts/references/java.md @@ -1,14 +1,16 @@ # Java parser-on-receipt -For every `template.output`, `codegen-spring`'s `SpringOutputParserGenerator` emits -a **typed parser** that validates an LLM/raw response against the template's -`@payloadRef` payload record. This is the receive side only — codegen emits **no** -provider/LLM-call layer; you compose the call yourself. +For every RESPONDING `template.prompt` — one declaring `@responseRef` — +`codegen-spring`'s `SpringOutputParserGenerator` emits a **typed parser** that validates +a model's reply against that shape. ADR-0052: the tier binds `@responseRef`, never +`@payloadRef` (which types the request the prompt renders outbound), and a +`template.output` gets no parser at all. This is the receive side only — codegen emits +**no** provider/LLM-call layer; you compose the call yourself. ## Contents - Wire the generator - What it emits -- The output-format prompt fragment (FR-010) +- The response-format prompt fragment (FR-010) - The three-step consumer pattern - Recommended LLM caller (bring-your-own) - Drift gate @@ -31,17 +33,20 @@ the payload record it parses into) to the Maven plugin's `` list: ## What it emits -Per `template.output`, `mvn metaobjects:generate` writes a `Parser` class with a static -`parse` method returning the `@payloadRef` payload record. The strict path throws -`com.fasterxml.jackson.core.JsonProcessingException` on malformed input: +Per responding `template.prompt`, `mvn metaobjects:generate` writes a `Parser` +class with a static `parse` method returning the `Response` record — this port's +records are TEMPLATE-named, so a responding prompt gets a SECOND record beside +`Payload`. The strict path throws +`com.fasterxml.jackson.core.JsonProcessingException` on malformed input, and is +JSON-only: an `@responseFormat: xml` reply gets the tolerant extract and no `parse`: ```java // generated Parser.java (shape) public final class NpcResponseParser { private NpcResponseParser() { } // no instances - public static NpcResponsePayload parse(String text) throws JsonProcessingException { - // Jackson-backed: validates the text against the payload record + public static NpcResponseResponse parse(String text) throws JsonProcessingException { + // Jackson-backed: validates the text against the @responseRef record } } ``` @@ -61,10 +66,10 @@ reference) return `MetaObjectAware` instances instead, and those need `JsonObjectWriter`/`MetaObjectSerializer` — not a bare mapper — to serialize correctly (see the codegen reference's "Serializing generated objects" section). -## The output-format prompt fragment (FR-010) +## The response-format prompt fragment (FR-010) -For every json/xml-format `template.output`, `codegen-spring`'s -`SpringOutputPromptGenerator` emits a `OutputPrompt` class with a +For every responding `template.prompt`, `codegen-spring`'s +`SpringOutputPromptGenerator` emits a `ResponseFormat` class with a static `renderFormat()` / `renderFormat(PromptOverrides)` pair, backed by `OutputFormatRenderer` from the `metaobjects-render` module — the "produce your answer like this" fragment for the model. Wire it alongside @@ -77,11 +82,12 @@ answer like this" fragment for the model. Wire it alongside ``` -`@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`) -controls the fragment's presentation; guidance is never emitted as comments. Skipped -for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` — -the same skip contract as the parser generator. The `SPEC`'s root name is the -capitalized payload class name, agreeing with the parser's extract-codegen root. +`@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`) +controls the fragment's presentation; guidance is never emitted as comments. Skipped for +`template.output` nodes and an unresolved `@responseRef` — the same skip contract as the +parser generator. There is NO format gate: the old `@format ∈ {json,xml}` test read the +syntax of the outbound body to decide whether to describe the reply. The `SPEC`'s root +name is the response record's, agreeing with the parser's extract-codegen root. ## The three-step consumer pattern diff --git a/agent-context/skills/metaobjects-prompts/references/kotlin.md b/agent-context/skills/metaobjects-prompts/references/kotlin.md index afd3cb4b8..c45c69b46 100644 --- a/agent-context/skills/metaobjects-prompts/references/kotlin.md +++ b/agent-context/skills/metaobjects-prompts/references/kotlin.md @@ -1,16 +1,19 @@ # Kotlin parser-on-receipt -For every `template.output`, `codegen-kotlin`'s `KotlinOutputParserGenerator` emits -a **typed parser** that validates an LLM/raw response against the template's -`@payloadRef` payload data class. This is the receive side only — codegen emits -**no** provider/LLM-call layer; you compose the call yourself. The payload data -class itself comes from `KotlinPayloadGenerator` (a `@Serializable data class`), so -the parser and the payload VO can't silently drift. +For every RESPONDING `template.prompt` — one declaring `@responseRef` — +`codegen-kotlin`'s `KotlinOutputParserGenerator` emits a **typed parser** that validates +a model's reply against that shape. ADR-0052: the tier binds `@responseRef`, never +`@payloadRef` (which types the request the prompt renders outbound), and a +`template.output` gets no parser at all. This is the receive side only — codegen emits +**no** provider/LLM-call layer; you compose the call yourself. The data class itself comes +from `KotlinPayloadGenerator` (a `@Serializable data class`) — this port's classes are +TEMPLATE-named, so a responding prompt gets a SECOND one, `Response`, beside +`Payload`, and the parser and the record can't silently drift. ## Contents - Wire the generators - What it emits -- The output-format prompt fragment (FR-010) +- The response-format prompt fragment (FR-010) - The three-step consumer pattern - Consumer dependency - Recommended LLM caller (bring-your-own) @@ -34,9 +37,10 @@ the payload it parses into) to the Maven plugin's `` list: ## What it emits -Per `template.output`, `mvn metaobjects:generate` writes a `Parser.kt` -`object` with a dual API matching kotlinx.serialization's exception model plus the -Kotlin stdlib `Result` convention: +Per responding `template.prompt`, `mvn metaobjects:generate` writes a +`Parser.kt` `object` with a dual API matching kotlinx.serialization's exception +model plus the Kotlin stdlib `Result` convention. The strict tier is JSON-only — an +`@responseFormat: xml` reply gets the tolerant extract and neither strict function: ```kotlin // generated Parser.kt (shape) @@ -44,8 +48,8 @@ object NpcResponseParser { private val json: Json = Json { ignoreUnknownKeys = false } /** Throws kotlinx.serialization.SerializationException on bad input. */ - fun parseNpcResponse(text: String): NpcResponsePayload = - json.decodeFromString(text) + fun parseNpcResponse(text: String): NpcResponseResponse = + json.decodeFromString(text) /** Result-style — does not throw. */ fun safeParseNpcResponse(text: String): Result = @@ -63,10 +67,10 @@ null) and a `extractLenient(loader, text)` overload that delegates to the runtim components. The lenient mirror type (`Extracted`) uses nullable fields per the Kotlin null-safety port — a missing/malformed component is `null`, not a throw. -## The output-format prompt fragment (FR-010) +## The response-format prompt fragment (FR-010) -For every json/xml-format `template.output`, `codegen-kotlin`'s -`KotlinOutputPromptGenerator` emits a `OutputPrompt.kt` `object` +For every responding `template.prompt`, `codegen-kotlin`'s +`KotlinOutputPromptGenerator` emits a `ResponseFormat.kt` `object` with `renderFormat()` / `renderFormat(overrides: PromptOverrides)`, backed by `OutputFormatRenderer` from the `metaobjects-render` module — the "produce your answer like this" fragment for the model. Wire it alongside @@ -79,11 +83,12 @@ answer like this" fragment for the model. Wire it alongside ``` -`@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`) -controls the fragment's presentation; guidance is never emitted as comments. Skipped -for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` — -the same skip contract as the parser generator. The `SPEC`'s root name is the -capitalized payload class name, agreeing with the parser's extract-codegen root. +`@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`) +controls the fragment's presentation; guidance is never emitted as comments. Skipped for +`template.output` nodes and an unresolved `@responseRef` — the same skip contract as the +parser generator. There is NO format gate: the old `@format ∈ {json,xml}` test read the +syntax of the outbound body to decide whether to describe the reply. The `SPEC`'s root +name is the response class's, agreeing with the parser's extract-codegen root. ## The three-step consumer pattern diff --git a/agent-context/skills/metaobjects-prompts/references/python.md b/agent-context/skills/metaobjects-prompts/references/python.md index e73a61387..a45c4c9d1 100644 --- a/agent-context/skills/metaobjects-prompts/references/python.md +++ b/agent-context/skills/metaobjects-prompts/references/python.md @@ -1,11 +1,13 @@ # Python parser-on-receipt -For every `template.output`, the `output-parser` generator (run via `metaobjects gen`) -emits a **typed parser** that validates an LLM/raw response against the template's -`@payloadRef` payload Pydantic model. This is the receive side only — codegen emits -**no** provider/LLM-call layer; you compose the call yourself. The payload class comes -from the sibling `payload` generator, so the parser and the payload VO can't silently -drift. +For every RESPONDING `template.prompt` — one declaring `@responseRef` — the +`output-parser` generator (run via `metaobjects gen`) emits a **typed parser** that +validates a model's reply against that shape's Pydantic model. ADR-0052: the tier binds +`@responseRef`, never `@payloadRef` (which types the request the prompt renders +outbound), and a `template.output` gets no parser at all. This is the receive side only — +codegen emits **no** provider/LLM-call layer; you compose the call yourself. The record +class comes from the sibling `payload` generator, so the parser and the record can't +silently drift. ## Contents - Wire the generators @@ -18,7 +20,7 @@ drift. ## Wire the generators -Select `output-parser` (the `payload` generator that emits the `Payload` it +Select `output-parser` (the `payload` generator that emits the `Response` it parses into runs alongside it): ```bash @@ -30,17 +32,17 @@ metaobjects gen ./metadata --out ./generated --generators payload,output-parser ## What it emits -Per `template.output`, `metaobjects gen` writes one `_output_parser.py` -with a single throw-only entry point. Python uses one API (not TS's +Per responding `template.prompt`, `metaobjects gen` writes one +`_response_parser.py` with a single throw-only entry point. Python uses one API (not TS's `parse`/`safeParse`) because raising `pydantic.ValidationError` is the idiomatic failure — the pydantic / Instructor / FastAPI norm; a Result-style wrapper would be un-Pythonic. ```python -# generated _output_parser.py (shape) -from .npc_response_payload import NpcResponsePayload # the @payloadRef VO (Pydantic v2 BaseModel) +# generated _response_parser.py (shape) +from .npc_response_response import NpcResponseResponse # the @responseRef record (Pydantic v2 BaseModel) -def parse_npc_response(text: str) -> NpcResponsePayload: +def parse_npc_response(text: str) -> NpcResponseResponse: """Validates text against the payload model. Raises: @@ -49,16 +51,20 @@ def parse_npc_response(text: str) -> NpcResponsePayload: ... ``` -For `@format: json|xml` outputs the generator additionally emits a **tolerant** -best-effort variant — `extract_lenient_(text) -> ExtractionResult[PayloadExtracted]` +Every responding prompt ALSO gets a **tolerant** best-effort variant — +`extract_lenient__with_loader(root, text) -> ExtractionResult[ResponseExtracted]` (from the `metaobjects` render `extract` engine) for cases where you want a classified -per-field report rather than a raise. The lenient mirror (`PayloadExtracted`) -uses `Optional[...]` fields — a missing/malformed component is `None`, not a raise. +per-field report rather than a raise. The lenient mirror (`ResponseExtracted`) uses +`Optional[...]` fields — a missing/malformed component is `None`, not a raise. -## The output-format prompt fragment (FR-010) +The STRICT `parse_*` is JSON-only (ADR-0053): an `@responseFormat: xml` reply gets the +tolerant path and no `parse_*` at all, because strict all-or-nothing semantics layered +over a REPAIRING XML reader would raise or accept based on how much repair happened. -For every json/xml-format `template.output`, the `output-prompt` generator (run via -`metaobjects gen`) emits one `_output_prompt.py` module exposing +## The response-format prompt fragment (FR-010) + +For every responding `template.prompt`, the `output-prompt` generator (run via +`metaobjects gen`) emits one `_response_format.py` module exposing `render__format(overrides=None) -> str`, backed by the render engine's `render_output_format()` — the "produce your answer like this" fragment for the model: @@ -67,11 +73,13 @@ model: metaobjects gen ./metadata --out ./generated --generators payload,output-prompt ``` -`@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`) -controls the fragment's presentation; guidance is never emitted as comments. Skipped -for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` — -the same skip contract as the `output-parser` generator. The baked spec's root name -is the payload class name, agreeing with the parser's `extract_()` root. +`@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`) +controls the fragment's presentation; guidance is never emitted as comments. Skipped for +`template.output` nodes and an unresolved `@responseRef` — the same skip contract as the +`output-parser` generator. There is NO format gate: the old `@format ∈ {json,xml}` test +read the syntax of the outbound body to decide whether to describe the reply, so a +text-bodied prompt asking for a JSON answer got no fragment. The baked spec's root name +is the response class name, agreeing with the parser's `extract_()` root. ## The three-step consumer pattern @@ -80,7 +88,7 @@ here) → parse the response with the generated parser: ```python from pydantic import ValidationError -from .npc_response_output_parser import parse_npc_response +from .npc_response_response_parser import parse_npc_response text = my_llm_client.complete(prompt_text) # YOUR code — no generated provider try: diff --git a/agent-context/skills/metaobjects-prompts/references/typescript.md b/agent-context/skills/metaobjects-prompts/references/typescript.md index c79e55081..e986763a4 100644 --- a/agent-context/skills/metaobjects-prompts/references/typescript.md +++ b/agent-context/skills/metaobjects-prompts/references/typescript.md @@ -1,15 +1,17 @@ # TypeScript parser-on-receipt -For every `template.output` in your metadata, the `outputParser()` generator (from -`@metaobjectsdev/codegen-ts/generators`) emits a **typed parser** that validates an -LLM/raw response against the template's `@payloadRef` payload value-object. This is -the receive side; codegen emits **no** provider/LLM-call layer — you compose the -call yourself. +For every RESPONDING `template.prompt` in your metadata — one declaring +`@responseRef` — the `outputParser()` generator (from +`@metaobjectsdev/codegen-ts/generators`) emits a **typed parser** that validates a +model's reply against that shape. ADR-0052: the tier binds `@responseRef`, never +`@payloadRef` (which types the request the prompt renders outbound), and a +`template.output` gets no parser at all. This is the receive side; codegen emits **no** +provider/LLM-call layer — you compose the call yourself. ## Contents - Wire the generator - What it emits -- The output-format prompt fragment (FR-010) +- The response-format prompt fragment (FR-010) - The three-step consumer pattern - Recommended LLM caller (bring-your-own) - Drift gate @@ -27,7 +29,7 @@ export default defineConfig({ generators: [ entityFile(), queriesFile(), barrel(), promptRender(), // render() per template.prompt (the send side) - outputParser(), // parse*/safeParse* per template.output (the receive side) + outputParser(), // parse*/safeParse* per responding template.prompt (the receive side) ], }); ``` @@ -37,9 +39,10 @@ options. ## What it emits -Per `template.output` (say named `NpcResponseOutput`, `@payloadRef: -"NpcResponsePayload"`), `meta gen` writes a self-contained `NpcResponseOutput.output.ts` -with a Zod schema + a dual API: +Per responding `template.prompt` (say named `NpcReview`, `@responseRef: +"NpcResponse"`), `meta gen` writes a self-contained `NpcReview.response.ts` with a Zod +schema + a dual API. The strict tier is JSON-only — an `@responseFormat: xml` reply gets +the tolerant `extract` and no `parse`/`safeParse`: ```ts import { z } from "zod"; @@ -76,11 +79,11 @@ including `field.enum` — falls through to `z.unknown()` in the strict in the entity insert/update schemas, not in this output parser; the lenient extract path carries the enum-as-string handling). -## The output-format prompt fragment (FR-010) +## The response-format prompt fragment (FR-010) -For every json/xml-format `template.output`, the `outputPrompt()` generator (same -import path, `@metaobjectsdev/codegen-ts/generators`) emits a -`.prompt.ts` exporting `renderFormat(overrides?)` — +For every responding `template.prompt`, the `outputPrompt()` generator (same import +path, `@metaobjectsdev/codegen-ts/generators`) emits a +`.responseFormat.ts` exporting `renderFormat(overrides?)` — backed by the render engine's `renderOutputFormat()`. This is the "produce your answer like this" fragment you splice into the prompt text so the model returns the shape the parser above expects: @@ -99,14 +102,16 @@ export default defineConfig({ }); ``` -`@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`) +`@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`) controls the fragment's presentation: `guide` is a prose field list + example skeleton, `inline` is one skeleton with inline placeholders/enum choices, `exampleOnly` is just the filled skeleton. Guidance is never emitted as comments — -models ignore them. Skipped for non-json/xml `@format` outputs and for any output -whose `@payloadRef` doesn't resolve to a value-object — the same skip contract as -`outputParser()`. The baked spec's root name matches the payload class name, so the -fragment and the parser's `extract()` agree on the same root. +models ignore them. Skipped for `template.output` and for any prompt whose +`@responseRef` doesn't resolve — the same skip contract as `outputParser()`. There is NO +format gate: the old `@format ∈ {json,xml}` test read the syntax of the outbound BODY to +decide whether to describe the REPLY. `@responseFormat` selects which syntax the fragment +teaches. The baked spec's root name matches the response shape, so the fragment and the +parser's `extract()` agree on the same root. ## The three-step consumer pattern @@ -164,16 +169,17 @@ recorder. ## Drift gate -`meta verify` walks every `template.output`'s `@payloadRef` resolution and fails the -build (exit 1, `(output)`-prefixed diagnostic) if a reference can't be resolved — -catching payload-VO ↔ parser drift at build time. The emitted parser imports `zod`; +`meta verify` walks every template's `@payloadRef` resolution and fails the build +(exit 1) if a reference can't be resolved. A `@responseRef` that does not resolve to a +value-object (or sourceless projection) is caught one layer earlier — it is a LOAD +error in every port. The emitted parser imports `zod`; it's usually already a dependency (Drizzle / `runtime-ts` lean on it), else `npm i zod`. ## See which fields a template consumes Run `meta docs` to emit the model surface to `./docs` — one page per -`template.output` at `docs/