Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
132aebf
docs(adr): ADR-0053 — the reply's syntax is @responseFormat on templa…
dmealing Aug 19, 2026
106ed94
feat(metamodel)!: @promptStyle moves to template.prompt; add @respons…
dmealing Aug 19, 2026
3bb1cda
feat(codegen-ts)!: the inbound tier keys on template.prompt @responseRef
dmealing Aug 19, 2026
60d0b8b
feat(codegen-ts)!: an XML reply gets the tolerant extract and nothing…
dmealing Aug 19, 2026
a516a61
fix(codegen-ts): the strict schema's contract is the metadata's contract
dmealing Aug 19, 2026
27a9290
test(codegen-ts): re-point inline inbound fixtures onto the respondin…
dmealing Aug 19, 2026
b0c5f6d
docs(plan): record ADR-0052 branch status and the precise remaining work
dmealing Aug 19, 2026
b3f9a4c
docs(plan): the trace helper is a fifth inbound consumer, in every port
dmealing Aug 19, 2026
183d6c5
test(codegen-ts): re-point the collision + extractor tiers at the res…
dmealing Aug 19, 2026
9b834ac
fix: four defects an independent review found in this branch's own work
dmealing Aug 19, 2026
696eebf
fix(codegen-ts): api-docs documents the inbound tier on the respondin…
dmealing Aug 19, 2026
f84dcb0
fix(codegen-ts): restore the render example's payload shape after the…
dmealing Aug 19, 2026
c315e4b
fix(codegen-ts)!: the trace helper parses the reply with @responseFormat
dmealing Aug 19, 2026
8243399
test(codegen-ts): the inbound golden moves to the responding prompt
dmealing Aug 19, 2026
4469131
feat(codegen-csharp)!: the inbound tier keys on template.prompt @resp…
dmealing Aug 19, 2026
bedf0cb
docs(plan): the C# gaps go first, and Phase F is bigger than described
dmealing Aug 20, 2026
9809d91
test(codegen-csharp): the inbound tier's three C# gates, each proven …
dmealing Aug 20, 2026
364a0a2
feat(codegen-spring)!: the inbound tier keys on template.prompt @resp…
dmealing Aug 20, 2026
094f580
fix(codegen-csharp)!: a @responseRef that is not a payload target emi…
dmealing Aug 20, 2026
0acbff5
fix(codegen-csharp): a prompt's @payloadRef gets a record, and api-do…
dmealing Aug 20, 2026
34cbd29
feat(codegen-kotlin)!: the inbound tier keys on template.prompt @resp…
dmealing Aug 20, 2026
06ece35
fix(loader)!: @responseRef obeys the same target rule as @payloadRef …
dmealing Aug 20, 2026
0601916
docs: the roadmap targeted a breaking change at a release that cannot…
dmealing Aug 20, 2026
82f66f2
docs(roadmap): conform the Release cells, and rule out the 1.0-cut sl…
dmealing Aug 20, 2026
ecbfe23
feat(codegen-python): FindInbound — the ADR-0052 direction rule, in o…
dmealing Aug 20, 2026
83c2b21
feat(codegen-python)!: the inbound tier keys on template.prompt @resp…
dmealing Aug 20, 2026
b5fb015
fix(docs): the shared api-docs corpus lost its inbound half, and noth…
dmealing Aug 20, 2026
af5abcf
fix(codegen-spring)!: the trace helper parses the reply with @respons…
dmealing Aug 20, 2026
d4a12e6
docs(adr-0052): the migration guide, and every place that taught the …
dmealing Aug 20, 2026
3b0f484
Merge origin/main into adr-0052-template-direction
dmealing Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, `<Prompt>Response`, beside `<Prompt>Payload`;
Python puts it in its own `<prompt>_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/<lang>/<pkg>/<Prompt>.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
Expand Down Expand Up @@ -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
Expand Down
83 changes: 52 additions & 31 deletions agent-context/skills/metaobjects-prompts/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
Expand Down Expand Up @@ -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
`render<Name>Format(...)`-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
`render<Name>Format(...)`-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 |
Expand Down
47 changes: 26 additions & 21 deletions agent-context/skills/metaobjects-prompts/references/csharp.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -27,12 +30,14 @@ dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog

## What it emits

Per `template.output`, `dotnet meta gen` writes one `<TemplateName>.output.cs` with a
static `<TemplateName>Parser` following the .NET BCL `Parse`/`TryParse` dual API —
Per responding `template.prompt`, `dotnet meta gen` writes one
`<PromptName>.response.cs` with a static `<PromptName>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 <TemplateName>.output.cs (shape)
// generated <PromptName>.response.cs (shape)
public static class NpcResponseParser
{
/// <exception cref="JsonException">malformed JSON or schema mismatch.</exception>
Expand All @@ -53,25 +58,25 @@ generator also emits a tolerant `Extract(string[, ExtractOptions])` (self-contai
components) returning an `ExtractionResult` with a nullable `<Payload>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
`<TemplateName>.prompt.cs` declaring a static `<TemplateName>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 `<PromptName>.responseFormat.cs`
declaring a static `<PromptName>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

Expand Down
Loading
Loading