dex-agents-google-adk 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.
@@ -0,0 +1,105 @@
1
+ # License
2
+
3
+ Copyright (c) 2026 Super Durable, Inc.
4
+
5
+ Portions of this software are licensed as follows:
6
+
7
+ - Legacy Materials remain under their original licenses as described in
8
+ `LEGACY_NOTICES.md`.
9
+ - Third-Party Materials remain under the licenses provided by their owners.
10
+ - The `docs/` and `examples/` directories are not licensed under the
11
+ Sustainable Use License and retain their existing licensing status.
12
+ - All other material owned or licensable by Super Durable, Inc. is available
13
+ under the Sustainable Use License below.
14
+
15
+ See `LEGACY_NOTICES.md` for the applicable legacy and third-party notices.
16
+
17
+ ## Sustainable Use License
18
+
19
+ Version 1.0
20
+
21
+ ### Acceptance
22
+
23
+ By using the software, you agree to all of the terms and conditions below.
24
+
25
+ ### Copyright License
26
+
27
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
28
+ non-sublicensable, non-transferable license to use, copy, distribute, make
29
+ available, and prepare derivative works of the software, in each case subject
30
+ to the limitations below.
31
+
32
+ ### Limitations
33
+
34
+ You may use or modify the software only for your own internal business purposes
35
+ or for non-commercial or personal use. You may distribute the software or
36
+ provide it to others only if you do so free of charge for non-commercial
37
+ purposes. You may not alter, remove, or obscure any licensing, copyright, or
38
+ other notices of the licensor in the software. Any use of the licensor's
39
+ trademarks is subject to applicable law.
40
+
41
+ ### Patents
42
+
43
+ The licensor grants you a license, under any patent claims the licensor can
44
+ license, or becomes able to license, to make, have made, use, sell, offer for
45
+ sale, import and have imported the software, in each case subject to the
46
+ limitations and conditions in this license. This license does not cover any
47
+ patent claims that you cause to be infringed by modifications or additions to
48
+ the software.
49
+
50
+ If you or your company make any written claim that the software infringes or
51
+ contributes to infringement of any patent, your patent license for the software
52
+ granted under these terms ends immediately. If your company makes such a claim,
53
+ your patent license ends immediately for work on behalf of your company.
54
+
55
+ ### Notices
56
+
57
+ You must ensure that anyone who gets a copy of any part of the software from
58
+ you also gets a copy of these terms. If you modify the software, you must
59
+ include in any modified copies of the software a prominent notice stating that
60
+ you have modified the software.
61
+
62
+ ### No Other Rights
63
+
64
+ These terms do not imply any licenses other than those expressly granted in
65
+ these terms.
66
+
67
+ ### Termination
68
+
69
+ If you use the software in violation of these terms, such use is not licensed,
70
+ and your license will automatically terminate. If the licensor provides you
71
+ with a notice of your violation, and you cease all violation of this license no
72
+ later than 30 days after you receive that notice, your license will be
73
+ reinstated retroactively. However, if you violate these terms after such
74
+ reinstatement, any additional violation of these terms will cause your license
75
+ to terminate automatically and permanently.
76
+
77
+ ### No Liability
78
+
79
+ As far as the law allows, the software comes as is, without any warranty or
80
+ condition, and the licensor will not be liable to you for any damages arising
81
+ out of these terms or the use or nature of the software, under any kind of
82
+ legal claim.
83
+
84
+ ### Definitions
85
+
86
+ The "licensor" is the entity offering these terms.
87
+
88
+ The "software" is the software the licensor makes available under these terms,
89
+ including any portion of it.
90
+
91
+ "You" refers to the individual or entity agreeing to these terms.
92
+
93
+ "Your company" is any legal entity, sole proprietorship, or other kind of
94
+ organization that you work for, plus all organizations that have control over,
95
+ are under the control of, or are under common control with that organization.
96
+ Control means ownership of substantially all the assets of an entity, or the
97
+ power to direct its management and policies by vote, contract, or otherwise.
98
+ Control can be direct or indirect.
99
+
100
+ "Your license" is the license granted to you for the software under these
101
+ terms.
102
+
103
+ "Use" means anything you do with the software requiring your license.
104
+
105
+ "Trademark" means trademarks, service marks, and similar rights.
@@ -0,0 +1,346 @@
1
+ Metadata-Version: 2.4
2
+ Name: dex-agents-google-adk
3
+ Version: 0.1.0
4
+ Summary: Run Google ADK agents as durable Dex agent sessions
5
+ Author: Super Durable
6
+ License-Expression: LicenseRef-Sustainable-Use-1.0
7
+ License-File: LICENSE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Typing :: Typed
12
+ Requires-Dist: dex-agents==0.1.0
13
+ Requires-Dist: dex-python-sdk==1.30.0
14
+ Requires-Dist: google-adk>=2.13,<3
15
+ Requires-Dist: pydantic>=2.11,<3
16
+ Requires-Python: >=3.11
17
+ Project-URL: Homepage, https://github.com/superdurable/dex/tree/main/agents/python/google-adk
18
+ Project-URL: Repository, https://github.com/superdurable/dex
19
+ Project-URL: Changelog, https://github.com/superdurable/dex/blob/main/agents/CHANGELOG.md
20
+ Description-Content-Type: text/markdown
21
+
22
+ # dex-agents-google-adk
23
+
24
+ `dex-agents-google-adk` (import `dex_agents_google_adk`) runs
25
+ [Google ADK](https://google.github.io/adk-docs/) agents as durable Dex agent
26
+ sessions. It implements the `AgentFrameworkRunner` contract of
27
+ [`dex-agents`](https://github.com/superdurable/dex/blob/main/agents/python/core/README.md) and registers the shared agent session Flow
28
+ for an ADK agent. One Flow execution is one session, and the Flow ID is both
29
+ the Dex session ID and the ADK session ID.
30
+
31
+ The session runs in **Step mode**. Each `RunModel` Step lets ADK make one
32
+ model call. Every tool call the model asks for, READ included, pauses the ADK
33
+ invocation through ADK's own tool confirmation and runs in its own Dex Step;
34
+ the tool calls of one response run in parallel. Each call therefore has its
35
+ own retry policy, failure route, history entry, and Dex Web node, and a crash
36
+ repeats at most the one call that was running. The plan is
37
+ [agent-framework-adapters.md](https://github.com/superdurable/dex/blob/main/docs/design/plan/agent-framework-adapters.md).
38
+
39
+ ## Installation
40
+
41
+ The package needs Python 3.11 or later. It requires `dex-agents` at the same
42
+ version, `dex-python-sdk==1.30.0`, and `google-adk` 2.13 or a later 2.x
43
+ release:
44
+
45
+ ```bash
46
+ pip install dex-agents-google-adk
47
+ ```
48
+
49
+ Develop the package inside this repository with uv 0.12.2. There it resolves
50
+ `dex-agents` from `../core` by path through `[tool.uv.sources]`; the published
51
+ wheel requires `dex-agents==<same version>` from PyPI instead:
52
+
53
+ ```bash
54
+ cd agents/python/google-adk
55
+ uv sync --locked
56
+ DEXCLI=/path/to/dexcli ./ci.sh
57
+ ```
58
+
59
+ ## Running an agent
60
+
61
+ ```python
62
+ import asyncio
63
+
64
+ from dex import AsyncClient, AsyncWorker, BlobCacheConfig, ClientOptions, Registry
65
+ from dex import WorkerOptions, open_blob_cache
66
+ from dex_agents import AgentSessionClient, ToolEffect, ToolPolicy
67
+ from dex_agents_google_adk import adk_agent_session_flow, idempotency_key_for_tool_call
68
+ from google.adk.agents import LlmAgent
69
+ from google.adk.tools.tool_context import ToolContext
70
+
71
+
72
+ def search_orders(customer_id: str) -> list[dict[str, str]]:
73
+ """List a customer's orders."""
74
+ return [{"order_id": "7", "status": "delivered"}]
75
+
76
+
77
+ def refund_order(order_id: str, tool_context: ToolContext) -> str:
78
+ """Refund one order."""
79
+ key = idempotency_key_for_tool_call(tool_context)
80
+ return f"refund for order {order_id} created with key {key}"
81
+
82
+
83
+ def send_email(to: str, body: str) -> str:
84
+ """Send an email to the customer."""
85
+ return f"sent to {to}"
86
+
87
+
88
+ agent = LlmAgent(
89
+ name="support",
90
+ model="gemini-2.5-flash",
91
+ instruction="Help customers with their orders.",
92
+ tools=[search_orders, refund_order, send_email],
93
+ )
94
+ flow = adk_agent_session_flow(
95
+ agent,
96
+ flow_type="SupportAgent",
97
+ tool_policies={
98
+ "search_orders": ToolPolicy(ToolEffect.READ),
99
+ "refund_order": ToolPolicy(ToolEffect.IDEMPOTENT_WRITE, requires_approval=True),
100
+ # send_email has no policy: unclassified, it runs once after approval.
101
+ },
102
+ )
103
+
104
+
105
+ async def start_support_session(sessions: AgentSessionClient) -> None:
106
+ await sessions.start_session("session-42", "Refund my last order, I am customer 9")
107
+
108
+
109
+ async def approve_pending_tool_calls(sessions: AgentSessionClient) -> None:
110
+ snapshot = await sessions.get_agent_snapshot("session-42")
111
+ for interrupt in snapshot.pending_interrupts:
112
+ await sessions.resolve_interrupt("session-42", interrupt.interrupt_id, approved=True)
113
+
114
+
115
+ async def main() -> None:
116
+ registry = Registry([flow], allow_async_handlers=True)
117
+ cache = open_blob_cache(BlobCacheConfig(".dex-cache", 64 * 1024 * 1024))
118
+ worker = AsyncWorker(registry, cache, WorkerOptions(server_address="localhost:8801"))
119
+ client = AsyncClient(registry, cache, ClientOptions(server_address="localhost:8801"))
120
+ serving = asyncio.create_task(worker.start())
121
+ await worker.wait_pull_registered(timeout=30)
122
+ sessions = AgentSessionClient(client, flow)
123
+ await start_support_session(sessions)
124
+ # Later, for example from an approval UI:
125
+ await approve_pending_tool_calls(sessions)
126
+ await serving
127
+
128
+
129
+ asyncio.run(main())
130
+ ```
131
+
132
+ Build the Flow once per agent, with a Flow type unique within the Registry,
133
+ and build the same Flow in every process that starts sessions or calls their
134
+ RPCs. The Steps are `async`, so the Registry needs `allow_async_handlers=True`
135
+ and an `AsyncWorker` serves it. Tool calls of one model response run as
136
+ parallel Steps, so give the Worker enough task slots
137
+ (`PullWorkerOptions.max_concurrent_tasks`) for the largest batch. The model
138
+ reads its key from the environment as in any ADK application, such as
139
+ `GOOGLE_API_KEY`.
140
+
141
+ `adk_agent_session_flow(agent, *, flow_type, ...)` takes:
142
+
143
+ | Argument | Default | Meaning |
144
+ | --- | --- | --- |
145
+ | `agent` | required | Root `BaseAgent`, or an ADK `App` that carries it with plugins and compaction or context cache configuration. |
146
+ | `flow_type` | required | Flow type of the agent's sessions. Keep it stable. |
147
+ | `tool_policies` | none | `ToolPolicy` by tool name. |
148
+ | `default_tool_effect` | `UNCLASSIFIED` | Effect of a tool without a policy or ADK metadata. |
149
+ | `interrupt_deadline` | none | How long an approval may stay pending. |
150
+ | `event_stream_capacity_bytes` | 10 MiB | Byte budget of the `AgentEvents` Stream. |
151
+ | `run_config` | `RunConfig(streaming_mode=StreamingMode.SSE)` | ADK run configuration of every run. |
152
+ | `user_id` | `"user"` | ADK user ID of every session. |
153
+ | `artifact_service`, `memory_service`, `credential_service` | none | ADK services, owned by the application. |
154
+
155
+ `AdkAgentRunner` takes the same agent and ADK arguments when you build
156
+ `AgentSessionFlow` yourself.
157
+
158
+ ## Step shape
159
+
160
+ A turn whose model reads once and then answers runs these Steps:
161
+
162
+ ```text
163
+ AwaitUserMessage -> RunModel -> CallTool -> ResumeTurn -> RunModel -> AwaitUserMessage
164
+ ```
165
+
166
+ A response with several tool calls starts one Step per call, in parallel. A
167
+ call that needs approval waits in `AwaitToolApproval` first. See the
168
+ [core README](https://github.com/superdurable/dex/blob/main/agents/python/core/README.md#flow-shape) for every Step.
169
+
170
+ ## Effects and policies
171
+
172
+ | Effect | Step | Retry | Crash mid-call | Default approval |
173
+ | --- | --- | --- | --- | --- |
174
+ | `read` | `CallTool` | Up to 3 attempts; then `failed` | Runs again | No |
175
+ | `idempotent_write` | `CallTool` | Up to 3 attempts, key `<flowId>/<turnId>/<toolCallId>`; then `outcome_unknown` | Runs again with the same key | No |
176
+ | `non_idempotent_write` | `CallSingleAttemptTool` | One attempt; then `outcome_unknown` | Outcome unknown | Yes |
177
+ | `unclassified` | `CallSingleAttemptTool` | One attempt; then `outcome_unknown` | Outcome unknown | Yes |
178
+
179
+ A tool's effect comes from, in order:
180
+
181
+ 1. Its `ToolPolicy` in `tool_policies`.
182
+ 2. ADK metadata: `load_artifacts`, `load_memory`, and `preload_memory` are
183
+ `read`. An MCP tool with `readOnlyHint` is `read`; with `idempotentHint` it
184
+ is `idempotent_write`.
185
+ 3. `default_tool_effect`.
186
+
187
+ A tool built with ADK's `FunctionTool(func, require_confirmation=...)` always
188
+ waits for approval when its `require_confirmation` is true for the call, even
189
+ when its policy says otherwise.
190
+
191
+ `idempotency_key_for_tool_call(tool_context)` returns a call's idempotency key,
192
+ `"<flow ID>/<turn ID>/<tool call ID>"`. It is the core's
193
+ `DeferredToolExecution.idempotency_key`, which the adapter passes to the tool
194
+ in `tool_context.custom_metadata` under `IDEMPOTENCY_KEY_CUSTOM_METADATA_KEY`.
195
+ Every attempt of an `idempotent_write` call gets the same key; pass it to the
196
+ external system.
197
+
198
+ ### ADK control tools
199
+
200
+ Some ADK tools steer the invocation itself, so they run inside `RunModel` and
201
+ get no Step: `transfer_to_agent`, `exit_loop`, `set_model_response`,
202
+ `finish_task`, and the tools ADK makes for sub-agents in `single_turn` or
203
+ `task` mode. The tool calls of those sub-agents get Steps as usual. A model
204
+ response that calls a control tool together with other tools fails the turn
205
+ before any tool runs, because ADK would apply the control tool before the
206
+ other results exist.
207
+
208
+ ## How a tool call runs
209
+
210
+ The adapter adds a plugin, after the App's own plugins, to a copy of the App
211
+ for every `RunModel` attempt. For each tool call except a control tool, the
212
+ plugin resolves the policy, then runs the calling agent's
213
+ `before_tool_callback` chain, so a guardrail callback can still answer the
214
+ call without a Step. Otherwise it calls `tool_context.request_confirmation`,
215
+ and ADK pauses the invocation on an `adk_request_confirmation` call once the
216
+ response's calls are all paused. `RunModel` returns `ToolCallsRequested` with
217
+ the turn's events so far as the framework run state.
218
+
219
+ Each tool Step calls `execute_deferred_tool_call`, which finds the first tool
220
+ with the call's name in a depth-first walk of the agent tree and calls its
221
+ `run_async` once. The next `RunModel` answers each `adk_request_confirmation`
222
+ call, one message per ADK branch, with `confirmed` set to whether the call was
223
+ approved. ADK re-executes the confirmed calls, and the plugin returns the
224
+ recorded result instead of running the tool:
225
+
226
+ | Record status | Function response the model sees |
227
+ | --- | --- |
228
+ | `succeeded` | The tool's response. A bare string result is `{"result": "<text>"}`. |
229
+ | `failed` | `{"error": "The tool call failed: <ToolExecutionError message>"}` |
230
+ | `rejected` | `{"error": "<rejection message>"}` |
231
+ | `outcome_unknown` | `{"error": "<OUTCOME_UNKNOWN_TOOL_RESULT>"}` |
232
+
233
+ Raise `dex_agents.ToolExecutionError` from a tool to report a failure the
234
+ model should see without a retry; the record becomes `failed`. Any other
235
+ exception, a timeout, or a Worker crash fails the tool Step: a `read` or
236
+ `idempotent_write` call retries, and once its attempts run out the record
237
+ becomes `failed` for a read and `outcome_unknown` for a write. A
238
+ single-attempt call records `outcome_unknown` at once.
239
+
240
+ When a turn fails after some of its tool calls finished, the session stores a
241
+ turn-failure note through `build_user_message`: an ADK user event whose text
242
+ lists each call and its outcome. The next turn's model sees it as user text,
243
+ so it does not repeat those writes.
244
+
245
+ ## Multi-agent trees
246
+
247
+ The adapter runs the App as resumable (`ResumabilityConfig(is_resumable=True)`),
248
+ so a paused `SequentialAgent`, `LoopAgent`, `ParallelAgent`, or sub-agent
249
+ continues where it stopped. A task-mode sub-agent that asked the user a
250
+ question receives the user's later messages, because the adapter lets ADK
251
+ append the turn's user message and replays stored user messages with the
252
+ isolation scope ADK gave them.
253
+
254
+ The next `RunModel` answers the paused calls of each ADK branch in its own
255
+ message, and ADK runs on once the first branch finishes. That is safe only
256
+ when every branch is an `LlmAgent` directly under a root `ParallelAgent`, so a
257
+ model step whose tool calls span several branches anywhere else fails the turn
258
+ before any tool runs: for example a `ParallelAgent` inside a `SequentialAgent`
259
+ or `LoopAgent`, a root `ParallelAgent` whose branch is a `SequentialAgent` or
260
+ `LoopAgent`, or two `single_turn` sub-agents called in one response. After every resume, each
261
+ answered call must have reached its agent with the recorded result; otherwise
262
+ the turn fails and the session stores the turn-failure note.
263
+
264
+ Sub-agents in `single_turn` or `task` mode need a root `LlmAgent`, because ADK
265
+ runs them only there; construction fails otherwise.
266
+
267
+ An `LlmAgent` makes one model call per `RunModel`. Agents that continue
268
+ without tools make their model calls in the same `RunModel`: the next agent of
269
+ a `SequentialAgent`, the target of `transfer_to_agent`, a sub-agent in
270
+ `single_turn` or `task` mode, and the other branches of a `ParallelAgent`. A
271
+ `RunModel` retry repeats those calls too.
272
+
273
+ A tool Step finds its tool by name, so different tools in different agents
274
+ must have different names. Construction fails when two agents declare
275
+ different tools with one name; a toolset's tools are checked when they are
276
+ called. An `AgentTool` runs its whole agent inside one tool Step, with the
277
+ App's plugins but without Steps for its inner calls. A read `AgentTool` whose
278
+ agent has a tool that is not `read` fails the turn, because a read Step can
279
+ retry and repeat those writes; give such an `AgentTool` a write effect.
280
+
281
+ ## History and live events
282
+
283
+ Each `RunModel` rebuilds the ADK session from the stored messages: every
284
+ message's `framework_item` is one ADK `Event` as JSON, in the format ADK's own
285
+ database session services store. Session state is the replay of the events'
286
+ state deltas. A completed turn stores every event ADK appended, as if ADK had
287
+ run each tool call directly: it leaves out the `adk_request_confirmation`
288
+ calls, their answers, the placeholder responses, and content-less
289
+ resumability markers. The display fields project each event:
290
+
291
+ | Event | `role` | Tool fields |
292
+ | --- | --- | --- |
293
+ | User text | `user` | none |
294
+ | Model text or function calls | `assistant` | Set when the event has one function call. |
295
+ | Function responses | `tool` | Set when the event has one function response. |
296
+
297
+ With the default `RunConfig`, model text streams to `AgentEvents` as
298
+ `text_delta` events, and provider thought summaries as `reasoning_delta`
299
+ events. Control tools write `tool_call_started` and `tool_call_finished`; the
300
+ session writes them for every tool Step. Every event carries `turn_id`,
301
+ `step_execution_id`, and `attempt`; group live text by them as the
302
+ [core README](https://github.com/superdurable/dex/blob/main/agents/python/core/README.md) describes. Usage comes from each response's
303
+ usage metadata: `input_tokens` counts prompt and tool-use prompt tokens,
304
+ `output_tokens` counts candidate and thought tokens, and `model_calls` counts
305
+ model requests.
306
+
307
+ ## Testing
308
+
309
+ `ci.sh` installs the locked dependencies, runs ruff, the format check, and
310
+ mypy, and runs the integration tests against private `dexcli dev --engine zd`
311
+ engines. It reads `DEXCLI`. The tests use real ADK agents, driven by scripted
312
+ `BaseLlm` models in [`tests/`](https://github.com/superdurable/dex/tree/main/agents/python/google-adk/tests), so no model key is needed. They assert
313
+ each turn's Step history with `dex_agents.testing.read_step_history`. A Worker
314
+ child runs in its own process group, so a test can SIGKILL it during
315
+ `RunModel` or a tool Step.
316
+
317
+ ## Limits
318
+
319
+ - The App root must be a `BaseAgent`. ADK `Workflow` roots are not supported.
320
+ - A tool runs outside the ADK run, with a fresh `ToolContext` whose session
321
+ has no events and empty state. Its state and artifact changes are discarded.
322
+ Pass what it needs as arguments.
323
+ - A tool that asks for a confirmation or credential inside its Step fails with
324
+ `ToolExecutionError`.
325
+ - A turn fails when ADK pauses for something other than a tool call, such as a
326
+ credential request.
327
+ - Agents that continue without tools make their model calls in the same
328
+ `RunModel`, as described above.
329
+ - Agent `before_tool_callback`s run in the `RunModel` that requests the
330
+ call, before its Step, and again if that `RunModel` retries. When the turn
331
+ resumes, the plugin answers first, so they do not run then;
332
+ `after_tool_callback`s see both the placeholder and the recorded response,
333
+ as with ADK's own confirmations.
334
+ - Code executors other than `BuiltInCodeExecutor` run model-written code
335
+ inside `RunModel`, or inside an `AgentTool`'s Step, where a retry would run
336
+ it again, so construction fails. Run such code from a tool instead.
337
+ - A model step whose tool calls span several ADK branches fails the turn,
338
+ except for `LlmAgent` branches of a root `ParallelAgent`, as described above.
339
+ - An application plugin's `on_user_message_callback` may rewrite the message
340
+ ADK appends for the turn; the session still stores and replays the original
341
+ text.
342
+ - `app:` and `user:` state keys live in the Dex session, not across sessions.
343
+ - The adapter never closes the agent's toolsets or plugins; the application
344
+ owns them.
345
+ - The core's limits apply: history is never compacted by Dex, and each model
346
+ call and tool call adds a Step round trip.