Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6d33639
Bump google-adk floor to 2.5.0
DABH Jul 24, 2026
9ebda28
Add graph workflow, dynamic workflow, and durable HITL support for AD…
DABH Jul 24, 2026
5a52006
Install ADK platform providers as process-wide defaults
DABH Jul 24, 2026
82dad30
Add graph, dynamic workflow, and durable HITL integration tests
DABH Jul 24, 2026
51dc7a5
Fix basedpyright warnings (deprecated Optional/Mapping aliases, unuse…
DABH Jul 27, 2026
91390f8
Merge branch 'main' into google-adk-v2-graph-hitl
DABH Aug 18, 2026
fe9ff1a
Pin google-adk to upstream main via uv source
DABH Aug 18, 2026
69f033e
Install the ADK random provider unconditionally
DABH Aug 18, 2026
831bf88
Fix unresolvable doc link in activity_node docstring
DABH Aug 18, 2026
c478cd2
Merge remote-tracking branch 'origin/main' into google-adk-v2-graph-hitl
DABH Sep 4, 2026
2b7c969
Require google-adk>=2.8.0 and drop the upstream git pin
DABH Sep 4, 2026
91f157d
Trim _install_provider docstring to one line
DABH Sep 4, 2026
abeab29
Merge remote-tracking branch 'origin/main' into google-adk-v2-graph-hitl
DABH Sep 8, 2026
e4b35b9
Bind positional-only activity parameters in activity_node
DABH Sep 8, 2026
8a98a4d
Drop the unanswerable credential kind from the HITL helpers
DABH Sep 8, 2026
9473bdb
Pass optional model SDKs through the ADK workflow sandbox
DABH Sep 8, 2026
fcdb533
Drop the temporary google-adk exclude-newer override
DABH Sep 9, 2026
6ae154e
Merge remote-tracking branch 'origin/main' into google-adk-v2-graph-hitl
DABH Sep 9, 2026
ae576b1
Merge remote-tracking branch 'origin/main' into google-adk-v2-graph-hitl
DABH Sep 10, 2026
c64118e
Only report HITL calls listed in long_running_tool_ids
DABH Sep 10, 2026
849c417
Reject variadic activities in activity_node
DABH Sep 10, 2026
e24c719
Note the RNG consumption change for in-flight ADK workflows
DABH Sep 10, 2026
7fd50e4
condense changelog
DABH Sep 11, 2026
b673f5f
condense changelog
DABH Sep 11, 2026
79fa3cb
Merge remote-tracking branch 'origin/main' into google-adk-v2-graph-hitl
DABH Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ to include examples, links to docs, or any other relevant information.

### Added

- **Experimental**: `temporalio.contrib.google_adk_agents` now supports ADK v2
graph workflows, dynamic `@node` workflows, and durable HITL.
#### Standalone Activity operator commands

- `ActivityHandle` now supports operator commands for standalone activities: `pause`,
Expand All @@ -41,6 +43,11 @@ to include examples, links to docs, or any other relevant information.

### :boom: Breaking Changes

- The `google-adk` extra now requires `google-adk>=2.8.0,<3`, up from `>=2.2.0`.
- `temporalio.contrib.google_adk_agents`: ADK-generated ids and retry jitter now draw from the
workflow's deterministic random stream. A workflow started under an earlier release that calls
`workflow.random()` or `workflow.uuid4()` after ADK code may not replay deterministically
across the upgrade; drain such workflows or use worker versioning.
- Experimental external storage: `ExternalStorage.driver_selector` is now called with a
`StorageDriverSelectContext` instead of a `StorageDriverStoreContext`. Update the annotation;
the new type carries the same `target` field. Since selectors are plain callables, a stale
Expand All @@ -56,6 +63,8 @@ to include examples, links to docs, or any other relevant information.

### Fixed

- `GoogleAdkPlugin` now passes the optional `anthropic`, `litellm`, and `openai` SDKs through
the workflow sandbox.
- `contrib.deepagents`: summarization middleware configured with a model name string now routes its LLM calls through Activities instead of running them in the Workflow.

- **Experimental**: External storage metrics now report the wall-clock time storage was in flight.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ grpc = ["grpcio>=1.48.2,<2"]
opentelemetry = ["opentelemetry-api>=1.26,<2", "opentelemetry-sdk>=1.26,<2"]
pydantic = ["pydantic>=2.0.0,<3"]
openai-agents = ["openai-agents>=0.19.2,<0.20", "mcp>=1.9.4, <2"]
google-adk = ["google-adk>=2.2.0,<3", "mcp>=1.24,<2"]
google-adk = ["google-adk>=2.8.0,<3", "mcp>=1.24,<2"]
langgraph = ["langgraph>=1.1.0"]
langsmith = ["langsmith>=0.7.34,<0.9"]
deepagents = [
Expand Down
162 changes: 162 additions & 0 deletions temporalio/contrib/google_adk_agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,168 @@ agent = Agent(
)
```

## Graph Workflows (ADK v2)

ADK v2's graph runtime (`google.adk.workflow`) runs inside Temporal workflows:
the scheduler is pure asyncio and executes deterministically on Temporal's
workflow event loop, while LLM calls (`TemporalModel`), MCP tools
(`TemporalMcpToolSet`), and activity-backed nodes leave the workflow as
activities.

Use `activity_node(...)` to run a graph node as a Temporal activity. The
previous node's output is passed to the activity — directly for a
single-parameter activity, bound by name (from a dict) for multi-parameter
activities:

```python
from google.adk.workflow import JoinNode, Workflow
from temporalio.contrib.google_adk_agents.workflow import activity_node

fetch = activity_node(fetch_data, start_to_close_timeout=timedelta(seconds=30))

def summarize(node_input): # plain nodes run in-workflow: keep deterministic
return f"{node_input} summarized"

graph = Workflow(name="pipeline", edges=[("START", fetch, summarize)])
```

Conditional routing (`(router, {"KEY": handler, ...})`, `DEFAULT_ROUTE`),
parallel fan-out with `JoinNode`, and `LlmAgent` nodes (with
`mode="task"`/`"single_turn"`) all work — agent nodes route their model calls
through `TemporalModel` as usual.

## Dynamic Workflows

Dynamic nodes (`await ctx.run_node(...)` with loops, branches, and
`asyncio.gather`) work in-workflow; child-run caching reads only the
in-memory session, so re-entry after a HITL resume replays deterministically.

```python
from google.adk.workflow import node

@node(rerun_on_resume=True)
async def pipeline(ctx):
data = await ctx.run_node(fetch, "query") # activity_node child
results = await asyncio.gather(
*(ctx.run_node(worker, item) for item in data) # parallel children
)
return results
```

On a HITL resume, a `rerun_on_resume=True` dynamic node re-executes its body
while completed children are skipped from the session cache. Place activity
invocations in child nodes (`activity_node`, `activity_as_tool`) rather than
inline in the dynamic node body, or make them idempotent — inline calls run
again on re-entry (ADK's documented at-least-once semantics).

A `Workflow` with an `input_schema` can also be passed in an agent's
`tools=[...]` list (Workflow-as-Tool), letting the model invoke whole graphs
as tools.

## Durable Human-in-the-Loop

ADK pauses a run for human input (a node yielding `RequestInput`) or tool
confirmation (`FunctionTool(..., require_confirmation=True)`); in a Temporal
workflow that pause becomes a durable wait. The
`pending_hitl_requests` / `hitl_input_response` / `hitl_confirmation_response`
helpers cover the wire format; the wait itself is ordinary workflow code:

```python
from temporalio.contrib.google_adk_agents import (
HitlRequest,
hitl_input_response,
pending_hitl_requests,
)

@workflow.defn
class ApprovalWorkflow:
def __init__(self) -> None:
self._pending: dict[str, HitlRequest] = {}
self._responses: dict[str, Any] = {}

@workflow.query
def pending_requests(self) -> list[HitlRequest]:
return list(self._pending.values())

@workflow.update
def respond(self, interrupt_id: str, response: Any) -> None:
self._responses[interrupt_id] = response

@workflow.run
async def run(self, prompt: str) -> str:
runner = Runner(
app_name="app", node=graph, session_service=InMemorySessionService()
)
session = await runner.session_service.create_session(
app_name="app", user_id="user"
)
message = types.Content(role="user", parts=[types.Part(text=prompt)])
result = ""
while True:
async for event in runner.run_async(
user_id="user", session_id=session.id, new_message=message
):
for request in pending_hitl_requests(event):
self._pending[request.interrupt_id] = request
if event.content and event.content.parts and event.content.parts[0].text:
result = event.content.parts[0].text
if not self._pending:
return result
await workflow.wait_condition(
lambda: any(i in self._responses for i in self._pending)
)
parts = [
hitl_input_response(i, self._responses.pop(i))
for i in list(self._pending)
if i in self._responses
]
for part in parts:
self._pending.pop(part.function_response.id)
message = types.Content(role="user", parts=parts)
```

Tool confirmation composes with `activity_as_tool` with no extra plumbing —
`FunctionTool(func=activity_as_tool(risky_activity, ...), require_confirmation=True)`
never schedules the activity until the human approves (answer with
`hitl_confirmation_response(interrupt_id, confirmed=True)`). MCP tools
requesting confirmation via `tool_context.request_confirmation(...)` flow
through the same loop. Partial responses are fine: unanswered requests stay
pending across `run_async` turns.

Auth requests (`adk_request_credential`) are not covered by these helpers: ADK
exchanges the credential with network I/O inside the flow, and the exchanged
secret would be recorded in workflow history. Resolve credentials worker-side
instead (for example inside an activity or an MCP toolset factory).

> **Replay-safety note:** HITL resume matches recorded human responses against
> generated interrupt/function-call ids, so those ids must regenerate
> identically on replay. The plugin installs ADK's platform time/uuid/random
> providers as process-wide defaults, so the ids ADK generates (including
> default `RequestInput` interrupt ids) derive from `workflow.uuid4()` and
> replay identically.

## Determinism Notes

- The plugin patches ADK's `google.adk.platform` time, uuid, and random
providers to `workflow.now()`, `workflow.uuid4()`, and `workflow.random()`
inside workflows.
- ADK node `timeout=`/`RetryConfig` map onto durable timers
(`asyncio.wait_for`/`asyncio.sleep`). For activity-backed nodes, prefer
Temporal activity timeouts and `retry_policy` via `activity_node(...)`
options; an ADK `RetryConfig` on top would retry on top of Temporal's own
activity retries, and an ADK node timeout cancels the in-flight activity.
- Never set `RunConfig.tool_thread_pool_config` inside a workflow — it runs
tools on threads, which breaks workflow determinism. Live/BIDI mode is
likewise unsupported in workflows.
- ADK resume is at-least-once: on a HITL resume, completed nodes fast-forward
from the in-memory session, but `rerun_on_resume=True` node bodies
re-execute. This is deterministic under Temporal replay; schedule side
effects through activities (retried/tracked by Temporal) or make them
idempotent.
- Very long HITL conversations grow the workflow history with each turn;
consider `continue-as-new` boundaries between `run_async` turns for
long-running chats.

## Telemetry and Workflow Replay

ADK records OpenTelemetry metrics (scope `gcp.vertex.agent`, e.g.
Expand Down
10 changes: 10 additions & 0 deletions temporalio/contrib/google_adk_agents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
This module provides the necessary components to run ADK Agents within Temporal Workflows.
"""

from temporalio.contrib.google_adk_agents._hitl import (
HitlRequest,
hitl_confirmation_response,
hitl_input_response,
pending_hitl_requests,
)
from temporalio.contrib.google_adk_agents._mcp import (
TemporalMcpToolSet,
TemporalMcpToolSetProvider,
Expand All @@ -16,9 +22,13 @@

__all__ = [
"GoogleAdkPlugin",
"HitlRequest",
"TemporalMcpToolSet",
"TemporalMcpToolSetProvider",
"TemporalStatefulMcpToolSet",
"TemporalStatefulMcpToolSetProvider",
"TemporalModel",
"hitl_confirmation_response",
"hitl_input_response",
"pending_hitl_requests",
]
Loading
Loading