Skip to content

Docs: write the Structured output page for kagent 1.x #518

Closed

Description

@Rachael-Graham

An AgentTemplate can now constrain its answer to a JSON Schema, and the 1.x doc set does not mention the feature anywhere. kagent#2890 landed the CRD fields, the translator, the runtime validation, the A2A response contract, the CLI, and the UI, so a reader can configure structured output today with nothing in the docs to tell them how.

This is new writing rather than an edit.

What the page covers

Declaring a JSON Schema for an agent's successful terminal response, and what the runtime does with a response that does not match.

The spine of the page is that the schema is a compile-time contract, not a request-time hint. The translator resolves the schema, validates it against a portable profile, and records a canonical form and digest in the immutable prepared revision. A schema the profile rejects fails compilation, so no agent runs at all, rather than failing later at inference. Lead with that and the admission rules below stop looking like a list of restrictions and start looking like one decision.

Upstream wrote the design document first. docs/architecture/structured-output.md is unusually complete and is the right starting point. It is an architecture document rather than a task guide, so it is a source, not a draft.

The configuration surface

Two mutually exclusive fields on AgentTemplateSpec, defined in go/api/v1alpha3/agenttemplate_types.go. Omitting both leaves the response unconstrained.

Field Meaning
outputSchema The JSON Schema, written inline.
outputSchemaFrom A name and key selecting a same-namespace ConfigMap that holds the schema as JSON.

Admission rules.

  • A CEL rule rejects a template that sets both, with the message outputSchema and outputSchemaFrom are mutually exclusive.
  • The schema must have an object root.
  • The portable profile supports primitive JSON types, nested objects and arrays, properties, required, additionalProperties, items, enum, const, anyOf, titles and descriptions, and non-recursive local references through $defs. External references, recursive references, conditional schemas and tuple arrays are rejected at compile time rather than silently weakened for a provider.
  • The serialized schema is limited to 64 KiB, and conversion is limited to 32 levels and 1,000 expanded nodes. Nodes reached through $defs count toward both bounds.
  • Only the kagent Harness supports structured output. The codex, claude and byo Harnesses fail compilation with Harness "<name>" does not support structured output.

Behavior the API does not announce

  • Two different conditions report a failure, and which one fires depends on what went wrong. An invalid schema, or a Harness that cannot run it, sets the Compatible condition to False with the reason UnsupportedConfiguration. A ConfigMap that does not exist, or a key that is missing from it, sets ResolvedRefs to False with the reason ReferenceResolutionFailed. A reader debugging a template needs to know which condition to read.
  • The contract belongs to the public root agent alone. A local child agent does not inherit it, and neither does an agent tool. If a child template is compiled and invoked as a root agent in its own right, its own schema applies to that separate revision.
  • There is no text fallback. A provider refusal, an incomplete generation, invalid JSON, or a validation failure fails the Task. The invalid output is not published and is not included in the error, because it may carry sensitive data, so the failure message alone will not show a reader what the model actually returned.
  • The response arrives as data, not text. The runtime suppresses partial root-answer fragments, validates the complete value, and publishes one A2A DataPart with media type application/json, carrying the canonical schema digest in kagent.dev/a2a/output-schema-sha256. The generated Agent Card advertises application/json as its default output mode. A streaming client that wants only the answer reads the last content-bearing artifact before the Task reaches TASK_STATE_COMPLETED.
  • Progress updates, tool events, approval requests and input-required messages are unchanged. Structured output constrains the terminal answer, not the conversation around it.
  • Tool calls work alongside it across OpenAI, Anthropic, Bedrock and Gemini. A provider or model can still refuse structured output at request time even though the schema compiled, and that failure fails the Task.

Where the file goes

docs-site/content/kagent/1.x/agents/structured-output.md sits with the other AgentTemplate behavior pages, alongside system-prompts.md. Placement is the one open question in this issue. The feature is a property of the portable template rather than of the runtime, which argues for agents/, but the harness restriction and the A2A response contract both pull toward the runtime pages. Decide before writing, and say which argument won.

What to check before starting

  1. Read docs/architecture/structured-output.md in the kagent repository.
  2. Read agents/system-prompts.md, which documents the closest existing pattern: an inline field and a ConfigMap-backed alternative on the same spec, including its error table.
  3. Confirm the limits and the profile against go/core/internal/translator/outputschema.go on the version you are documenting.
  4. Run an agent with a schema on a cluster, and run one that violates it, so that the failure behavior on the page is observed rather than inferred.

Done when

  • The page states that the schema is resolved and validated at compile time, and that an invalid schema stops an agent from running.
  • The field table covers outputSchema and outputSchemaFrom, and the mutual exclusion is stated with its CEL message.
  • The portable profile is described, including what it rejects and the three limits.
  • The harness restriction is stated, and was verified by pairing a schema with a claude or codex Harness on a cluster.
  • An error table maps each failure to the condition and reason it sets, distinguishing Compatible from ResolvedRefs.
  • The A2A response contract is documented, including the DataPart, the media type, the digest metadata key, and the absence of a text fallback.
  • The root-agent scope is stated, so a reader does not expect a child agent to inherit the schema.
  • Both examples on the page were run against a live agent.

Activity

  1. added
    documentationImprovements or additions to documentation
    help wantedExtra attention is needed
    and removed
    documentationImprovements or additions to documentation
    on Sep 23, 2026
  2. Rachael-Graham commented on Sep 25, 2026

    @Rachael-Graham
    ContributorAuthor

    Re-verified against kagent-dev/kagent main at 15e0fc15 on 2026-09-25. The feature shipped in v1.0.0-alpha3, cut at 375fe73a, and the scoping above holds. Two claims re-checked rather than assumed, plus one scope decision.

    The harness restriction is unchanged. go/core/internal/translator/compiler.go:215 still gates structured output to the kagent harness.

    The metadata key survived a refactor. kagent#2902 replaced the literal in go/api/a2a/structured_output.go with MetadataPrefix + "output-schema-sha256". MetadataPrefix is kagent.dev/a2a/, so the wire key is still kagent.dev/a2a/output-schema-sha256. The same pull request removed the legacy adk_* aliases, which does not affect this page.

    systemPromptFrom stays out of scope. kagent#2890 added outputSchemaFrom and systemPromptFrom together, and only the first belongs here. systemPromptFrom goes on agents/system-prompts.md, which already documents an inline field with a ConfigMap-backed alternative and carries the error table it would join. Tracking it separately rather than widening this issue.

    Confirm which release the page targets before writing the A2A section. For 1.0.0-alpha4, kagent#2933 adds an HTTP JSON-RPC transport and a per-instance agent card, so the card that advertises application/json as its default output mode is served over two transports rather than one.

  3. thc1006 commented on Sep 29, 2026

    @thc1006
    Contributor

    I'd like to work on this. I've got a draft that targets 1.0.0-alpha3, since that's what the 1.x docs install, and I've tried the examples and the failure cases on a kind cluster. I put the page under agents/, next to system-prompts.md.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

help wantedExtra attention is needed

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions