fix(graph): auto-merge only schema-declared keys from llm output
state_updates::apply merged EVERY top-level key of an llm/agent node's JSON
output into graph state whenever the node had an output_schema, and
structured::extract never validated the output against that schema. Any
prompt text able to steer the model — including plan/diff text pasted from a
repo under review — could therefore plant an undeclared key (e.g.
verification_commands) into state and overwrite a caller-declared value
before a downstream script consumed it.
- apply() now takes the node's output_schema and merges only keys present in
schema.properties; undeclared keys are dropped. No schema, or a schema
without an object properties map, merges nothing.
- Nodes without output_schema are unchanged (None == the old false path);
explicit state_updates are unchanged and still see the full output via
{{output.<key>}}, which remains the author-controlled way to lift a key.
- Callers: llm.rs, agent.rs (x2), rag.rs (None).
- Sweep test bundled_graph_output_schemas_declare_every_key_relied_on: for
every bundled graph, every consumed state key has a declared writer
(initial_state, variable, state_updates target, map key, script-emitted key,
or an output_schema property), and no schema node's field list names a key
its schema omits — so this restriction cannot silently break a shipped
graph. It passes with no graph.yaml changes.
- graph.example.yaml documents the declared-keys-only merge; the residual
disclosed in 665fee9's README/docstring notes is now recorded as closed.
This commit is contained in:
1 parent
300c7aeff0
commit
27101909e0
9 files changed
+370
-43
No files matched your search
+9
-5
@@ -213,8 +213,10 @@ nodes:
|
||||
# agent/graph/global default when omitted.
|
||||
skill_instructions: null # Per-node skill-hint text override; uses the built-in default when omitted.
|
||||
output_schema: # Optional JSON Schema. The output is parsed to JSON
|
||||
type: object # and its top-level object keys auto-merge into state
|
||||
properties: # (so `topic` / `needs_deep_dive` become {{topic}} etc).
|
||||
type: object # and only the keys declared under `properties` auto-merge
|
||||
properties: # into state (so `topic` / `needs_deep_dive` become {{topic}}
|
||||
# etc). Undeclared keys the model emits are dropped; lift
|
||||
# one explicitly via state_updates: `{{output.<key>}}`.
|
||||
topic: { type: string }
|
||||
needs_deep_dive: { type: boolean }
|
||||
required: [topic, needs_deep_dive]
|
||||
@@ -284,8 +286,9 @@ nodes:
|
||||
type: array
|
||||
items: { type: string }
|
||||
required: [context, sources]
|
||||
# When `output_schema` is set, top-level keys auto-merge into state, so
|
||||
# `context` and `sources` are produced without needing `state_updates`.
|
||||
# When `output_schema` is set, the keys declared under `properties`
|
||||
# auto-merge into state, so `context` and `sources` are produced without
|
||||
# needing `state_updates`. Keys the schema does not declare are dropped.
|
||||
next: synthesize # Joins with retrieve at `synthesize`.
|
||||
|
||||
# --- script node (the diamond's join; also dispatches) -----------------
|
||||
@@ -369,7 +372,8 @@ nodes:
|
||||
# and is retired when the chain ends. A message delivered
|
||||
# after the chain's last flagged agent step is accepted
|
||||
# but never read.
|
||||
output_schema: # Optional. Same extraction as llm nodes
|
||||
output_schema: # Optional. Same extraction as llm nodes: only the keys
|
||||
# declared under `properties` auto-merge into state.
|
||||
type: object
|
||||
properties:
|
||||
summary: { type: string }
|
||||
|
||||
Reference in new issue
Block a user