Skip to content

Commit fd340cb

Browse files
authored
Merge branch 'main' into NEXUS-283
2 parents 2be8831 + 6d3351a commit fd340cb

File tree

13 files changed

+2013
-1671
lines changed

13 files changed

+2013
-1671
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ jobs:
163163
- run: poe build-develop
164164
- run: poe lint
165165
- run: mkdir junit-xml
166-
- run: poe test -s --junit-xml=junit-xml/latest-deps.xml
166+
- run: poe test -s --junit-xml=junit-xml/latest-deps.xml
167167
timeout-minutes: 15
168168
- name: "Upload junit-xml artifacts"
169169
uses: actions/upload-artifact@v4

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,6 +2066,13 @@ To execute tests:
20662066
poe test
20672067
```
20682068

2069+
`poe test` spreads tests across multiple worker processes by default. If you
2070+
need a serial run for debugging, invoke pytest directly:
2071+
2072+
```bash
2073+
uv run pytest
2074+
```
2075+
20692076
This runs against [Temporalite](https://github.com/temporalio/temporalite). To run against the time-skipping test
20702077
server, pass `--workflow-environment time-skipping`. To run against the `default` namespace of an already-running
20712078
server, pass the `host:port` to `--workflow-environment`. Can also use regular pytest arguments. For example, here's how

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ dev = [
7575
"openinference-instrumentation-google-adk>=0.1.8",
7676
"googleapis-common-protos==1.70.0",
7777
"pytest-rerunfailures>=16.1",
78+
"pytest-xdist>=3.6,<4",
7879
"moto[s3,server]>=5",
80+
"setuptools<82",
7981
"opentelemetry-exporter-otlp-proto-grpc>=1.11.1,<2",
8082
"opentelemetry-semantic-conventions>=0.40b0,<1",
8183
"opentelemetry-sdk-extension-aws>=2.0.0,<3",
@@ -118,7 +120,7 @@ lint-types = [
118120
{ cmd = "uv run basedpyright" },
119121
]
120122
run-bench = "uv run python scripts/run_bench.py"
121-
test = "uv run pytest"
123+
test = "uv run pytest -n auto --dist=worksteal"
122124

123125

124126
[tool.pytest.ini_options]

temporalio/contrib/google_adk_agents/README.md

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -118,35 +118,44 @@ from temporalio.worker import Worker
118118
from temporalio.contrib.google_adk_agents import (
119119
GoogleAdkPlugin,
120120
TemporalMcpToolSetProvider,
121-
TemporalMcpToolSet
121+
TemporalMcpToolSet,
122122
)
123123

124-
# Create toolset provider
125-
provider = TemporalMcpToolSetProvider("my-tools",
126-
lambda _: McpToolset(
127-
connection_params=StdioConnectionParams(
128-
server_params=StdioServerParameters(
129-
command="npx",
130-
args=[
131-
"-y",
132-
"@modelcontextprotocol/server-filesystem",
133-
os.path.dirname(os.path.abspath(__file__)),
134-
],
135-
),
136-
),
137-
))
124+
125+
def toolset_factory(_):
126+
return McpToolset(
127+
connection_params=StdioConnectionParams(
128+
server_params=StdioServerParameters(
129+
command="npx",
130+
args=[
131+
"-y",
132+
"@modelcontextprotocol/server-filesystem",
133+
os.path.dirname(os.path.abspath(__file__)),
134+
],
135+
),
136+
),
137+
)
138138

139139
# Use in agent workflow
140140
agent = Agent(
141141
name="test_agent",
142142
model="gemini-2.5-pro",
143-
tools=[TemporalMcpToolSet("my-tools")]
143+
tools=[
144+
TemporalMcpToolSet(
145+
"my-tools",
146+
not_in_workflow_toolset=toolset_factory,
147+
)
148+
],
144149
)
145150

146151
client = await Client.connect(
147152
"localhost:7233",
148153
plugins=[
149-
GoogleAdkPlugin(toolset_providers=[provider]),
154+
GoogleAdkPlugin(
155+
toolset_providers=[
156+
TemporalMcpToolSetProvider("my-tools", toolset_factory),
157+
],
158+
),
150159
],
151160
)
152161

@@ -157,6 +166,35 @@ worker = Worker(
157166
)
158167
```
159168

169+
### Local ADK Runs
170+
171+
The same agent definitions can also be exercised outside Temporal with
172+
`adk run` or `adk web`.
173+
174+
- `TemporalModel` and `activity_tool(...)` work in local ADK runs without
175+
additional configuration.
176+
- If the agent uses `TemporalMcpToolSet`, define a shared toolset factory,
177+
register it with `TemporalMcpToolSetProvider(...)` for workflow runs, and
178+
reuse the same function for `not_in_workflow_toolset=...` so the agent can
179+
fall back to the underlying `McpToolset` when it is not running inside
180+
`workflow.in_workflow()`.
181+
182+
Example:
183+
184+
```python
185+
# Reuse the same toolset_factory registered in GoogleAdkPlugin above.
186+
agent = Agent(
187+
name="test_agent",
188+
model=TemporalModel("gemini-2.5-pro"),
189+
tools=[
190+
TemporalMcpToolSet(
191+
"my-tools",
192+
not_in_workflow_toolset=toolset_factory,
193+
)
194+
],
195+
)
196+
```
197+
160198
## Integration Points
161199

162200
This integration provides comprehensive support for running Google ADK Agents within Temporal workflows while maintaining:

temporalio/contrib/google_adk_agents/_mcp.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ class TemporalMcpToolSetProvider:
9090
within Temporal workflows.
9191
"""
9292

93-
def __init__(self, name: str, toolset_factory: Callable[[Any | None], McpToolset]):
93+
def __init__(
94+
self, name: str, toolset_factory: Callable[[Any | None], McpToolset]
95+
) -> None:
9496
"""Initializes the toolset provider.
9597
9698
Args:
@@ -215,20 +217,28 @@ def __init__(
215217
name: str,
216218
config: ActivityConfig | None = None,
217219
factory_argument: Any | None = None,
220+
not_in_workflow_toolset: Callable[[Any | None], McpToolset] | None = None,
218221
):
219222
"""Initializes the Temporal MCP toolset.
220223
221224
Args:
222225
name: Name of the toolset (used for activity naming).
223226
config: Optional activity configuration.
224227
factory_argument: Optional argument passed to toolset factory.
228+
not_in_workflow_toolset: Optional factory that returns the
229+
underlying ``McpToolset`` to use when this wrapper executes
230+
outside ``workflow.in_workflow()``, such as local ADK runs.
231+
This is not needed during normal workflow execution, but
232+
``get_tools()`` raises ``ValueError`` outside a workflow if it
233+
is omitted.
225234
"""
226235
super().__init__()
227236
self._name = name
228237
self._factory_argument = factory_argument
229238
self._config = config or ActivityConfig(
230239
start_to_close_timeout=timedelta(minutes=1)
231240
)
241+
self._not_in_workflow_toolset = not_in_workflow_toolset
232242

233243
async def get_tools(
234244
self, readonly_context: ReadonlyContext | None = None
@@ -241,6 +251,17 @@ async def get_tools(
241251
Returns:
242252
List of available tools wrapped as Temporal activities.
243253
"""
254+
# If executed outside a workflow, like when doing local adk runs, use the mcp server directly
255+
if not workflow.in_workflow():
256+
if self._not_in_workflow_toolset is None:
257+
raise ValueError(
258+
"Attempted to use TemporalMcpToolSet outside a workflow, but "
259+
"no not_in_workflow_toolset was provided. Either use "
260+
"McpToolSet directly or pass a factory that returns the "
261+
"underlying McpToolset for non-workflow execution."
262+
)
263+
return await self._not_in_workflow_toolset(None).get_tools(readonly_context)
264+
244265
tool_results: list[_ToolResult] = await workflow.execute_activity(
245266
self._name + "-list-tools",
246267
_GetToolsArguments(self._factory_argument),

temporalio/contrib/google_adk_agents/_model.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from google.adk.models.llm_request import LlmRequest
66
from google.adk.models.llm_response import LlmResponse
77

8+
import temporalio.workflow
89
from temporalio import activity, workflow
910
from temporalio.workflow import ActivityConfig
1011

@@ -67,6 +68,14 @@ async def generate_content_async(
6768
Yields:
6869
The responses from the model.
6970
"""
71+
# If executed outside a workflow, like when doing local adk runs, use the model directly
72+
if not temporalio.workflow.in_workflow():
73+
async for response in LLMRegistry.new_llm(
74+
self._model_name
75+
).generate_content_async(llm_request, stream=stream):
76+
yield response
77+
return
78+
7079
responses = await workflow.execute_activity(
7180
invoke_model,
7281
args=[llm_request],

temporalio/contrib/google_adk_agents/workflow.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import inspect
44
from typing import Any, Callable
55

6+
import temporalio.workflow
67
from temporalio import workflow
78

89

@@ -29,6 +30,14 @@ async def wrapper(*args: Any, **kw: Any):
2930
# Decorator kwargs are defaults.
3031
options = kwargs.copy()
3132

33+
if not temporalio.workflow.in_workflow():
34+
# If executed outside a workflow, like when doing local adk runs, use the function directly
35+
result = activity_def(*args, **kw)
36+
if inspect.isawaitable(result):
37+
return await result
38+
else:
39+
return result
40+
3241
return await workflow.execute_activity(activity_def, *activity_args, **options)
3342

3443
# Copy metadata

tests/conftest.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,16 @@ async def worker(
187187
@pytest.hookimpl(hookwrapper=True, trylast=True)
188188
def pytest_cmdline_main(config): # type: ignore[reportMissingParameterType, reportUnusedParameter]
189189
result = yield
190-
if result.get_result() == 0:
190+
exit_code = result.get_result()
191+
numprocesses = getattr(config.option, "numprocesses", None)
192+
running_with_xdist = hasattr(config, "workerinput") or numprocesses not in (
193+
None,
194+
0,
195+
"0",
196+
)
197+
if exit_code == 0 and not running_with_xdist:
191198
os._exit(0)
192-
return result.get_result()
199+
return exit_code
193200

194201

195202
CONTINUE_AS_NEW_SUGGEST_HISTORY_COUNT = 50

0 commit comments

Comments
 (0)