Skip to content

fix(runners): widen Runner.__init__ agent param to accept BaseNode#6308

Open
anxkhn wants to merge 1 commit into
google:mainfrom
anxkhn:fix/runner-agent-node-type
Open

fix(runners): widen Runner.__init__ agent param to accept BaseNode#6308
anxkhn wants to merge 1 commit into
google:mainfrom
anxkhn:fix/runner-agent-node-type

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 5, 2026

Copy link
Copy Markdown

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

Problem:

The Runner.agent class field is typed Optional[BaseAgent | 'BaseNode']
(runners.py:158) and the runtime explicitly branches on
isinstance(self.agent, BaseNode) in both run_async and run_live, so a
BaseNode root (for example a Workflow) is fully supported at execution time.

However, the Runner.__init__ agent parameter (runners.py:180) was still
typed Optional[BaseAgent], narrower than the field it initializes. As a result,
static type checkers reject Runner(agent=my_workflow) even though it runs
correctly. The parameter annotation was simply out of sync with the class field
and with the runtime's own BaseNode handling.

This is a static typing correctness issue, not a runtime one (Python does not
enforce annotations at runtime, so the call already works). It shows up in ADK's
own codebase: with the current annotation, mypy flags the internal call site
in a2a/utils/agent_to_a2a.py where a plain BaseAgent is forwarded into
Runner:

src/google/adk/a2a/utils/agent_to_a2a.py:169: error: Argument 1 to "Runner"
  has incompatible type ...; expected "BaseAgent | None"  [arg-type]

Solution:

Widen the __init__ agent annotation to Optional[BaseAgent | 'BaseNode'] so
it matches the class field byte-for-byte. This mirrors the existing
forward-reference style already used for the field (runners.py:158) and for the
run_live node parameter (runners.py:455); BaseNode is kept as a lazy
forward reference (the module uses from __future__ import annotations and
imports BaseNode inside the methods that need it), so no new import is added.

This is a type-only change with no runtime behavior change. The narrower internal
helper _resolve_app was intentionally left as-is: this PR is scoped to the
public __init__ surface that #6270 reports.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added TestRunnerResolveApp::test_resolve_app_with_base_node_via_agent_param,
which constructs Runner(agent=<BaseNode>, app_name=...) and asserts the node is
accepted and wrapped (runner.app.root_agent is node, runner.agent is node).
This is a behavioral contract guard for the accepted-and-wrapped path; the
annotation itself is validated by the type checker (below), since Python does not
enforce annotations at runtime.

pytest summary:

$ pytest tests/unittests/test_runners.py -q
70 passed

Type check (the actual validation of a type-only change): with this change,
mypy no longer reports the expected "BaseAgent | None" error at the
a2a/utils/agent_to_a2a.py Runner(...) call site, and no new mypy errors are
introduced elsewhere.

Manual End-to-End (E2E) Tests:

Not applicable. This is a type-annotation-only change with no runtime behavior
change; it is covered by the unit test and the static type check above.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas. (No comment needed; one-line annotation change mirroring the existing field.)
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end. (Not applicable; type-only change.)
  • Any dependent changes have been merged and published in downstream modules. (None.)

Additional context

The change is a single line plus one test:

-      agent: Optional[BaseAgent] = None,
+      agent: Optional[BaseAgent | 'BaseNode'] = None,

The `Runner.agent` class field is typed `Optional[BaseAgent | 'BaseNode']`
and the runtime explicitly branches on `isinstance(self.agent, BaseNode)`
in both `run_async` and `run_live`, so a `BaseNode` root (e.g. a
`Workflow`) works at runtime. However, the `__init__` `agent` parameter
was still typed `Optional[BaseAgent]`, so type checkers reject
`Runner(agent=my_workflow)`.

Widen the `__init__` `agent` annotation to `Optional[BaseAgent | 'BaseNode']`
to match the class field, mirroring the existing forward-reference style
used for the field and the `run_live` `node` parameter. This is a
type-only change with no runtime behavior change.

Fixes google#6270
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.

Runner.__init__ 'agent' param typed Optional[BaseAgent] but class field and runtime accept BaseNode (e.g. Workflow) roots

1 participant