From 4504f85157a6142650ed5cb75dc6173073a6b0bc Mon Sep 17 00:00:00 2001 From: Nick Hollon Date: Thu, 7 May 2026 16:01:19 -0400 Subject: [PATCH] refactor(transformers): rename spawn_metadata to invocation_metadata --- .../langgraph/stream/transformers.py | 44 +++++++++---------- .../test_stream_lifecycle_transformer.py | 12 ++--- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/libs/langgraph/langgraph/stream/transformers.py b/libs/langgraph/langgraph/stream/transformers.py index 53ca1a79a..4421744a6 100644 --- a/libs/langgraph/langgraph/stream/transformers.py +++ b/libs/langgraph/langgraph/stream/transformers.py @@ -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": "", "description": ""}` - — 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 diff --git a/libs/langgraph/tests/test_stream_lifecycle_transformer.py b/libs/langgraph/tests/test_stream_lifecycle_transformer.py index 768f120d4..86baeb105 100644 --- a/libs/langgraph/tests/test_stream_lifecycle_transformer.py +++ b/libs/langgraph/tests/test_stream_lifecycle_transformer.py @@ -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:`), 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`."""