diff --git a/contributing/samples/workflows/dynamic_fan_out_fan_in/README.md b/contributing/samples/workflows/dynamic_fan_out_fan_in/README.md index 1486f78f..4b534d81 100644 --- a/contributing/samples/workflows/dynamic_fan_out_fan_in/README.md +++ b/contributing/samples/workflows/dynamic_fan_out_fan_in/README.md @@ -30,7 +30,7 @@ graph TD Key techniques demonstrated in this sample: 1. **Dynamic Scheduling**: Using a loop to create tasks via `ctx.run_node()`. -1. **Context Isolation**: Using `sub_branch` in `run_node` to isolate events for each parallel task, preventing context contamination. +1. **Context Isolation**: Using `use_sub_branch` in `run_node` to isolate events for each parallel task, preventing context contamination. 1. **`rerun_on_resume=True`**: Required on the orchestrator node to support resumption if any child node interrupts. ### Code Snippet @@ -38,12 +38,12 @@ Key techniques demonstrated in this sample: ```python # Fan-out: Schedule a dynamic node for each topic tasks = [] - for i, topic in enumerate(topics): + for topic in topics: tasks.append( ctx.run_node( generator, node_input=topic, - sub_branch=f"branch_{i}" + use_sub_branch=True, ) ) diff --git a/contributing/samples/workflows/dynamic_fan_out_fan_in/agent.py b/contributing/samples/workflows/dynamic_fan_out_fan_in/agent.py index cf49cc5d..9b334f94 100644 --- a/contributing/samples/workflows/dynamic_fan_out_fan_in/agent.py +++ b/contributing/samples/workflows/dynamic_fan_out_fan_in/agent.py @@ -15,6 +15,7 @@ from __future__ import annotations import asyncio +from typing import AsyncGenerator from google.adk import Agent from google.adk import Context @@ -33,7 +34,9 @@ generator = Agent( @node(rerun_on_resume=True) -async def orchestrator(ctx: Context, node_input: str) -> str: +async def orchestrator( + ctx: Context, node_input: str +) -> AsyncGenerator[Event, None]: """Orchestrator node that performs dynamic fan-out and fan-in.""" # Split input comma-separated string into topics topics = [t.strip() for t in node_input.split(",") if t.strip()] @@ -41,7 +44,7 @@ async def orchestrator(ctx: Context, node_input: str) -> str: # Fan-out: Schedule a dynamic node for each topic tasks = [] - for i, topic in enumerate(topics): + for topic in topics: tasks.append( ctx.run_node( generator, diff --git a/contributing/samples/workflows/dynamic_nodes/agent.py b/contributing/samples/workflows/dynamic_nodes/agent.py index 57c2ec10..286fc42e 100644 --- a/contributing/samples/workflows/dynamic_nodes/agent.py +++ b/contributing/samples/workflows/dynamic_nodes/agent.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import AsyncGenerator from typing import Literal from google.adk import Agent @@ -59,7 +60,9 @@ evaluate_headline = Agent( @node(rerun_on_resume=True) -async def orchestrate(ctx: Context, node_input: str) -> str: +async def orchestrate( + ctx: Context, node_input: str +) -> AsyncGenerator[Event | str, None]: yield Event(state={"topic": node_input}) while True: diff --git a/contributing/samples/workflows/loop_config/README.md b/contributing/samples/workflows/loop_config/README.md index b24723dd..f66e9371 100644 --- a/contributing/samples/workflows/loop_config/README.md +++ b/contributing/samples/workflows/loop_config/README.md @@ -7,6 +7,13 @@ YAML configuration file. It mirrors the `contributing/samples/workflows/loop` sample, but uses YAML to define the workflow structure instead of Python. +> **Status**: not runnable yet. The YAML agent loader resolves `agent_class` +> against `google.adk.agents` and requires a `BaseAgent` subclass with a config +> type, and no config field binds `edges`. `Workflow` satisfies neither, so +> `root_agent.yaml` below describes the intended syntax rather than syntax the +> loader accepts today. The individual agent files +> (`generate_headline.yaml`, `evaluate_headline.yaml`) do load. + ## Sample Inputs - `Python programming` @@ -28,12 +35,12 @@ graph TD This sample uses some special syntax in `root_agent.yaml` to support dynamic resolution and graph construction: -### 1. `_code` Suffix +### 1. Code References -Fields ending with `_code` (like `output_schema_code` in `evaluate_headline.yaml`) tell the ADK YAML mapper to resolve the value as a Python code reference rather than treating it as a plain string. +Fields that hold a Python object (like `output_schema` in `evaluate_headline.yaml`) take a `name` entry holding the fully qualified name of that object, which the loader imports. -- If it starts with `.`, it resolves relative to the current agent directory's Python package path. -- Example: `output_schema_code: .agent.Feedback` resolves to the `Feedback` Pydantic model in `agent.py` in the same directory. +- The name is resolved against `sys.path`, which includes the directory holding the agent folders. +- Example: `name: loop_config.agent.Feedback` resolves to the `Feedback` Pydantic model in `agent.py` in this directory. ### 2. Function References in Edges diff --git a/contributing/samples/workflows/loop_config/evaluate_headline.yaml b/contributing/samples/workflows/loop_config/evaluate_headline.yaml index 3481ecad..687e0d12 100644 --- a/contributing/samples/workflows/loop_config/evaluate_headline.yaml +++ b/contributing/samples/workflows/loop_config/evaluate_headline.yaml @@ -16,5 +16,6 @@ agent_class: LlmAgent name: evaluate_headline instruction: | Grade whether the headline is related to technology or software engineering. -output_schema_code: .agent.Feedback +output_schema: + name: loop_config.agent.Feedback output_key: feedback diff --git a/contributing/samples/workflows/message/README.md b/contributing/samples/workflows/message/README.md index 6cd15eb0..b0f035a9 100644 --- a/contributing/samples/workflows/message/README.md +++ b/contributing/samples/workflows/message/README.md @@ -65,7 +65,7 @@ To send messages in an ADK node, yield an `Event` object with the `message` argu ``` 1. **Stream a message in chunks**: - Provide the `partial=True` flag for intermediate chunks. This provides a better user experience by allowing the UI to show the response in a streaming fashion, thereby lowering the latency to see the first word. ADK automatically accumulates all partial messages and merges them into a final message for you for session storage. + Provide the `partial=True` flag for intermediate chunks. This provides a better user experience by allowing the UI to show the response in a streaming fashion, thereby lowering the latency to see the first word. Partial events are forwarded to the client but are not stored in the session, so finish by yielding the assembled message once without `partial=True`. > **Note**: To stream multiple messages or tokens smoothly, your node function **must be an asynchronous generator** (`async def`). This allows ADK to yield messages to the client immediately without blocking. @@ -78,4 +78,5 @@ To send messages in an ADK node, yield an `Event` object with the `message` argu yield Event(message="may I", partial=True) await asyncio.sleep(0.5) yield Event(message=" help you?", partial=True) + yield Event(message="How may I help you?") ``` diff --git a/contributing/samples/workflows/message/agent.py b/contributing/samples/workflows/message/agent.py index c62c6916..45e31d81 100644 --- a/contributing/samples/workflows/message/agent.py +++ b/contributing/samples/workflows/message/agent.py @@ -71,6 +71,8 @@ async def stream_sentence(node_input: Any = None): """ Demonstrates streaming by sending a sentence in chunks. The `partial=True` flag tells the UI that this is part of an ongoing message. + Partial events are not written to the session, so the node ends by yielding + the assembled sentence once as a non-partial event. """ yield Event(message="#4 Starting to stream...") sentence = """\ @@ -89,6 +91,8 @@ You can stream in markdown as well. For example, the table below: yield Event(message=chunk, partial=True) await sleep_if_not_pytest(0.2) + yield Event(message=sentence) + root_agent = Workflow( name="message", diff --git a/contributing/samples/workflows/message/tests/go.json b/contributing/samples/workflows/message/tests/go.json index 694a9978..1a6fc3bd 100644 --- a/contributing/samples/workflows/message/tests/go.json +++ b/contributing/samples/workflows/message/tests/go.json @@ -134,6 +134,22 @@ "nodeInfo": { "path": "message@1/stream_sentence@1" } + }, + { + "author": "message", + "content": { + "parts": [ + { + "text": "This is a streaming message sent in chunks.\n\nYou can stream in markdown as well. For example, the table below:\n\n| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |\n| Cell 3 | Cell 4 |\n" + } + ], + "role": "user" + }, + "id": "e-9", + "invocationId": "i-1", + "nodeInfo": { + "path": "message@1/stream_sentence@1" + } } ], "id": "9cfc0ef6-11d6-4260-84cf-be22731ab69e", diff --git a/contributing/samples/workflows/node_as_tool/agent.py b/contributing/samples/workflows/node_as_tool/agent.py index 916a2a02..54965e96 100644 --- a/contributing/samples/workflows/node_as_tool/agent.py +++ b/contributing/samples/workflows/node_as_tool/agent.py @@ -19,7 +19,7 @@ from typing import Generator from google.adk import Agent from google.adk import Event from google.adk import Workflow -from google.adk.apps._configs import ResumabilityConfig +from google.adk.apps import ResumabilityConfig from google.adk.apps.app import App from google.adk.workflow import node from pydantic import BaseModel @@ -38,10 +38,10 @@ from google.adk.events import RequestInput # 2. Define a regular Node using the @node decorator. # This Node is wrapped as a NodeTool automatically by the Agent. # As a NodeTool, it has the ability to yield intermediate Events during execution. +# Annotate the yield type with the data the tool returns, not the Event and +# RequestInput control-flow items, so the tool's response schema stays small. @node(rerun_on_resume=True) -def calculate_discount( - tier: str, ctx: Context -) -> Generator[Event | RequestInput | str, None, None]: +def calculate_discount(tier: str, ctx: Context) -> Generator[str, None, None]: """Calculates the discount percentage based on customer tier. Args: diff --git a/contributing/samples/workflows/request_input/README.md b/contributing/samples/workflows/request_input/README.md index 6a42e3bf..c518d586 100644 --- a/contributing/samples/workflows/request_input/README.md +++ b/contributing/samples/workflows/request_input/README.md @@ -20,7 +20,8 @@ This pattern is crucial for tasks where AI actions require human verification be ```mermaid graph TD - START --> draft_email + START --> process_input + process_input --> draft_email draft_email --> request_human_review request_human_review --> handle_human_review handle_human_review -->|revise| draft_email @@ -59,7 +60,7 @@ graph TD Workflow( name="request_input", edges=[ - ("START", ..., draft_email, request_human_review, handle_human_review), + ("START", process_input, draft_email, request_human_review, handle_human_review), (handle_human_review, {"revise": draft_email, "approved": send_email}), ], ) diff --git a/contributing/samples/workflows/request_input_advanced/README.md b/contributing/samples/workflows/request_input_advanced/README.md index 476bf819..5df5817b 100644 --- a/contributing/samples/workflows/request_input_advanced/README.md +++ b/contributing/samples/workflows/request_input_advanced/README.md @@ -54,16 +54,16 @@ graph TD approved_days: Optional[int] = Field(None) ``` -1. **Yield a RequestInput:** Pass the schema and optionally a `payload` for the client to display. +1. **Return a RequestInput:** Pass the schema and optionally a `payload` for the client to display. ```python def evaluate_request(request: TimeOffRequest): # ... logic to check if manager review is needed ... - yield RequestInput( + return RequestInput( interrupt_id="manager_approval", message="Please review this time off request.", payload=request, - response_schema=TimeOffDecision.model_json_schema() + response_schema=TimeOffDecision, ) ``` diff --git a/contributing/samples/workflows/request_input_rerun/README.md b/contributing/samples/workflows/request_input_rerun/README.md index d0372118..6f6a804a 100644 --- a/contributing/samples/workflows/request_input_rerun/README.md +++ b/contributing/samples/workflows/request_input_rerun/README.md @@ -25,7 +25,8 @@ This allows you to combine the requesting and handling of human input into a sin ```mermaid graph TD - START --> draft_email + START --> process_input + process_input --> draft_email draft_email --> human_review[human_review
reruns on resume] human_review -->|revise| draft_email human_review -->|approved| send_email diff --git a/contributing/samples/workflows/retry/README.md b/contributing/samples/workflows/retry/README.md index 0c86625b..9645df37 100644 --- a/contributing/samples/workflows/retry/README.md +++ b/contributing/samples/workflows/retry/README.md @@ -8,7 +8,7 @@ The ADK framework allows you to easily handle these scenarios by wrapping the un When a node raises an exception, the framework automatically emits an error event (with `error_code` and `error_message`) so the error is visible in the event stream. If the node has retry configured, it will be retried after the backoff delay. -This sample demonstrates a `get_weather` node that intentionally fails randomly (70% chance) by raising an `HTTPError` representing a 500 Internal Server error. The framework gracefully recovers and eventually succeeds, passing the result to `report_weather`. +This sample demonstrates a `get_weather` node that intentionally fails randomly (70% chance) by raising an `HTTPError` representing a 500 Internal Server error. The framework gracefully recovers and usually succeeds within the five configured attempts, passing the result to `report_weather`. ## Graph diff --git a/contributing/samples/workflows/use_as_output/agent.py b/contributing/samples/workflows/use_as_output/agent.py index 9516fedc..d86563b0 100644 --- a/contributing/samples/workflows/use_as_output/agent.py +++ b/contributing/samples/workflows/use_as_output/agent.py @@ -15,8 +15,8 @@ from google.adk import Agent from google.adk import Context from google.adk.workflow import node +from google.adk.workflow import START from google.adk.workflow import Workflow -from google.adk.workflow._base_node import START summarizer = Agent( name='summarizer',