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.
- dex_agents_google_adk-0.1.0/LICENSE +105 -0
- dex_agents_google_adk-0.1.0/PKG-INFO +346 -0
- dex_agents_google_adk-0.1.0/README.md +325 -0
- dex_agents_google_adk-0.1.0/pyproject.toml +98 -0
- dex_agents_google_adk-0.1.0/pyproject.toml.orig +79 -0
- dex_agents_google_adk-0.1.0/src/dex_agents_google_adk/__init__.py +35 -0
- dex_agents_google_adk-0.1.0/src/dex_agents_google_adk/_history.py +207 -0
- dex_agents_google_adk-0.1.0/src/dex_agents_google_adk/_run_state.py +70 -0
- dex_agents_google_adk-0.1.0/src/dex_agents_google_adk/_session_service.py +79 -0
- dex_agents_google_adk-0.1.0/src/dex_agents_google_adk/_tool_deferral.py +449 -0
- dex_agents_google_adk-0.1.0/src/dex_agents_google_adk/py.typed +0 -0
- dex_agents_google_adk-0.1.0/src/dex_agents_google_adk/runner.py +709 -0
- dex_agents_google_adk-0.1.0/src/dex_agents_google_adk/tools.py +51 -0
|
@@ -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.
|