stop naming ToolCallWithContext in lifecycle docstrings

`ToolCallWithContext` is an internal prebuilt type — public-facing
docstrings shouldn't be calling it out by name. Describe the two
recognised per-call envelope shapes (single-element list of tool-call
dicts, dict wrapping a `tool_call`) by structure instead, and lead
with the list shape since that's what `langchain.agents.create_agent`
currently dispatches.
This commit is contained in:
Nick Hollon
2026-05-08 10:04:49 -04:00
parent 837e1ba6d2
commit fc297fc576
2 changed files with 40 additions and 38 deletions
+21 -20
View File
@@ -343,16 +343,17 @@ def _parse_ns_segment(segment: str) -> tuple[str, str | None]:
def _extract_per_call_args(payload: Any) -> tuple[dict[str, Any], str | None] | None:
"""Pull `(args, tool_call_id)` out of a per-call dispatched task's `input`.
Two shapes are recognised; both are duck-typed so 3rd-party tool
runners that mimic the layout participate without importing prebuilt
types:
Two shapes are recognised; both are duck-typed so any tool runner
that mimics the layout participates without naming any specific
dispatcher's types:
1. `ToolCallWithContext`-style envelope used by
`langgraph.prebuilt.ToolNode` Send-fan-out:
`{"tool_call": {"id": ..., "args": {...}, ...}, ...}`.
2. Single-element list of tool-call dicts used by langchain v1's
`create_agent` Send-fan-out:
`[{"id": ..., "name": ..., "args": {...}}]`.
1. Single-element list of tool-call dicts:
`[{"id": ..., "name": ..., "args": {...}}]`. The current public
shape — `langchain.agents.create_agent` Send-fans this out per
pending tool call.
2. Dict envelope wrapping a tool call:
`{"tool_call": {"id": ..., "args": {...}, ...}, ...}`. Older
prebuilt agent paths Send-fan-out this shape.
Returns `None` if the payload doesn't match either shape or its
`args` isn't a dict. `tool_call_id` is `None` when the envelope
@@ -395,10 +396,11 @@ class LifecyclePayload(TypedDict, total=False):
Shape:
- `{"type": "tool_call", "subagent_type": "<name>", "description": "<text>",
"tool_call_id": "<id>"}` — 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`. `subagent_type` and
`description` describe the invoking intent; `tool_call_id` is the
"tool_call_id": "<id>"}` — set when the subgraph was triggered by a
per-call tool dispatch (a model tool call routed through whatever
tool node the agent uses). Mined from the per-call task's `input` —
see `_extract_per_call_args` for the recognised shapes. `subagent_type`
and `description` describe the invoking intent; `tool_call_id` is the
model-side id of the originating tool call, exposed so UI consumers
can anchor the lifecycle event back to the AI message that dispatched
it (the per-call Send fan-out gives each tool_call its own pregel task,
@@ -410,7 +412,7 @@ class LifecyclePayload(TypedDict, total=False):
a `cause` if at least one field was extractable.
Absent for structurally-triggered subgraphs (parallel branches via
`Send` without ToolNode, nested `graph.invoke()`, etc.)."""
`Send` with non-tool-call payloads, nested `graph.invoke()`, etc.)."""
error: NotRequired[str]
@@ -451,12 +453,11 @@ 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 -> 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 invocation to the model tool call's args.
# Maps task_id -> invocation metadata for tasks whose `input` matched
# a recognized per-call tool-dispatch shape (see `_extract_per_call_args`
# for the accepted layouts). The lifecycle hook joins on this when a
# child subgraph fires its first task event so it can attribute the
# invocation to the model tool call's args.
self._invocation_metadata: dict[str, dict[str, str]] = {}
# --- Template-method hooks (subclass overrides) ---
@@ -39,11 +39,12 @@ def _tasks_start(
) -> dict[str, Any]:
"""Build a `tasks` ProtocolEvent carrying a TaskPayload (start).
Pass `input={"tool_call": {"args": {...}}}` (or any envelope with
that shape) to exercise the lifecycle transformer's input mining of
invocation-intent metadata (`subagent_type`, `description`) — this
mirrors the `ToolCallWithContext` payload `langgraph.prebuilt.ToolNode`
Send-fans out per tool call.
Pass `input=[{"id": ..., "name": ..., "args": {...}}]` (the per-call
list shape `langchain.agents.create_agent` Send-fans out) or
`input={"tool_call": {"args": {...}}, ...}` (the dict envelope older
prebuilt agent paths emit) to exercise the lifecycle transformer's
input mining of invocation-intent metadata (`subagent_type`,
`description`).
"""
return {
"type": "event",
@@ -135,14 +136,14 @@ def test_started_emitted_on_first_direct_child_task() -> 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`, `description`, and `tool_call_id`
from `tool_call` and remembers them keyed by `parent_task_id`.
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": ...,
"""When a parent task's `input` is a dict envelope with a `tool_call`
field (`{"tool_call": {"args": {...}}, ...}`, the layout older
prebuilt agent paths Send-fan out per call), the transformer mines
`subagent_type`, `description`, and `tool_call_id` from `tool_call`
and remembers them keyed by `parent_task_id`. 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": ...,
"tool_call_id": ...}`. Identity-level correlation still uses
`trigger_call_id`; `tool_call_id` is exposed so UI consumers can
anchor the lifecycle event back to the originating AI message."""
@@ -210,12 +211,12 @@ def test_started_cause_with_description_but_no_subagent_type() -> None:
def test_started_carries_cause_for_list_shape_per_call_input() -> None:
"""langchain v1's `create_agent` Send-fans out a per-call task whose
`input` is a single-element list of tool-call dicts:
"""`langchain.agents.create_agent` Send-fans out a per-call task
whose `input` is a single-element list of tool-call dicts:
`[{"id": ..., "name": ..., "args": {...}}]`. The transformer mines
`subagent_type`, `description`, and `tool_call_id` exactly as for the
`ToolCallWithContext` dict envelope, so `lifecycle.started.cause`
fires regardless of which agent factory drove the dispatch."""
`subagent_type`, `description`, and `tool_call_id` exactly as for
the dict envelope shape, so `lifecycle.started.cause` fires
regardless of which agent factory drove the dispatch."""
mux = _build_lifecycle_mux()
mux.push(
_tasks_start(