temporalio-google-genai 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. temporalio_google_genai-0.1.0/LICENSE +21 -0
  2. temporalio_google_genai-0.1.0/PKG-INFO +318 -0
  3. temporalio_google_genai-0.1.0/README.md +292 -0
  4. temporalio_google_genai-0.1.0/pyproject.toml +127 -0
  5. temporalio_google_genai-0.1.0/pyproject.toml.orig +111 -0
  6. temporalio_google_genai-0.1.0/src/temporalio/google_genai/__init__.py +119 -0
  7. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_compat.py +24 -0
  8. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_errors.py +16 -0
  9. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_gemini_activity.py +358 -0
  10. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_google_genai_plugin.py +159 -0
  11. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_mcp.py +226 -0
  12. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_models.py +173 -0
  13. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_agents.py +143 -0
  14. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_api_client.py +319 -0
  15. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_async_client.py +291 -0
  16. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_file_search_stores.py +109 -0
  17. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_files.py +217 -0
  18. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_interactions.py +258 -0
  19. temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_mcp.py +116 -0
  20. temporalio_google_genai-0.1.0/src/temporalio/google_genai/py.typed +1 -0
  21. temporalio_google_genai-0.1.0/src/temporalio/google_genai/testing.py +177 -0
  22. temporalio_google_genai-0.1.0/src/temporalio/google_genai/workflow.py +111 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 temporal.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,318 @@
1
+ Metadata-Version: 2.4
2
+ Name: temporalio-google-genai
3
+ Version: 0.1.0
4
+ Summary: Temporal integration for google genai
5
+ Author: Temporal Technologies Inc
6
+ Author-email: Temporal Technologies Inc <sdk@temporal.io>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: temporalio>=1.34.0,<2
17
+ Requires-Dist: google-genai>=2.21.0,<3
18
+ Requires-Dist: pydantic>=2.0.0,<3
19
+ Requires-Dist: mcp>=1.24,<2 ; extra == 'mcp'
20
+ Requires-Python: >=3.10
21
+ Project-URL: Homepage, https://github.com/temporalio/ai-integrations/tree/main/python/google_genai
22
+ Project-URL: Repository, https://github.com/temporalio/ai-integrations
23
+ Project-URL: Documentation, https://docs.temporal.io/develop/python/integrations/google-genai
24
+ Provides-Extra: mcp
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Google Gemini SDK Integration for Temporal
28
+
29
+ > Release stage: [Public Preview](https://docs.temporal.io/develop/python/integrations/google-genai).
30
+
31
+ ## Overview
32
+
33
+ This plugin lets you use the [Google Gemini SDK](https://googleapis.github.io/python-genai/)
34
+ (`google-genai`) inside Temporal workflows with durable execution. Every Gemini
35
+ API call becomes a **Temporal activity**, so model calls, tool calls, file
36
+ operations, interactions, and managed agents are retried, recorded in history,
37
+ and survive worker restarts.
38
+
39
+ Key properties:
40
+
41
+ - **Credentials never enter the workflow.** The real `genai.Client` lives only
42
+ on the worker, inside activities; no API keys or tokens appear in event
43
+ history.
44
+ - **The SDK's automatic function calling (AFC) loop runs in the workflow**, so
45
+ tool wrappers (`activity_as_tool`) work naturally — no manual agent loop.
46
+ - **Temporal owns retries.** Configure them via the activity `retry_policy`; the
47
+ SDK's own retry loop is rejected to avoid double-retry (see
48
+ [Retries & errors](#retries--errors)).
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ uv add temporalio-google-genai
54
+ ```
55
+
56
+ ## Hello World
57
+
58
+ ```python
59
+ import os
60
+ from datetime import timedelta
61
+
62
+ from google import genai
63
+ from google.genai import types
64
+
65
+ from temporalio import activity, workflow
66
+ from temporalio.client import Client
67
+ from temporalio.google_genai import (
68
+ GoogleGenAIPlugin,
69
+ TemporalAsyncClient,
70
+ activity_as_tool,
71
+ )
72
+ from temporalio.worker import Worker
73
+ from temporalio.workflow import ActivityConfig
74
+
75
+
76
+ # ---- a tool, as a normal Temporal activity (runs on the worker) ----
77
+ @activity.defn
78
+ async def get_weather(city: str) -> str:
79
+ return f"It's sunny in {city}."
80
+
81
+
82
+ # ---- the workflow (runs in the Temporal sandbox) ----
83
+ @workflow.defn
84
+ class WeatherAgent:
85
+ @workflow.run
86
+ async def run(self, prompt: str) -> str:
87
+ client = TemporalAsyncClient()
88
+ response = await client.models.generate_content(
89
+ model="gemini-2.5-flash",
90
+ contents=prompt,
91
+ config=types.GenerateContentConfig(
92
+ tools=[
93
+ activity_as_tool(
94
+ get_weather,
95
+ activity_config=ActivityConfig(
96
+ start_to_close_timeout=timedelta(seconds=30),
97
+ ),
98
+ ),
99
+ ],
100
+ ),
101
+ )
102
+ return response.text or ""
103
+
104
+
105
+ # ---- worker setup (outside the sandbox: real client + credentials) ----
106
+ async def main() -> None:
107
+ gemini = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
108
+ plugin = GoogleGenAIPlugin(gemini)
109
+
110
+ client = await Client.connect("localhost:7233", plugins=[plugin])
111
+ async with Worker(
112
+ client,
113
+ task_queue="gemini",
114
+ workflows=[WeatherAgent],
115
+ activities=[get_weather],
116
+ ):
117
+ result = await client.execute_workflow(
118
+ WeatherAgent.run,
119
+ "What's the weather in Tokyo?",
120
+ id="weather-1",
121
+ task_queue="gemini",
122
+ )
123
+ print(result)
124
+ ```
125
+
126
+ Construct `TemporalAsyncClient` **inside** the workflow; construct the real
127
+ `genai.Client` and `GoogleGenAIPlugin` **on the worker**.
128
+
129
+ ## What this plugin gives you
130
+
131
+ | Surface | Workflow API | Runs as |
132
+ | --- | --- | --- |
133
+ | Model calls | `client.models.generate_content` / `generate_content_stream` | activity (AFC loop in workflow) |
134
+ | Tools | `activity_as_tool(fn, ...)` | one activity per tool call |
135
+ | Files | `client.files.upload` / `download` | activity |
136
+ | File search | `client.file_search_stores.upload_to_file_search_store` | activity |
137
+ | Interactions | `client.interactions.create` / `get` / `cancel` / `delete` | whole-operation activity |
138
+ | Managed agents | `client.agents.create` / `get` / `list` / `delete` | whole-operation activity |
139
+ | MCP (client-side) | `TemporalMcpClientSession(name)` in `tools=[...]` | `list_tools` / `call_tool` activities |
140
+
141
+ Streamed responses are batched: the activity drains the stream and the workflow
142
+ iterates the collected chunks/events. `client.webhooks` is not supported in
143
+ workflows and raises.
144
+
145
+ ## Tool calling
146
+
147
+ `activity_as_tool` wraps any `@activity.defn` function as a Gemini tool. When the
148
+ model calls it, the AFC loop (running in the workflow) dispatches it as a
149
+ durable activity:
150
+
151
+ ```python
152
+ activity_as_tool(
153
+ get_weather,
154
+ activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)),
155
+ )
156
+ ```
157
+
158
+ A timeout is required — `activity_config` must set `start_to_close_timeout` or
159
+ `schedule_to_close_timeout` (Temporal needs one; there is no default for tools).
160
+
161
+ ## MCP support
162
+
163
+ Install the optional dependency for client-side MCP:
164
+
165
+ ```bash
166
+ uv add "temporalio-google-genai[mcp]"
167
+ ```
168
+
169
+ This adapter requires MCP Python SDK v1. The base package does not require MCP
170
+ and can be installed alongside MCP v2 integrations. Server-side MCP on Vertex AI
171
+ and the Interactions API do not require this extra.
172
+
173
+ Client-side MCP (Gemini Developer API) is wired through the plugin: register the
174
+ server on the worker and reference it by name in the workflow.
175
+
176
+ ```python
177
+ from contextlib import asynccontextmanager
178
+ import sys
179
+
180
+ from mcp import ClientSession, StdioServerParameters
181
+ from mcp.client.stdio import stdio_client
182
+
183
+ from temporalio.google_genai import TemporalMcpClientSession
184
+
185
+
186
+ # ---- worker: a factory yielding a connected, initialized session ----
187
+ @asynccontextmanager
188
+ async def weather_mcp():
189
+ params = StdioServerParameters(command=sys.executable, args=["weather_server.py"])
190
+ async with stdio_client(params) as (read, write):
191
+ async with ClientSession(read, write) as session:
192
+ await session.initialize()
193
+ yield session
194
+
195
+
196
+ plugin = GoogleGenAIPlugin(
197
+ genai.Client(api_key=os.environ["GOOGLE_API_KEY"]),
198
+ mcp_servers={"weather": weather_mcp},
199
+ mcp_connection_idle_timeout=timedelta(minutes=5),
200
+ )
201
+
202
+
203
+ # ---- workflow: reference the server by name in the tools list ----
204
+ @workflow.defn
205
+ class McpAgent:
206
+ @workflow.run
207
+ async def run(self, prompt: str) -> str:
208
+ client = TemporalAsyncClient()
209
+ session = TemporalMcpClientSession(
210
+ "weather",
211
+ activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)),
212
+ )
213
+ response = await client.models.generate_content(
214
+ model="gemini-2.5-flash",
215
+ contents=prompt,
216
+ config=types.GenerateContentConfig(tools=[session]),
217
+ )
218
+ return response.text or ""
219
+ ```
220
+
221
+ The MCP connection lives on the worker (pooled, idle-evicted); the workflow only
222
+ carries the server name. Tool discovery and calls run as `{name}-list-tools` /
223
+ `{name}-call-tool` activities, so the full tool parameter schema reaches the
224
+ model. Set `cache_tools=True` to list a server's tools once per workflow instead
225
+ of per turn.
226
+
227
+ ## Streaming
228
+
229
+ `generate_content_stream` works as usual — the workflow iterates chunks (batched
230
+ from the activity). To let an **external** consumer (a chat UI) observe chunks in
231
+ real time while the workflow runs durably, set `streaming_topic` on the client
232
+ and host a [`WorkflowStream`](https://github.com/temporalio/sdk-python/tree/6adc0d84290a79952dee3ef02c36f6ed9334874a/temporalio/contrib/workflow_streams/) in the workflow. Each
233
+ streamed `GenerateContentResponse` is published to that topic as it arrives:
234
+
235
+ ```python
236
+ from temporalio.contrib.workflow_streams import WorkflowStream
237
+
238
+
239
+ @workflow.defn
240
+ class StreamingAgent:
241
+ @workflow.init
242
+ def __init__(self, prompt: str) -> None:
243
+ self.stream = WorkflowStream() # required when streaming_topic is set
244
+
245
+ @workflow.run
246
+ async def run(self, prompt: str) -> str:
247
+ client = TemporalAsyncClient(streaming_topic="gemini")
248
+ text = []
249
+ async for chunk in await client.models.generate_content_stream(
250
+ model="gemini-2.5-flash", contents=prompt,
251
+ ):
252
+ text.append(chunk.text or "")
253
+ return "".join(text)
254
+ ```
255
+
256
+ Consume the stream from outside the workflow:
257
+
258
+ ```python
259
+ from temporalio.contrib.workflow_streams import WorkflowStreamClient
260
+
261
+
262
+ async def consume(client, workflow_id):
263
+ stream = WorkflowStreamClient.create(client, workflow_id)
264
+ async for item in stream.subscribe(
265
+ ["gemini"], result_type=types.GenerateContentResponse,
266
+ ):
267
+ print(item.data.text, end="", flush=True)
268
+ ```
269
+
270
+ The workflow's own iteration is unchanged (it still receives batched chunks for
271
+ the SDK to parse); the topic is purely for external real-time observation. If
272
+ `streaming_topic` is set but the workflow hosts no `WorkflowStream`, the call
273
+ raises `GoogleGenAIError`. Tune flush cadence with
274
+ `TemporalAsyncClient(streaming_topic=..., streaming_batch_interval=...)`
275
+ (default 100ms).
276
+
277
+ ## Retries & errors
278
+
279
+ Temporal owns retries. Configure them with the activity `retry_policy` via
280
+ `activity_config`. The plugin **rejects** the SDK's own retry config so retries
281
+ don't compound:
282
+
283
+ - Constructing the plugin with a `genai.Client` that has
284
+ `http_options.retry_options` raises `ValueError`.
285
+ - Setting `http_options.retry_options` on a per-request call raises
286
+ `GoogleGenAIError`.
287
+
288
+ API-call activities classify failures: transient statuses (408, 429, 5xx) stay
289
+ retryable (the activity's `retry_policy` applies); other statuses (e.g. 4xx) are
290
+ non-retryable so the workflow fails fast.
291
+
292
+ ## Vertex AI
293
+
294
+ Pass `vertexai=True` to both the worker-side `genai.Client` and the
295
+ workflow-side `TemporalAsyncClient`. On the workflow side you must also set
296
+ `project` and `location` **explicitly**:
297
+
298
+ ```python
299
+ # worker
300
+ genai.Client(vertexai=True, project="my-project", location="us-central1")
301
+
302
+ # workflow
303
+ TemporalAsyncClient(vertexai=True, project="my-project", location="us-central1")
304
+ ```
305
+
306
+ Normally the SDK auto-discovers `project`/`location` from the environment
307
+ (credentials, ADC, metadata server). That discovery
308
+ would be non-deterministic and break replay. Setting them by hand
309
+ keeps it deterministic.
310
+
311
+ ## Composing with other plugins
312
+
313
+ `GoogleGenAIPlugin` is a `temporalio.plugin.SimplePlugin`; pass it in the
314
+ `plugins=[...]` list alongside others (e.g. OpenTelemetry). It contributes a
315
+ Pydantic data converter, the Gemini activities, a sandbox-passthrough config for
316
+ `google.genai` (and `mcp`), and registers `GoogleGenAIError` as a workflow
317
+ failure type. When composing data converters, construct the plugins so their
318
+ converters are compatible.
@@ -0,0 +1,292 @@
1
+ # Google Gemini SDK Integration for Temporal
2
+
3
+ > Release stage: [Public Preview](https://docs.temporal.io/develop/python/integrations/google-genai).
4
+
5
+ ## Overview
6
+
7
+ This plugin lets you use the [Google Gemini SDK](https://googleapis.github.io/python-genai/)
8
+ (`google-genai`) inside Temporal workflows with durable execution. Every Gemini
9
+ API call becomes a **Temporal activity**, so model calls, tool calls, file
10
+ operations, interactions, and managed agents are retried, recorded in history,
11
+ and survive worker restarts.
12
+
13
+ Key properties:
14
+
15
+ - **Credentials never enter the workflow.** The real `genai.Client` lives only
16
+ on the worker, inside activities; no API keys or tokens appear in event
17
+ history.
18
+ - **The SDK's automatic function calling (AFC) loop runs in the workflow**, so
19
+ tool wrappers (`activity_as_tool`) work naturally — no manual agent loop.
20
+ - **Temporal owns retries.** Configure them via the activity `retry_policy`; the
21
+ SDK's own retry loop is rejected to avoid double-retry (see
22
+ [Retries & errors](#retries--errors)).
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ uv add temporalio-google-genai
28
+ ```
29
+
30
+ ## Hello World
31
+
32
+ ```python
33
+ import os
34
+ from datetime import timedelta
35
+
36
+ from google import genai
37
+ from google.genai import types
38
+
39
+ from temporalio import activity, workflow
40
+ from temporalio.client import Client
41
+ from temporalio.google_genai import (
42
+ GoogleGenAIPlugin,
43
+ TemporalAsyncClient,
44
+ activity_as_tool,
45
+ )
46
+ from temporalio.worker import Worker
47
+ from temporalio.workflow import ActivityConfig
48
+
49
+
50
+ # ---- a tool, as a normal Temporal activity (runs on the worker) ----
51
+ @activity.defn
52
+ async def get_weather(city: str) -> str:
53
+ return f"It's sunny in {city}."
54
+
55
+
56
+ # ---- the workflow (runs in the Temporal sandbox) ----
57
+ @workflow.defn
58
+ class WeatherAgent:
59
+ @workflow.run
60
+ async def run(self, prompt: str) -> str:
61
+ client = TemporalAsyncClient()
62
+ response = await client.models.generate_content(
63
+ model="gemini-2.5-flash",
64
+ contents=prompt,
65
+ config=types.GenerateContentConfig(
66
+ tools=[
67
+ activity_as_tool(
68
+ get_weather,
69
+ activity_config=ActivityConfig(
70
+ start_to_close_timeout=timedelta(seconds=30),
71
+ ),
72
+ ),
73
+ ],
74
+ ),
75
+ )
76
+ return response.text or ""
77
+
78
+
79
+ # ---- worker setup (outside the sandbox: real client + credentials) ----
80
+ async def main() -> None:
81
+ gemini = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
82
+ plugin = GoogleGenAIPlugin(gemini)
83
+
84
+ client = await Client.connect("localhost:7233", plugins=[plugin])
85
+ async with Worker(
86
+ client,
87
+ task_queue="gemini",
88
+ workflows=[WeatherAgent],
89
+ activities=[get_weather],
90
+ ):
91
+ result = await client.execute_workflow(
92
+ WeatherAgent.run,
93
+ "What's the weather in Tokyo?",
94
+ id="weather-1",
95
+ task_queue="gemini",
96
+ )
97
+ print(result)
98
+ ```
99
+
100
+ Construct `TemporalAsyncClient` **inside** the workflow; construct the real
101
+ `genai.Client` and `GoogleGenAIPlugin` **on the worker**.
102
+
103
+ ## What this plugin gives you
104
+
105
+ | Surface | Workflow API | Runs as |
106
+ | --- | --- | --- |
107
+ | Model calls | `client.models.generate_content` / `generate_content_stream` | activity (AFC loop in workflow) |
108
+ | Tools | `activity_as_tool(fn, ...)` | one activity per tool call |
109
+ | Files | `client.files.upload` / `download` | activity |
110
+ | File search | `client.file_search_stores.upload_to_file_search_store` | activity |
111
+ | Interactions | `client.interactions.create` / `get` / `cancel` / `delete` | whole-operation activity |
112
+ | Managed agents | `client.agents.create` / `get` / `list` / `delete` | whole-operation activity |
113
+ | MCP (client-side) | `TemporalMcpClientSession(name)` in `tools=[...]` | `list_tools` / `call_tool` activities |
114
+
115
+ Streamed responses are batched: the activity drains the stream and the workflow
116
+ iterates the collected chunks/events. `client.webhooks` is not supported in
117
+ workflows and raises.
118
+
119
+ ## Tool calling
120
+
121
+ `activity_as_tool` wraps any `@activity.defn` function as a Gemini tool. When the
122
+ model calls it, the AFC loop (running in the workflow) dispatches it as a
123
+ durable activity:
124
+
125
+ ```python
126
+ activity_as_tool(
127
+ get_weather,
128
+ activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)),
129
+ )
130
+ ```
131
+
132
+ A timeout is required — `activity_config` must set `start_to_close_timeout` or
133
+ `schedule_to_close_timeout` (Temporal needs one; there is no default for tools).
134
+
135
+ ## MCP support
136
+
137
+ Install the optional dependency for client-side MCP:
138
+
139
+ ```bash
140
+ uv add "temporalio-google-genai[mcp]"
141
+ ```
142
+
143
+ This adapter requires MCP Python SDK v1. The base package does not require MCP
144
+ and can be installed alongside MCP v2 integrations. Server-side MCP on Vertex AI
145
+ and the Interactions API do not require this extra.
146
+
147
+ Client-side MCP (Gemini Developer API) is wired through the plugin: register the
148
+ server on the worker and reference it by name in the workflow.
149
+
150
+ ```python
151
+ from contextlib import asynccontextmanager
152
+ import sys
153
+
154
+ from mcp import ClientSession, StdioServerParameters
155
+ from mcp.client.stdio import stdio_client
156
+
157
+ from temporalio.google_genai import TemporalMcpClientSession
158
+
159
+
160
+ # ---- worker: a factory yielding a connected, initialized session ----
161
+ @asynccontextmanager
162
+ async def weather_mcp():
163
+ params = StdioServerParameters(command=sys.executable, args=["weather_server.py"])
164
+ async with stdio_client(params) as (read, write):
165
+ async with ClientSession(read, write) as session:
166
+ await session.initialize()
167
+ yield session
168
+
169
+
170
+ plugin = GoogleGenAIPlugin(
171
+ genai.Client(api_key=os.environ["GOOGLE_API_KEY"]),
172
+ mcp_servers={"weather": weather_mcp},
173
+ mcp_connection_idle_timeout=timedelta(minutes=5),
174
+ )
175
+
176
+
177
+ # ---- workflow: reference the server by name in the tools list ----
178
+ @workflow.defn
179
+ class McpAgent:
180
+ @workflow.run
181
+ async def run(self, prompt: str) -> str:
182
+ client = TemporalAsyncClient()
183
+ session = TemporalMcpClientSession(
184
+ "weather",
185
+ activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)),
186
+ )
187
+ response = await client.models.generate_content(
188
+ model="gemini-2.5-flash",
189
+ contents=prompt,
190
+ config=types.GenerateContentConfig(tools=[session]),
191
+ )
192
+ return response.text or ""
193
+ ```
194
+
195
+ The MCP connection lives on the worker (pooled, idle-evicted); the workflow only
196
+ carries the server name. Tool discovery and calls run as `{name}-list-tools` /
197
+ `{name}-call-tool` activities, so the full tool parameter schema reaches the
198
+ model. Set `cache_tools=True` to list a server's tools once per workflow instead
199
+ of per turn.
200
+
201
+ ## Streaming
202
+
203
+ `generate_content_stream` works as usual — the workflow iterates chunks (batched
204
+ from the activity). To let an **external** consumer (a chat UI) observe chunks in
205
+ real time while the workflow runs durably, set `streaming_topic` on the client
206
+ and host a [`WorkflowStream`](https://github.com/temporalio/sdk-python/tree/6adc0d84290a79952dee3ef02c36f6ed9334874a/temporalio/contrib/workflow_streams/) in the workflow. Each
207
+ streamed `GenerateContentResponse` is published to that topic as it arrives:
208
+
209
+ ```python
210
+ from temporalio.contrib.workflow_streams import WorkflowStream
211
+
212
+
213
+ @workflow.defn
214
+ class StreamingAgent:
215
+ @workflow.init
216
+ def __init__(self, prompt: str) -> None:
217
+ self.stream = WorkflowStream() # required when streaming_topic is set
218
+
219
+ @workflow.run
220
+ async def run(self, prompt: str) -> str:
221
+ client = TemporalAsyncClient(streaming_topic="gemini")
222
+ text = []
223
+ async for chunk in await client.models.generate_content_stream(
224
+ model="gemini-2.5-flash", contents=prompt,
225
+ ):
226
+ text.append(chunk.text or "")
227
+ return "".join(text)
228
+ ```
229
+
230
+ Consume the stream from outside the workflow:
231
+
232
+ ```python
233
+ from temporalio.contrib.workflow_streams import WorkflowStreamClient
234
+
235
+
236
+ async def consume(client, workflow_id):
237
+ stream = WorkflowStreamClient.create(client, workflow_id)
238
+ async for item in stream.subscribe(
239
+ ["gemini"], result_type=types.GenerateContentResponse,
240
+ ):
241
+ print(item.data.text, end="", flush=True)
242
+ ```
243
+
244
+ The workflow's own iteration is unchanged (it still receives batched chunks for
245
+ the SDK to parse); the topic is purely for external real-time observation. If
246
+ `streaming_topic` is set but the workflow hosts no `WorkflowStream`, the call
247
+ raises `GoogleGenAIError`. Tune flush cadence with
248
+ `TemporalAsyncClient(streaming_topic=..., streaming_batch_interval=...)`
249
+ (default 100ms).
250
+
251
+ ## Retries & errors
252
+
253
+ Temporal owns retries. Configure them with the activity `retry_policy` via
254
+ `activity_config`. The plugin **rejects** the SDK's own retry config so retries
255
+ don't compound:
256
+
257
+ - Constructing the plugin with a `genai.Client` that has
258
+ `http_options.retry_options` raises `ValueError`.
259
+ - Setting `http_options.retry_options` on a per-request call raises
260
+ `GoogleGenAIError`.
261
+
262
+ API-call activities classify failures: transient statuses (408, 429, 5xx) stay
263
+ retryable (the activity's `retry_policy` applies); other statuses (e.g. 4xx) are
264
+ non-retryable so the workflow fails fast.
265
+
266
+ ## Vertex AI
267
+
268
+ Pass `vertexai=True` to both the worker-side `genai.Client` and the
269
+ workflow-side `TemporalAsyncClient`. On the workflow side you must also set
270
+ `project` and `location` **explicitly**:
271
+
272
+ ```python
273
+ # worker
274
+ genai.Client(vertexai=True, project="my-project", location="us-central1")
275
+
276
+ # workflow
277
+ TemporalAsyncClient(vertexai=True, project="my-project", location="us-central1")
278
+ ```
279
+
280
+ Normally the SDK auto-discovers `project`/`location` from the environment
281
+ (credentials, ADC, metadata server). That discovery
282
+ would be non-deterministic and break replay. Setting them by hand
283
+ keeps it deterministic.
284
+
285
+ ## Composing with other plugins
286
+
287
+ `GoogleGenAIPlugin` is a `temporalio.plugin.SimplePlugin`; pass it in the
288
+ `plugins=[...]` list alongside others (e.g. OpenTelemetry). It contributes a
289
+ Pydantic data converter, the Gemini activities, a sandbox-passthrough config for
290
+ `google.genai` (and `mcp`), and registers `GoogleGenAIError` as a workflow
291
+ failure type. When composing data converters, construct the plugins so their
292
+ converters are compatible.