Skip to content

feat(cli): add Temporal + LangGraph agent template and example#383

Merged
danielmillerp merged 1 commit into
nextfrom
dm/temporal-langgraph-template
Jun 1, 2026
Merged

feat(cli): add Temporal + LangGraph agent template and example#383
danielmillerp merged 1 commit into
nextfrom
dm/temporal-langgraph-template

Conversation

@danielmillerp
Copy link
Copy Markdown
Contributor

@danielmillerp danielmillerp commented Jun 1, 2026

What

Adds a temporal-langgraph template to agentex init (in the Async → Temporal submenu) and a runnable tutorial example (examples/tutorials/10_async/10_temporal/130_langgraph). LangGraph previously had default-langgraph and sync-langgraph but no Temporal variant; OpenAI Agents and Pydantic AI both did.

Approach: the official Temporal LangGraph plugin

Uses temporalio.contrib.langgraph.LangGraphPlugin (shipped in temporalio[langgraph]>=1.27.0, experimental). Each LangGraph node runs as a durable Temporal activity or inline in the workflow, set per node via execute_in. Temporal is the runtime; LangGraph is the agent framework.

The plugin landed after this PR was first drafted — the initial version hand-rolled the same nodes-as-activities behavior; this revision uses the real plugin.

Verified locally before shipping

I ran the plugin against a local Temporal server (WorkflowEnvironment.start_local) with a real LLM and encoded the working shape into the template (the docs don't spell these out):

  • agent (LLM) node → execute_in="activity"; tools node → execute_in="workflow". Tool-node-as-activity is broken in the experimental plugin (the AIMessage with tool calls doesn't survive the activity payload boundary → ToolNode raises "No AIMessage found"); running it in the workflow keeps it intact and is also where LangGraph interrupt must run.
  • Router and tools are async — sync callables get offloaded via run_in_executor, which Temporal's workflow event loop forbids (NotImplementedError).
  • Relies on AgentexWorker's UnsandboxedWorkflowRunner, so langchain imports in the workflow module are fine.

Showcase

  • Nodes as activities — LLM call is a retried, observable Temporal activity.
  • Human-in-the-loop — approval-gated tools raise a LangGraph interrupt; the workflow pauses on a provide_approval Temporal signal and resumes with Command(resume=...).
  • Live introspection queriesget_status, get_pending_approval, get_graph_state, get_graph_mermaid / get_graph_ascii.
  • Multi-turn memory on the workflow instance; tracing per turn.

Tests

  • tests/lib/cli/test_init_templates.py — renders every init template and asserts valid, substituted Python (catches .j2 regressions; already caught a Jinja brace bug), with focused coverage of the new template.
  • Example tests/test_graph_temporal.pyhermetic ReAct + HIL approve/reject via a stub model (no network), plus a live end-to-end run through the real Temporal plugin (skipped unless LITELLM_API_KEY is set). tests/test_agent.py — live integration against a running agent.

Verification run in this PR

  • pytest tests/lib/cli/59 passed.
  • Rendered template imports under temporalio 1.27.2; graph renders mermaid with per-node execute_in metadata.
  • Example tests: 3 passed + 1 skipped without a key; 4 passed with a LiteLLM key (live Temporal run answered a weather question via the tool through the plugin).
  • HIL (interrupt→signal→Command(resume)) and multi-turn message round-tripping verified live on a local Temporal server.
  • ruff clean on rendered template + example; secret-scanned.

🤖 Generated with Claude Code

Greptile Summary

This PR introduces a temporal-langgraph template to agentex init and a matching runnable tutorial example, making LangGraph a first-class option alongside the existing OpenAI Agents and Pydantic AI Temporal templates. The integration uses temporalio.contrib.langgraph.LangGraphPlugin (≥ 1.27.0) to run graph nodes as durable Temporal activities, with human-in-the-loop gating via LangGraph interrupt and Temporal signals.

  • New template (src/agentex/lib/cli/templates/temporal-langgraph/): full project scaffold with custom tools_node supporting HIL approval, multi-turn memory on the workflow instance, graph-visualization queries, and per-node execute_in metadata routing the LLM call to a retried activity.
  • New ADK helper (_langgraph_messages.py): converts finished LangGraph messages to Agentex task messages after ainvoke, handling both plain-string and list-of-blocks AIMessage.content.
  • New example (examples/tutorials/10_async/10_temporal/130_langgraph/): simplified (no HIL) variant with hermetic ReAct tests via a stub model plus a live end-to-end Temporal-plugin test gated on LITELLM_API_KEY.

Confidence Score: 4/5

Safe to merge with one open concern: the signal handler in the template workflow runs the full LangGraph turn (including awaiting LLM activities) directly inside on_task_event_send, which was flagged in a previous review thread and remains unaddressed.

The template's on_task_event_send signal handler awaits compiled.ainvoke(...) for the entire agent turn. If a second user message arrives while that await is in progress, a second handler coroutine starts concurrently, and both share self._messages, self._emitted, self._status, and self._pending_approval. Whichever handler writes last silently drops the other turn's messages. Everything else in the PR — the ADK helper, the graph template, the test suite, the pyproject deps, and the CLI wiring — looks correct and well-tested.

src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 — the on_task_event_send signal handler deserves the most attention due to the concurrent-execution concern noted in a prior review thread.

Important Files Changed

Filename Overview
src/agentex/lib/adk/_modules/_langgraph_messages.py New ADK helper that converts finished LangGraph messages to Agentex task messages; correctly handles both plain-string and list-of-blocks AIMessage content, and gracefully falls back on unknown block types.
src/agentex/lib/cli/templates/temporal-langgraph/project/workflow.py.j2 Template workflow: implements multi-turn memory, HIL approve/reject via Temporal signal, and graph-visualization queries; the signal handler runs the full graph turn (including awaiting LLM activities) directly inside on_task_event_send, which was flagged in a previous review thread as allowing concurrent handler execution that corrupts shared state.
src/agentex/lib/cli/templates/temporal-langgraph/project/graph.py.j2 Defines the ReAct graph with per-node execute_in metadata; the custom tools_node now correctly handles unknown tool names (returning an error ToolMessage instead of raising KeyError) and the HIL interrupt/resume pattern is properly wired.
src/agentex/lib/cli/commands/init.py Adds TEMPORAL_LANGGRAPH enum value, its project-file list, and the CLI menu entry; change is minimal and consistent with the existing temporal-openai-agents and temporal-pydantic-ai entries.
tests/lib/cli/test_init_templates.py Parametrized smoke-test renders every template and validates Python AST; focused tests verify that the temporal-langgraph template produces the expected files, substituted workflow class, execute_in metadata, HIL constructs, and LangGraph-plugin requirements.
examples/tutorials/10_async/10_temporal/130_langgraph/project/workflow.py Simplified (no HIL) example workflow demonstrating multi-turn memory and message emission; has the same signal-handler concurrency shape as the template but without the approval gate, making the race condition less likely in practice for sequential single-user use.
examples/tutorials/10_async/10_temporal/130_langgraph/tests/test_graph_temporal.py Good two-layer test strategy: hermetic ReAct loop with a stub model (always runs offline) plus a live Temporal-plugin end-to-end test gated on LITELLM_API_KEY; covers the ToolNode round-trip and plugin activity routing.
src/agentex/lib/cli/templates/temporal-langgraph/pyproject.toml.j2 Template pyproject correctly includes pytest-asyncio and httpx in dev dependencies, matching the example's pyproject and making generated test files immediately runnable.

Reviews (6): Last reviewed commit: "feat(cli): add Temporal + LangGraph agen..." | Re-trigger Greptile

Comment thread src/agentex/lib/cli/templates/temporal-langgraph/test_agent.py.j2 Outdated
Comment thread src/agentex/lib/cli/templates/temporal-langgraph/test_agent.py.j2 Outdated
Comment thread src/agentex/lib/cli/templates/temporal-langgraph/Dockerfile.j2 Outdated
@danielmillerp danielmillerp force-pushed the dm/temporal-langgraph-template branch from 1c291ed to 3ccb61c Compare June 1, 2026 16:56
@danielmillerp danielmillerp changed the title feat(cli): add Temporal + LangGraph agent template feat(cli): add Temporal + LangGraph agent template and example Jun 1, 2026
@socket-security
Copy link
Copy Markdown

socket-security Bot commented Jun 1, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgrandalf@​0.810010010010070

View full report

Comment thread src/agentex/lib/cli/templates/temporal-langgraph/pyproject.toml.j2
@danielmillerp danielmillerp force-pushed the dm/temporal-langgraph-template branch from 3ccb61c to d556838 Compare June 1, 2026 18:14
@danielmillerp
Copy link
Copy Markdown
Contributor Author

Pushed fixes for the failing CI + review comments (rebased on latest next):

CI failure (the blocker): the example manifest.yaml was invalid YAML — the description value contained a colon (...agent: nodes...), which broke the unquoted scalar. Fixed by (a) quoting description: "{{ description }}" in the template manifest so any user description is colon-safe, and (b) giving the example a colon-free description. Verified it now loads via AgentManifest.from_yaml(...) (the exact path that was erroring), and that a colon-containing description renders to valid YAML.

Review comments:

  • pytest-asyncio (+ httpx) added to the template's pyproject.toml.j2 dev deps.
  • _agent_nameagent_name fixture name fixed in test_agent.py.j2 (both test methods).
  • nodenodejs in the template Dockerfile.j2 (the uv Dockerfile was already correct).
  • On the signal-handler concurrency note: this matches the existing temporal-pydantic-ai / temporal-openai-agents templates (each turn is awaited in the RECEIVE_EVENT signal handler); leaving it consistent with those for now.

Also — made the example minimal. Per feedback that it had more state/machinery than the other LangGraph tutorials, the 130_langgraph example is now a lean nodes-as-activities ReAct agent (LLM node = activity, ToolNode = workflow, async router) with multi-turn memory — no HIL/queries. The template keeps the fuller showcase (HIL via interrupt + a Temporal signal, plus the introspection queries), matching how the other temporal templates are richer than their tutorials.

Re-verified locally: example tests pass (hermetic ReAct + a live end-to-end run through the real Temporal plugin on a local server), pytest tests/lib/cli/ → 59 passed, ruff clean, manifests parse.

Comment thread src/agentex/lib/cli/templates/temporal-langgraph/project/graph.py.j2 Outdated
@danielmillerp danielmillerp force-pushed the dm/temporal-langgraph-template branch from d556838 to 7562843 Compare June 1, 2026 18:22
Comment thread src/agentex/lib/cli/templates/temporal-langgraph/project/graph.py.j2 Outdated
Comment thread src/agentex/lib/cli/templates/temporal-langgraph/Dockerfile.j2 Outdated
Comment thread src/agentex/lib/cli/templates/temporal-langgraph/Dockerfile-uv.j2 Outdated
@danielmillerp danielmillerp force-pushed the dm/temporal-langgraph-template branch from 7562843 to cf64c14 Compare June 1, 2026 21:57
@danielmillerp
Copy link
Copy Markdown
Contributor Author

Thanks @smoreinis — addressed all three:

  • graph.py.j2 unknown-tool lookup: the tool node now does TOOLS_BY_NAME.get(name) and, on a miss (hallucinated tool name), appends a ToolMessage("Error: unknown tool ...") and continues instead of KeyError-crashing the workflow — so the model can recover. (The example uses prebuilt ToolNode, which already handles this.)
  • Dockerfile.j2 / Dockerfile-uv.j2 trailing space after npm \: removed in both — the line is now npm \ with no trailing whitespace, so the line continuation isn't broken.

Re-verified: rendered template builds the graph, ruff clean, pytest tests/lib/cli/ → 59 passed.

Comment thread src/agentex/lib/adk/_modules/_langgraph_messages.py Outdated
Adds a `temporal-langgraph` init template (registered in the Temporal
submenu of `agentex init`) plus a runnable tutorial example, filling the
gap left by the existing `temporal-openai-agents` / `temporal-pydantic-ai`
templates — LangGraph previously only had `default-langgraph` and
`sync-langgraph`.

Uses the official Temporal LangGraph plugin (`temporalio.contrib.langgraph`,
shipped in `temporalio[langgraph]>=1.27.0`): each LangGraph node runs as a
durable Temporal activity (the LLM/`agent` node) or inline in the workflow
(the `tools` node). Temporal is the runtime; LangGraph is the agent framework.

Verified the plugin's behavior locally (Temporal dev server + real LLM) and
encoded the working shape into the template:
- agent node `execute_in="activity"`, tools node `execute_in="workflow"`
  (tool-node-as-activity is broken in the experimental plugin — AIMessage
  doesn't survive the activity boundary);
- async router + async tools (sync callables hit run_in_executor, which
  Temporal's workflow loop forbids);
- AgentexWorker's UnsandboxedWorkflowRunner makes langchain imports safe.

Showcases nodes-as-activities, human-in-the-loop via LangGraph `interrupt`
resumed by a Temporal `provide_approval` signal + `Command(resume=...)`,
multi-turn memory on the workflow, graph-visualization queries
(mermaid/ascii), and tracing.

Example: examples/.../10_temporal/130_langgraph (full agent + tests).

Tests:
- tests/lib/cli/test_init_templates.py — renders every init template and
  asserts valid, substituted Python (catches .j2 regressions), with focused
  coverage of the new template (nodes-as-activities, HIL, queries, deps).
- example tests/test_graph_temporal.py — hermetic graph + HIL coverage with
  a stub model, plus a live end-to-end run through the real Temporal plugin
  (skipped without LITELLM_API_KEY); tests/test_agent.py — live integration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@danielmillerp danielmillerp force-pushed the dm/temporal-langgraph-template branch from cf64c14 to ab18952 Compare June 1, 2026 22:23
@danielmillerp
Copy link
Copy Markdown
Contributor Author

Good catch — fixed. emit_langgraph_messages now handles both content shapes: a plain string (OpenAI) and a list of content blocks (Anthropic/Claude via LangChain, e.g. [{"type": "text", "text": "..."}]), joining the text blocks. So switching MODEL_NAME to a Claude model still shows the final response instead of a blank bubble.

Verified with a unit check on both shapes (OpenAI str → text; Claude block-list → joined text); ruff clean; rebased on latest next; pytest tests/lib/cli/ → 59 passed.

@danielmillerp danielmillerp merged commit bbc9e02 into next Jun 1, 2026
45 checks passed
@danielmillerp danielmillerp deleted the dm/temporal-langgraph-template branch June 1, 2026 22:59
@stainless-app stainless-app Bot mentioned this pull request Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants