Skip to content

Support schema-validated component returns #176

Description

@taras

Why

Components currently produce rendered Markdown, and as captures that text as
a string. Agent workflows also need validated values for control flow, such as
verdicts, questions, findings, and lists.

The component contract uses one return path. A component either returns its
rendered text or explicitly declares and produces a typed value. It does not
produce presentation and structured data through separate return channels.

Root documents follow the same contract. A text root produces rendered Markdown;
a value root produces one schema-validated JSON result. Rendered body output may
remain observable while the final value stays the only successful result.

Component modes

Text components

A component without a returns declaration keeps the existing behavior:

  • its rendered Markdown is its return value;
  • <Output> may select which region renders;
  • invoking it normally renders that text;
  • invoking it with as binds the text and renders nothing.

The default return type is therefore text.

Value components

A returns declaration switches the component into value mode:

returns:
  type: object
  properties:
    passed: { type: boolean }
    revisionPrompt: { type: string }
  required: [passed]
  additionalProperties: false

The declaration is a JSON Schema and may describe any JSON value: string,
number, boolean, array, object, or null.

A Markdown value component produces exactly one value with a direct top-level
<Return>:

---
returns:
  type: object
  properties:
    passed: { type: boolean }
  required: [passed]
  additionalProperties: false
---

```js eval
const verdict = { passed: true };
```

<Return value={verdict} />

Value components:

  • render nothing;
  • must be invoked with as;
  • bind the validated value to as;
  • cannot declare <Output>;
  • contain exactly one direct top-level <Return>;
  • fail structural validation before body effects when <Return> is missing,
    duplicated, misplaced, or combined with <Output>.

Invoking a value component without as is an error. Callers render any desired
presentation from the captured value:

<Review as="review" />

<Show when={review.passed}>
Review passed.
</Show>

<Return> renders nothing and is not available in a text component.

Root documents

A root document uses the same text and value modes as a component, with one
difference: a value root has no caller and therefore does not require as.

A root without returns keeps the existing streaming Markdown behavior. A root
with returns:

  • executes its complete body;
  • contains exactly one direct top-level <Return>;
  • validates that value against its declared schema;
  • returns the validated JSON value from DocumentExecution;
  • does not treat rendered body Markdown as its successful result.

The root output stream remains an observability channel for body output. It is
not a second return value. Library consumers may observe it independently while
the execution completion carries the structured result.

Command-line output

xmd run reserves stdout for the successful result of a value root:

  • stdout contains only the final value encoded as valid JSON, followed by a
    newline; strings remain quoted JSON strings;
  • JSON output bypasses Markdown normalization and terminal formatting;
  • without --verbose, rendered body output is discarded;
  • with --verbose, rendered body output and journal diagnostics go to stderr;
  • failures write diagnostics to stderr, exit non-zero, and write no result to
    stdout.

Text roots remain unchanged: rendered Markdown goes to stdout, and --verbose
adds diagnostics on stderr.

Function components

Markdown and TypeScript components share the same declaration and validation
contract:

  • without export const returns, a function component returns its existing
    rendered string;
  • with export const returns, its generator returns a JSON value validated
    against that schema;
  • a value function component renders nothing and must be captured with as.

Root documents remain Markdown files; this change does not make TypeScript
function components executable as roots.

Validation and execution

  • The produced value is validated at the component or root boundary before it
    reaches the caller or execution result.
  • Invalid values fail with component and source context.
  • Values are restricted to JSON so capture and replay remain deterministic.
  • Persistence remains a separate caller-owned effect; returning a value does
    not choose where it is stored.

Inspection

inspectDocument({ path }) returns the document's effective return schema
without executing it:

const description = yield* inspectDocument({ path: "Review.md" });
const schema = description.returns;

DocumentInfo.returns is { type: "string" } for a text document with no
declaration. For a value document it is the validated schema declared under
returns. Inspection rejects an invalid return schema through the same
definition-loading path as execution and performs no document effects.

Acceptance criteria

  • Components and roots without returns preserve existing rendering and string
    results.
  • returns accepts schemas for every JSON value kind.
  • A Markdown value component binds its validated <Return> value through as
    and emits no segments.
  • A function value component follows the same schema and capture behavior.
  • A value root returns its validated JSON value and does not require as.
  • DocumentExecution completion exposes the structured root result while its
    output stream remains observability rather than a second return value.
  • xmd run writes only the final JSON result of a successful value root to
    stdout.
  • xmd run --verbose writes value-root body output and journal diagnostics to
    stderr without contaminating stdout.
  • A failed value root exits non-zero and writes no JSON result to stdout.
  • Invalid return values fail at the component or root boundary.
  • inspectDocument().returns reports the default text schema or the declared
    value schema without executing the document.
  • Invalid return schemas fail consistently during inspection and execution.
  • Missing, duplicate, nested, or misplaced <Return> declarations fail before
    body effects.
  • <Output> and returns are mutually exclusive.
  • Invoking a value component without as fails before body effects.
  • Tests cover scalar, array, object, and null values; schema failure; text-mode
    compatibility; function components; root API and CLI behavior; nested
    composition; and deterministic replay.
  • Author documentation shows a normal text component, a captured value
    component, and a structured root result.

Not included

  • Separate presentation and structured-return channels on one component.
  • TypeScript function components as root documents.
  • Non-JSON return values.
  • Implicit serialization of structured values into Markdown.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions