# matrx-ai agents — JSON parsing & structured output

Cross-repo system-of-record: `/Users/armanisadeghi/code/common-docs/systems/agent-variable-binding/FEATURE.md` — read it before touching `NamedAgent.Inputs`, `variable_map`, `prepare_variables`, or `validate()` in ANY repo. It governs how code-side values reach an agent and the mapping vocabulary implemented here.

matrx-ai owns the **one funnel** for turning raw LLM text into structured data. Every consumer (this package, aidream routers, tools) goes through it — never a hand-rolled scraper.

> Entry-point signatures and Gemini grounded structured output verified against `response_parser.py` / `output.py` / `definition.py` / `providers/google/translator.py` 2026-08-17. A `FEATURE.md` is a promise it is currently true — re-verify after a gap.

## Entry points (all in `matrx_ai.agents`, none raise)

- **`response_parser.extract_json(text, *, schema=None, return_all=False, nth=None, detailed=False)`** — the single entry point. Default call is the simple "first valid JSON block, or `None`" (backward-compatible). Kwargs unlock schema-aware filtering, multi-block collection, the Nth match, and the `JsonExtraction(data, success, reason, all_matches)` wrapper.
- **`output.parse_agent_output(text, source=None)`** — convenience wrapper; pulls a JSON Schema from any reasonable source (raw schema dict, agent DB record, `response_format` envelope, an `Agent` instance) and always returns a `JsonExtraction`. Use when you have an agent in hand.
- **`response_parser.extract_model(text, MyModel)`** — Pydantic-validated extraction (`extract_model[T: BaseModel]`).

## What the funnel handles for free

Markdown code fences (with or without a `json` tag), `<thinking>` blocks, trailing commas, JSON embedded in prose, multiple JSON blocks in one response, schema-driven candidate selection, and shape normalization between bare arrays and `{key: array}` wrappers.

## Schema-aware extraction from a DB agent

Agents loaded from the DB carry `Agent.output_schema` (from the `agx_agent.output_schema` column via `AgentConfig.output_schema`, set in `definition.py`). Pass it as the second arg to `parse_agent_output` for automatic schema-aware extraction — reference call site: `aidream/api/routers/page_extraction.py`.

## Automatic `STRUCTURED_OUTPUT` stream event

`Agent.execute()` emits a `STRUCTURED_OUTPUT` stream event whenever the agent has an `output_schema` set, regardless of the invoking route (test: `agents/tests/test_structured_output_emit.py`). The event carries the schema verbatim **and** the parsed payload, so the FE drives generic renderers (tables, forms, typed accessors) without re-running extraction. Fires on success **or** failure — `success=False` is the explicit signal that the agent declared a contract the parser couldn't honour. Wire shape + FE typing: [`packages/matrx-connect/CLAUDE.md`](../../../matrx-connect/CLAUDE.md) ("STRUCTURED_OUTPUT" section).

## Provider-native structured output

**Live and kind-bound for active definitions.** `aidream/services/ai_execution/ai_task.py` is the shared route funnel: when an agent declares `output_schema` and the caller did not supply a real schema, it derives the stable Content IR output contract and resolves `UnifiedConfig.response_format` through `response_format_for_kind`. **Hydration also fires over a bare `{"type": "json_object"}`** — that is a format hint, not a contract, and the agent's declared `output_schema` is strictly stronger (an agent saved with `json_object` otherwise loses structured output on every path that doesn't send the FE's `json_schema` placeholder — the workflow `ai.agent.start` node, found live 2026-08-08). Second, independent layer: `graph_nodes/shared.py::_extract_structured_output` parses a `json_object` result schema-less so the graph payload still carries the object. `USE_CONTENT_IR_AGENT_RESPONSE_FORMAT = False` restores the copied-schema route. Missing kinds and historical pinned versions fall back to their copied schema explicitly, because binding an old version to the current base-agent kind could enforce the wrong contract. The chosen source is recorded in request metadata. The orchestrator parses once and emits `kind`, `kind_version`, `kind_checked`, and `kind_errors` on the existing `STRUCTURED_OUTPUT` event without changing the business payload. The live publisher currently covers all 68 active agents with structured output schemas.

🚨 **`__kind` is re-hoisted to the FIRST property at the request boundary — never
"fix" this by trusting stored property order.** A grammar-constrained model emits
an object's keys in the order the schema's `properties` map declares them (proven
live on gemini-3.7-flash 2026-08-18: swapping `properties` swaps the wire order;
swapping `required` changes nothing). So the discriminator's position in the
schema IS its position on the wire, and a live surface cannot route a streaming
payload until `__kind` has arrived. **That order does not survive storage:**
`content_ir.kind_definition.emitted_json_schema` is a **jsonb** column and jsonb
sorts object keys by (length, bytewise). The flashcard generator's authored
`["__kind","title","cards"]` came back out of the registry as
`["cards","title","__kind"]`, the model emitted `__kind` as the last key of a
15-second payload, and the card-by-card preview showed a spinner for the whole
run while the stream underneath it was perfectly healthy.
`matrx_ai.schema.rules.hoist_discriminator_first` therefore runs in the lint
gate's `_make_portable` (so every `portable_schema` /
`response_format_for_kind` binding is `__kind`-first) **and** in
`BaseTranslator.sanitize_structured_output_schema`, the seam all four provider
boundaries call — applied even for providers that strip nothing, which is the
exact path this run took. Same doctrine as `rewrite_const_as_enum`: the stored
schema is untouched, so no data migration is needed and any jsonb round-trip
anywhere is undone on the way out. Pinned by
`packages/matrx-ai/tests/test_schema_rules_kind_first.py`.

**Typed programmatic runs are code-bound.** Passing a Pydantic model as
`run_agent(json_schema=...)` (including every `NamedAgent.Output`) derives a
provider-portable strict schema through `response_format_for_model`, replaces
any incomplete or stale saved response-format placeholder before the request,
and sets `Agent.output_schema` to that same envelope. If the model cannot form
an enforceable object-root contract, execution fails before the paid call.
Pydantic validation still runs after the response as the independent loud
backstop. The declared Python type is therefore the single contract at both
boundaries; a saved agent cannot silently downgrade Gemini to prompt-only JSON.
Provider translators silently remove schema keywords their target cannot accept;
that expected request normalization is not an error or operator event. Actual
provider rejections still propagate through the normal provider error path.
`run_agent` also treats a contained `CompletedRequest` with
`metadata.status="failed"` as an execution failure and skips structured parsing.
Any partial provider buffer remains available on `AgentRunResult.output` for
diagnostics, but it can never be mislabeled as a successful malformed-JSON
response.

**Programmatic tool isolation is definition-faithful.** `AgentConfig`'s
`auto_tools_disabled` value is carried onto `Agent`, survives cloning, and is
passed to the host preparation hook before automatic surface/capability tools
are merged. A `NamedAgent` may declare the same flag as a code-side safety
override for a machine-consumed contract; `None` preserves the resolved
definition. This is the direct-run equivalent of the HTTP agent-start policy,
not a second tool-merging path.

**Gemini Search uses provider-native structured output in one call.**
`GoogleTranslator.to_google` sends native `googleSearch`,
`response_mime_type="application/json"`, and `response_json_schema` together on
the streamed request. The temporary grounded-digest text containment is gone.
This combination passed 12/12 genuinely grounded, citation-heavy Gemini 3.7
Flash trials on 2026-08-17: every concatenated stream parsed as complete JSON,
contained exactly five researched items and ten Google grounding redirect URLs,
and passed the full required-field schema. Google's same-day docs now explicitly
document Gemini 3 structured output with built-in Google Search and say streamed
chunks concatenate into the final JSON object. **Do not disable Search, add a
repair call, or replace Google Search.** `extract_json`, kind validation, and
`STRUCTURED_OUTPUT` remain the independent downstream enforcement layers.

## Cost-only system runs in a user-facing stream

`run_agent(system_run=True)` remains cost-spine-only and muted by default.
User-facing multi-stage pipelines that must expose their AI output in real time
set `stream_system_run=True`; this preserves the caller's emitter while keeping
`system_run=True`, skipping throwaway chat transcripts and disabling client
delegation. Aidream's `run_one_agent(..., stream_output=True)` is the host
convenience surface. This is a delivery switch, not a second persistence mode.

When a host configures `internal_run_tracker`, its open is a mandatory
pre-provider gate. A tracker exception is logged red and aborts before
`Agent.execute`; the executor never converts it into “run unaffected.” This is
what lets aidream guarantee that every programmatic paid call has a runtime
cost execution and can carry a tree dollar ceiling. The executor also awaits
the host tracker's settle contract after `Agent.execute`; hosts decide the
durability posture, and aidream does not return from that settle attempt until
its isolated terminal DB task finishes.

## Agent inputs — faithful variable → tool-schema projection

**`named.resolve_variable_mapping` is the one code→agent binding resolver.**
Mappings are keyed by the agent variable and use `code_value`, `direct_value`,
or `unmapped`; `prompt_user` is parsed only to reject it with a clear
server-side error. Agent defaults remain untouched when code has no value.
Undeclared code values are never passed through silently: they become
`dropped`, or `spilled_to_user_input` when explicitly named for spill. Only
`missing_from_code`, `required_unmapped`, and lossy `type_mismatch` verdicts
block. `NamedAgent.run` loads the resolved source before applying this contract.
`NamedAgent.validate(source_override=...)` labels its report `resolved`. A
mandated class (`mandate_key`, no `source`) resolves the live Holder and also
labels `resolved`. An un-mandated class with a `source` still reports as
`seed`. **`_check_definition` refuses a class that declares both** — the
ship-time validator records that as a report error and never crashes the run.

**`variable_schema.py` is the ONE converter** from `variable_definitions` JSONB to projected tool parameters (`agent_projection._variable_definitions_to_parameters` delegates to it). Full fidelity per the frontend bridge's semantics: option sets, **open enums (`allowOther`) as `anyOf: [{enum}, {string}]`**, defaults, descriptions, numeric bounds, checkbox items enums, toggleValues as a 2-value enum of the labels. **Wire values stay strings** — dispatch (`agent_tool._merge_projected_variables`) `json.dumps`-es non-string args; never change runtime value handling here, only schema fidelity. Picklist (`structured_list`) and scope (`binding`) bindings are server-side provenance (`AgentVariable.picklist_binding/scope_binding`) — they NEVER become schema constructs a caller could forge. Tests: `tests/test_variable_schema_fidelity.py` (18 live variable docs shared with the frontend bridge suite). Bulk-binding the 578 variable-bearing agents to kinds is deferred until the bridge soaks; see `/Users/armanisadeghi/code/common-docs/systems/content-ir-system/FEATURE.md` (gap 6).

**Automatic assignment:** `auto_assignment.py` owns the exact
`{"type":"auto_assign","strategy":"random"}` request model and derives eligible
static/structured-list sources from the trusted `AgentVariable.custom_component`
(`assignment.random=true`). `Agent.apply_variables()` delegates actual selection to
the host-injected `auto_assign_resolve_sync` seam before picklist resolution. Ordinary
scalar values keep their historical string coercion; only the tagged marker remains
structured through this boundary. Aidream is the host implementation and uses
`secrets.choice`.

**Coordinated assignment:** `ai.agent.assignment_batch` is deliberately outside
the normal agent definition/execution path. Its strict request carries an
ordinary `AgentStartStrictInput` plus a paired, random, or cartesian plan. The
standalone `matrx-assignment` engine resolves and durably tracks items, then the
action passes each resolved variable map through the same injected `agent_runner`
as `ai.agent.start`. The agent remains unaware of assignment sessions. Both the
batch input and result are automatically published as Content IR `action_io`
contracts.

## Forbidden (in any consumer — package, router, service, tool)

Any new `re.search(r"\`\`\`json…")`, `text.find("[")…text.rfind("]")`, or `json.loads(raw_llm_text)`. Use the funnel.

Background conversation labeling commits its title/description mutation through
an isolated coordinator using the exact `chat.conversation` registry identity.
It must never queue a bare `conversation` target.

## Change Log

- 2026-08-18 — A mandated `NamedAgent` carries `mandate_key` and no `source`.
  `validate()` resolves the live Holder (or records a report error); a class
  that still declares both no longer crashes `validate_all_named_agents`.
- 2026-08-17 — Awaited the host's internal-run settlement contract through its
  terminal attempt so a short-lived host cannot exit after metering but before
  the runtime outcome write.
- 2026-08-17 — Preserved `auto_tools_disabled` through direct/programmatic
  `Agent` and `NamedAgent` execution so machine-consumed structured calls do not
  inherit unrelated surface tools.
- 2026-08-17 — Removed Gemini's grounded-digest containment after 12/12
  genuinely grounded Gemini 3.7 Flash streams returned complete,
  schema-conformant provider-native JSON with Google Search enabled.
- 2026-08-17 — Made the configured internal-run tracker fail closed before
  provider execution; paid programmatic runs no longer proceed when their cost
  ledger cannot open.

- 2026-08-15 — Contained Gemini's Search + streamed structured-output byte-loss
  defect in one call: native Search stays enabled, a grounded digest precedes
  fenced JSON, and the canonical parser preserves the structured payload.
- 2026-08-15 — Added the complete code-side ValueMapping vocabulary, typed
  scenario verdicts, explicit drop/spill handling, default fallback, lossless
  coercion, and resolved-source validation while retaining seed validation.
- 2026-08-14 — Pinned the schema-qualified coordinator identity used by
  background conversation labeling.
- 2026-08-09 — Graph agent nodes fail with `structured_output_invalid` when a declared provider-enforced JSON Schema produces no validated payload. Plain-text completion can no longer masquerade as a successful structured workflow step.
- 2026-08-08 — Described the complete coordinated-assignment agent request and
  durable planner/result surface so workflow forms and agents receive usable schemas.
- 2026-08-08 — Hydrate `response_format` over a bare `json_object` when the agent declares `output_schema` (fixes `structured_output=None` on the workflow `ai.agent.start` path, run defc22b1); added schema-less `json_object` parsing as the second layer in `graph_nodes/shared.py`.
- 2026-07-23 — Propagated contained orchestrator failures through
  `AgentRunResult.success=False`, preventing partial buffers from being parsed
  and reported as secondary JSON errors.
- 2026-07-23 — Removed the success-path console announcement from expected
  provider schema normalization; conversion remains covered at every boundary.
- 2026-07-23 — Allowed explicit real-time streaming for cost-only system agents
  without enabling throwaway chat persistence.
- 2026-07-22 — Bound every typed `run_agent` / `NamedAgent.Output` call to a provider-native Pydantic-derived schema before execution, with pre-spend refusal and post-response validation guards.
- 2026-07-18 — Added the typed automatic-assignment marker, trusted binding projection, and host resolver seam used before normal variable/picklist substitution.
- 2026-07-18 — Added the strict, durable `ai.agent.assignment_batch` action over the independent `matrx-assignment` coordinator and Content IR action contracts.
- 2026-07-15 — **Agent-input bridge (W3-A):** added `variable_schema.py` (faithful variable → tool-parameter conversion; replaces the everything-→-string projection); `ToolDefinition._build_json_schema` gained typeless-`anyOf` pass-through + `multipleOf`; wire-string invariant pinned in `tests/test_variable_schema_fidelity.py`.
- 2026-07-15 — Routed active-agent provider schemas through registered kinds with a reversible copied-schema fallback and documented pinned-version safety.
- 2026-07-15 — Documented live Content IR binding and verdict fields for all active structured agent outputs.
- 2026-07-15 — Corrected stale provider-native status after verifying the shared `run_ai_task` response-format hydration and orchestrator emission chokepoint; linked the remaining kind-binding work.
- 2026-06-09 — Created. Extracted the parsing-funnel detail from root `CLAUDE.md`; verified entry-point signatures + `output_schema` + `STRUCTURED_OUTPUT` against live code.
