Skip to content

Commit 9d306f5

Browse files
DeanChensjcopybara-github
authored andcommitted
feat(workflow): Support state-based resumption for task-mode agent workflow nodes
- Decouple task-mode node execution from default output binding so standard text output does not prematurely finish the node. - Enhance the runner to automatically resolve and reuse the invocation_id of active suspended task nodes. - Route incoming plain text resume messages to the active task node by mapping to its isolation scope. - Enforce strict concurrency serialization by suspending parallel branch execution when a task node is waiting for user input. - Align schema validation helpers to cleanly handle Content wrapping and raw JSON strings. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 945423735
1 parent d831ee6 commit 9d306f5

17 files changed

Lines changed: 1942 additions & 177 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Agents In Workflow
2+
3+
This sample demonstrates how to use both `task` mode and `single_turn` mode Agents as nodes within a `Workflow`.
4+
5+
## Overview
6+
7+
The workflow represents a medical lab intake process:
8+
9+
1. **`intake_agent`**: A `task` mode Agent that chats with the user to collect their `name` and `phone_number`. It handles a multi-turn conversation until its `PatientIdentity` output schema is fulfilled.
10+
1. **`check_identity`**: A regular Python function node that receives the `PatientIdentity`. It mocks checking the database.
11+
- If the name is anything other than "Jane Doe", it yields a `retry` route, sending the user back to the `intake_agent`.
12+
- If the name is "Jane Doe", it routes to the `generate_instruction` agent.
13+
1. **`generate_instruction`**: A `single_turn` mode Agent that uses the `find_orders` tool to look up orders. It requires tool confirmation before execution.
14+
15+
## Sample Inputs
16+
17+
- `Hi, I am Jane Doe, my phone number is 555-1234.`
18+
19+
*The system will process this and return the mock lab orders along with AI-generated instructions on how to prepare.*
20+
21+
- `I'm here for my blood work.`
22+
23+
*The system will ask for your name and phone number.*
24+
25+
- `My name is John Doe, and my number is 123-456-7890.`
26+
27+
*The system will fail to find John's orders and route back to the intake agent.*
28+
29+
## Graph
30+
31+
```text
32+
[ START ]
33+
|
34+
v
35+
[ intake_agent ] <----.
36+
| |
37+
v |
38+
[ check_identity ] --- retry
39+
|
40+
| (DEFAULT_ROUTE)
41+
v
42+
[ generate_instruction ]
43+
```
44+
45+
## How To
46+
47+
Within an ADK workflow, you can embed LLM agents directly as nodes. The ADK runner handles them according to their `mode`:
48+
49+
### 1. Task Mode Agents
50+
51+
A `task` agent (`mode="task"`) handles a multi-turn conversation on its own before passing control to the next node. It will continually interact with the user until its specified task is completed.
52+
53+
```python
54+
class PatientIdentity(BaseModel):
55+
name: str
56+
phone_number: str
57+
58+
intake_agent = Agent(
59+
name="intake_agent",
60+
mode="task", # Stops and chats with the user until the schema is populated
61+
output_schema=PatientIdentity,
62+
instruction="...",
63+
)
64+
```
65+
66+
The parsed `output_schema` object is automatically forwarded as the `node_input` to the next node in the graph.
67+
68+
### 2. Single Turn Mode Agents
69+
70+
A `single_turn` agent (the default mode if omitted) executes a single LLM call. It is typically used for inline text generation, summarization, or classification without chatting with the user.
71+
72+
```python
73+
generate_instruction = Agent(
74+
name="generate_instruction",
75+
tools=[FunctionTool(find_orders, require_confirmation=True)],
76+
instruction="""
77+
Use the find_orders tool to get the patient's orders.
78+
List the orders found, and then generate a concise instruction about how to prepare based on those orders.
79+
""",
80+
)
81+
```
82+
83+
In this sample, the `generate_instruction` agent uses the `find_orders` tool to retrieve the orders. It also demonstrates **tool confirmation**, requiring the user to approve the tool call before it executes.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from google.adk import Agent
16+
from google.adk import Event
17+
from google.adk import Workflow
18+
from google.adk.tools.function_tool import FunctionTool
19+
from google.adk.workflow import DEFAULT_ROUTE
20+
from pydantic import BaseModel
21+
from pydantic import Field
22+
23+
24+
class PatientIdentity(BaseModel):
25+
"""Output schema for the intake agent."""
26+
27+
name: str = Field(description="The patient's full name.")
28+
phone_number: str = Field(description="The patient's phone number.")
29+
30+
31+
intake_agent = Agent(
32+
name="intake_agent",
33+
mode="task",
34+
output_schema=PatientIdentity,
35+
instruction="""\
36+
You are a medical lab intake assistant. Your job is to chat with
37+
the user to get their full name and phone number. Do not make up
38+
information. Once you have both, finish your task.
39+
If identity check failed, ask for another name.
40+
""",
41+
)
42+
43+
44+
def check_identity(node_input: PatientIdentity):
45+
"""Mocks checking the database for the patient.
46+
47+
Routes back to intake_agent if the name is not Jane Doe.
48+
"""
49+
if node_input.name.lower() != "jane doe":
50+
yield Event(
51+
message=(
52+
f"Could not find matching records for {node_input.name}. Let's"
53+
" try again."
54+
),
55+
route="retry",
56+
)
57+
else:
58+
yield Event(
59+
message=f"""Hello {node_input.name}! Let me look up your orders."""
60+
)
61+
62+
63+
def find_orders() -> list[str]:
64+
"""Finds orders for the patient."""
65+
return ["CBC (Complete Blood Count)", "Lipid Panel"]
66+
67+
68+
generate_instruction = Agent(
69+
name="generate_instruction",
70+
tools=[FunctionTool(find_orders, require_confirmation=True)],
71+
instruction="""
72+
Use the find_orders tool to get the patient's orders.
73+
List the orders found, and then generate a concise instruction about how to prepare based on those orders.
74+
""",
75+
)
76+
77+
78+
root_agent = Workflow(
79+
name="task_in_workflow",
80+
edges=[
81+
("START", intake_agent, check_identity),
82+
(
83+
check_identity,
84+
{"retry": intake_agent, DEFAULT_ROUTE: generate_instruction},
85+
),
86+
],
87+
)

0 commit comments

Comments
 (0)