refactor(transformers): rename spawn_metadata to invocation_metadata

This commit is contained in:
Nick Hollon
2026-05-07 16:54:19 -04:00
parent 30b0ebe5bf
commit 4504f85157
2 changed files with 28 additions and 28 deletions
+22 -22
View File
@@ -354,21 +354,21 @@ class LifecyclePayload(TypedDict, total=False):
graph_name: NotRequired[str]
trigger_call_id: NotRequired[str]
cause: NotRequired[dict[str, Any]]
"""Optional generic descriptor of what spawned this subgraph. Forwarded
"""Optional generic descriptor of what triggered this subgraph. Forwarded
by protocol layers as the wire `lifecycle.started.cause` field.
Shape:
- `{"type": "tool_call", "subagent_type": "<name>", "description": "<text>"}`
— set when the subgraph was spawned by a tool invocation routed
— set when the subgraph was triggered by a tool invocation routed
through `langgraph.prebuilt.ToolNode`. Mined from the per-call
dispatched task's `input.tool_call.args`. Lets consumers attribute
`lifecycle.started` to the spawning intent without needing the
`lifecycle.started` to the invoking intent without needing the
model's `tool_call_id` (consumers join on the existing
`trigger_call_id` field, which is the unique pregel task id of the
spawn).
invocation).
Absent for structurally-spawned subgraphs (parallel branches via
Absent for structurally-triggered subgraphs (parallel branches via
`Send` without ToolNode, nested `graph.invoke()`, etc.)."""
error: NotRequired[str]
@@ -410,13 +410,13 @@ class _TasksLifecycleBase(StreamTransformer):
# Maps tracked namespace -> task_id of the parent task whose
# `TaskResultPayload` will close it.
self._open: dict[tuple[str, ...], str] = {}
# Maps task_id -> spawn metadata for tasks whose `input` looked
# Maps task_id -> invocation metadata for tasks whose `input` looked
# like a `{"tool_call": {...}, ...}` envelope (the shape
# `langgraph.prebuilt.ToolNode` Send-fans out via
# `ToolCallWithContext`). The lifecycle hook joins on this
# when a child subgraph fires its first task event so it can
# attribute the spawn to the model tool call's args.
self._spawn_metadata: dict[str, dict[str, str]] = {}
# attribute the invocation to the model tool call's args.
self._invocation_metadata: dict[str, dict[str, str]] = {}
# --- Template-method hooks (subclass overrides) ---
@@ -429,14 +429,14 @@ class _TasksLifecycleBase(StreamTransformer):
ns: tuple[str, ...],
graph_name: str | None,
trigger_call_id: str | None,
spawn_metadata: dict[str, str] | None = None,
invocation_metadata: dict[str, str] | None = None,
) -> None:
"""Fired once per discovered namespace (first observed task event).
`spawn_metadata` carries spawn-intent fields mined from the
`invocation_metadata` carries invocation-intent fields mined from the
per-call dispatched task's `input.tool_call.args` envelope:
`{"subagent_type": str, "description": str}` (either or both
may be absent). `None` for structurally-spawned subgraphs.
may be absent). `None` for structurally-triggered subgraphs.
Consumers join on `trigger_call_id` (the pregel task id) for
identity; this dict is purely descriptive metadata.
"""
@@ -474,13 +474,13 @@ class _TasksLifecycleBase(StreamTransformer):
# so we capture parent tasks that themselves live outside the
# tracked region but whose `id` will appear as `trigger_call_id`
# for a child subgraph.
self._record_spawn_metadata(data)
self._record_invocation_metadata(data)
if not self._should_track(ns) or ns in self._seen:
return
self._seen.add(ns)
graph_name, trigger_call_id = _parse_ns_segment(ns[-1])
spawn_metadata = (
self._spawn_metadata.get(trigger_call_id)
invocation_metadata = (
self._invocation_metadata.get(trigger_call_id)
if trigger_call_id is not None
else None
)
@@ -488,13 +488,13 @@ class _TasksLifecycleBase(StreamTransformer):
ns,
graph_name or None,
trigger_call_id,
spawn_metadata,
invocation_metadata,
)
if trigger_call_id is not None:
self._open[ns] = trigger_call_id
def _record_spawn_metadata(self, data: dict[str, Any]) -> None:
"""Remember `task_id -> spawn metadata` if the task input matches
def _record_invocation_metadata(self, data: dict[str, Any]) -> None:
"""Remember `task_id -> invocation metadata` if the task input matches
a recognized per-call tool-dispatch shape.
Two shapes are accepted (both duck-typed so 3rd-party tool runners
@@ -541,7 +541,7 @@ class _TasksLifecycleBase(StreamTransformer):
if isinstance(description, str):
metadata["description"] = description
if metadata:
self._spawn_metadata[task_id] = metadata
self._invocation_metadata[task_id] = metadata
def _pop_terminal_transitions(
self, ns: tuple[str, ...], data: dict[str, Any]
@@ -638,7 +638,7 @@ class LifecycleTransformer(_TasksLifecycleBase):
ns: tuple[str, ...],
graph_name: str | None,
trigger_call_id: str | None,
spawn_metadata: dict[str, str] | None = None,
invocation_metadata: dict[str, str] | None = None,
) -> None:
if trigger_call_id is None:
# Without a task id we can't correlate a parent-result
@@ -649,9 +649,9 @@ class LifecycleTransformer(_TasksLifecycleBase):
if graph_name:
payload["graph_name"] = graph_name
payload["trigger_call_id"] = trigger_call_id
if spawn_metadata:
if invocation_metadata:
cause: dict[str, Any] = {"type": "tool_call"}
cause.update(spawn_metadata)
cause.update(invocation_metadata)
payload["cause"] = cause
self._channel.push(payload)
@@ -715,7 +715,7 @@ class SubgraphTransformer(_TasksLifecycleBase):
ns: tuple[str, ...],
graph_name: str | None,
trigger_call_id: str | None,
spawn_metadata: dict[str, str] | None = None, # noqa: ARG002
invocation_metadata: dict[str, str] | None = None, # noqa: ARG002
) -> None:
if self._mux is None:
return
@@ -41,7 +41,7 @@ def _tasks_start(
Pass `input={"tool_call": {"args": {...}}}` (or any envelope with
that shape) to exercise the lifecycle transformer's input mining of
spawn-intent metadata (`subagent_type`, `description`) — this
invocation-intent metadata (`subagent_type`, `description`) — this
mirrors the `ToolCallWithContext` payload `langgraph.prebuilt.ToolNode`
Send-fans out per tool call.
"""
@@ -134,13 +134,13 @@ def test_started_emitted_on_first_direct_child_task() -> None:
assert payload["trigger_call_id"] == "abc123"
def test_started_carries_cause_when_parent_input_has_spawn_metadata() -> None:
def test_started_carries_cause_when_parent_input_has_invocation_metadata() -> None:
"""When a parent task's `input` is a `ToolCallWithContext`-shaped
envelope (`{"tool_call": {"args": {...}}, ...}`, the layout
`langgraph.prebuilt.ToolNode` Send-fans out per call), the
transformer mines `subagent_type` and `description` from
`tool_call.args` and remembers them keyed by `parent_task_id`.
When that parent task spawns a subgraph (the child's namespace
When that parent task triggers a subgraph (the child's namespace
ends in `name:<parent_task_id>`), the `lifecycle.started` payload
carries `cause = {"type": "tool_call", "subagent_type": ..., "description": ...}`.
Consumers join on `trigger_call_id` (the pregel task id) for
@@ -181,7 +181,7 @@ def test_started_carries_cause_when_parent_input_has_spawn_metadata() -> None:
def test_started_cause_with_description_but_no_subagent_type() -> None:
"""Partial spawn metadata (only `description`, or only `subagent_type`)
"""Partial invocation metadata (only `description`, or only `subagent_type`)
still produces a cause — both fields are optional within the dict."""
mux = _build_lifecycle_mux()
mux.push(
@@ -322,8 +322,8 @@ def test_list_shape_robust_to_non_dict_or_missing_args() -> None:
assert "cause" not in payload3
def test_started_omits_cause_for_structurally_spawned_subgraph() -> None:
"""Subgraphs spawned without a recognizable tool-call envelope on
def test_started_omits_cause_for_structurally_triggered_subgraph() -> None:
"""Subgraphs triggered without a recognizable tool-call envelope on
the parent's input (Send with custom payloads, plain nested
`graph.invoke`, etc.) don't get a `cause` field on
`lifecycle.started`."""