agent-switch 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.
- agent_switch-0.1.0/.env.example +6 -0
- agent_switch-0.1.0/.gitignore +31 -0
- agent_switch-0.1.0/PKG-INFO +380 -0
- agent_switch-0.1.0/README.md +344 -0
- agent_switch-0.1.0/README.zh-CN.md +332 -0
- agent_switch-0.1.0/agent_core/__init__.py +80 -0
- agent_switch-0.1.0/agent_core/abc.py +27 -0
- agent_switch-0.1.0/agent_core/adapter_base.py +437 -0
- agent_switch-0.1.0/agent_core/backends/__init__.py +14 -0
- agent_switch-0.1.0/agent_core/backends/deepagents/__init__.py +7 -0
- agent_switch-0.1.0/agent_core/backends/deepagents/adapter.py +134 -0
- agent_switch-0.1.0/agent_core/backends/deepagents/hooks_middleware.py +403 -0
- agent_switch-0.1.0/agent_core/backends/deepagents/mapping.py +426 -0
- agent_switch-0.1.0/agent_core/backends/qcoder/__init__.py +7 -0
- agent_switch-0.1.0/agent_core/backends/qcoder/adapter.py +113 -0
- agent_switch-0.1.0/agent_core/backends/qcoder/hooks_bridge.py +201 -0
- agent_switch-0.1.0/agent_core/backends/qcoder/mapping.py +371 -0
- agent_switch-0.1.0/agent_core/backends/stub.py +52 -0
- agent_switch-0.1.0/agent_core/exceptions.py +54 -0
- agent_switch-0.1.0/agent_core/factory.py +27 -0
- agent_switch-0.1.0/agent_core/hooks/__init__.py +53 -0
- agent_switch-0.1.0/agent_core/hooks/base.py +78 -0
- agent_switch-0.1.0/agent_core/hooks/context.py +167 -0
- agent_switch-0.1.0/agent_core/hooks/dispatcher.py +123 -0
- agent_switch-0.1.0/agent_core/hooks/emitter.py +161 -0
- agent_switch-0.1.0/agent_core/hooks/enums.py +34 -0
- agent_switch-0.1.0/agent_core/hooks/result.py +41 -0
- agent_switch-0.1.0/agent_core/logging.py +194 -0
- agent_switch-0.1.0/agent_core/registry.py +43 -0
- agent_switch-0.1.0/agent_core/types/__init__.py +30 -0
- agent_switch-0.1.0/agent_core/types/config.py +87 -0
- agent_switch-0.1.0/agent_core/types/enums.py +21 -0
- agent_switch-0.1.0/agent_core/types/mcp.py +31 -0
- agent_switch-0.1.0/agent_core/types/message.py +76 -0
- agent_switch-0.1.0/agent_core/types/model.py +19 -0
- agent_switch-0.1.0/agent_core/types/response.py +38 -0
- agent_switch-0.1.0/agent_core/types/skill.py +14 -0
- agent_switch-0.1.0/agent_core/types/subagent.py +27 -0
- agent_switch-0.1.0/agent_core/types/tool.py +18 -0
- agent_switch-0.1.0/agent_core/utils/__init__.py +7 -0
- agent_switch-0.1.0/agent_core/utils/input.py +24 -0
- agent_switch-0.1.0/config/deepseek_flash.py +44 -0
- agent_switch-0.1.0/examples/__init__.py +11 -0
- agent_switch-0.1.0/examples/__main__.py +8 -0
- agent_switch-0.1.0/examples/basic_usage.py +55 -0
- agent_switch-0.1.0/examples/deepseek_flash_usage.py +40 -0
- agent_switch-0.1.0/examples/entry_demo.py +171 -0
- agent_switch-0.1.0/examples/hooks.py +154 -0
- agent_switch-0.1.0/main.py +16 -0
- agent_switch-0.1.0/pyproject.toml +75 -0
- agent_switch-0.1.0/tests/backends/test_deepagents_adapter.py +119 -0
- agent_switch-0.1.0/tests/backends/test_deepagents_hooks_middleware.py +244 -0
- agent_switch-0.1.0/tests/backends/test_deepagents_mapping.py +137 -0
- agent_switch-0.1.0/tests/backends/test_qcoder_adapter.py +100 -0
- agent_switch-0.1.0/tests/backends/test_qcoder_hooks_bridge.py +110 -0
- agent_switch-0.1.0/tests/backends/test_qcoder_mapping.py +199 -0
- agent_switch-0.1.0/tests/conftest.py +60 -0
- agent_switch-0.1.0/tests/hooks/test_adapter_hooks.py +126 -0
- agent_switch-0.1.0/tests/hooks/test_base_hooks.py +65 -0
- agent_switch-0.1.0/tests/hooks/test_context.py +38 -0
- agent_switch-0.1.0/tests/hooks/test_enums.py +37 -0
- agent_switch-0.1.0/tests/test_factory.py +61 -0
- agent_switch-0.1.0/tests/test_logging.py +122 -0
- agent_switch-0.1.0/tests/test_registry.py +48 -0
- agent_switch-0.1.0/tests/test_types.py +35 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.eggs/
|
|
8
|
+
|
|
9
|
+
# 虚拟环境
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
env/
|
|
13
|
+
|
|
14
|
+
# 环境变量(保留 .env.example)
|
|
15
|
+
.env
|
|
16
|
+
.env.*
|
|
17
|
+
!.env.example
|
|
18
|
+
|
|
19
|
+
# 测试 / 缓存
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
htmlcov/
|
|
25
|
+
|
|
26
|
+
# IDE
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
29
|
+
|
|
30
|
+
# 系统文件
|
|
31
|
+
.DS_Store
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: agent-switch
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified abstraction layer for agent SDKs (deepagents, Qcoder SDK, etc.)
|
|
5
|
+
Author: agent-switch contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: abstraction,agent,deepagents,llm,sdk
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: pydantic>=2.0
|
|
18
|
+
Provides-Extra: all
|
|
19
|
+
Requires-Dist: deepagents; extra == 'all'
|
|
20
|
+
Requires-Dist: qoder-agent-sdk; extra == 'all'
|
|
21
|
+
Provides-Extra: deepagents
|
|
22
|
+
Requires-Dist: deepagents; extra == 'deepagents'
|
|
23
|
+
Requires-Dist: langchain-deepseek>=0.1.0; extra == 'deepagents'
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: deepagents; extra == 'dev'
|
|
26
|
+
Requires-Dist: langchain-deepseek>=0.1.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: python-dotenv>=1.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: qoder-agent-sdk; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
33
|
+
Provides-Extra: qcoder
|
|
34
|
+
Requires-Dist: qoder-agent-sdk; extra == 'qcoder'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# agent-switch
|
|
38
|
+
|
|
39
|
+
**Unified abstraction layer for agent SDKs (deepagents, Qcoder SDK, etc.)**
|
|
40
|
+
|
|
41
|
+
`agent-switch` gives your business code a single, stable API — `create_agent` + `run` / `stream` —
|
|
42
|
+
so you can switch underlying agent frameworks (deepagents, qcoder, …) without touching
|
|
43
|
+
your upper-layer types or call sites.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **One API, many backends**: `create_agent(AgentBackend.DEEPAGENTS | "qcoder", config)` returns
|
|
48
|
+
an adapter exposing the same `run(input) -> AgentResponse` and `stream(input) -> AsyncIterator[AgentChunk]`.
|
|
49
|
+
- **Rich, validated type system**: `AgentConfig`, `AgentMessage`, `AgentTool`, `AgentSkillsConfig`,
|
|
50
|
+
`AgentMcpConfig`, `AgentSubagent`, `AgentChunk`, `AgentResponse`, … (Pydantic v2, `extra="forbid"`).
|
|
51
|
+
- **Hooks lifecycle**: 12 async hook events (`beforeAgent` … `afterStop`) with
|
|
52
|
+
`BLOCK` / `MODIFY` outcomes and a per-`AgentConfig` hook list.
|
|
53
|
+
- **Structured logging**: redacts secrets, summarizes config/input, Dev & JSON formatters,
|
|
54
|
+
configured only for the `agent_core` namespace (no global handlers).
|
|
55
|
+
- **Lazy dependencies**: `deepagents` is imported only when the `deepagents` backend is actually used;
|
|
56
|
+
`import agent_core` never requires it.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install agent-switch
|
|
62
|
+
# or with extras
|
|
63
|
+
pip install "agent-switch[deepagents]" # deepagents backend
|
|
64
|
+
pip install "agent-switch[qcoder]" # qcoder backend (qoder-agent-sdk)
|
|
65
|
+
pip install "agent-switch[all]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> The `qcoder` backend runs the real `qoder-agent-sdk`, which spawns the
|
|
69
|
+
> `qodercli` CLI. Install the CLI and log in once (`qodercli auth`) before use.
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from agent_core import AgentConfig, AgentMessage, MessageRole, create_agent, AgentBackend
|
|
75
|
+
|
|
76
|
+
# qcoder runs on the real qoder-agent-sdk (needs `qodercli` installed & logged in)
|
|
77
|
+
config = AgentConfig(system_prompt="Be concise.")
|
|
78
|
+
agent = create_agent(AgentBackend.QCODER, config)
|
|
79
|
+
|
|
80
|
+
response = agent.run("Tell me a joke")
|
|
81
|
+
print(response.content)
|
|
82
|
+
|
|
83
|
+
async def demo_stream() -> None:
|
|
84
|
+
async for chunk in agent.stream("Hello"):
|
|
85
|
+
print(chunk.delta_content, end="")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Run the demos
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python -m examples # unified API entry-point demo (all call styles)
|
|
92
|
+
python -m examples.basic_usage # DEEPAGENTS + QCODER sync run & QCODER stream
|
|
93
|
+
python -m examples.deepseek_flash_usage # DeepSeek Flash via env config (needs DEEPSEEK_API_KEY)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Reusable hook implementations live in `examples/hooks.py` (audit logging, rate
|
|
97
|
+
limiting, sensitive-word blocking, context injection) — import them in your own
|
|
98
|
+
entry point and pass them to `AgentConfig(hooks=[...])`.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## DeepAgents + `extra["model"]`
|
|
102
|
+
|
|
103
|
+
For the real `deepagents` backend, pass a pre-built LangChain `ChatModel` through
|
|
104
|
+
`AgentConfig.extra["model"]` — it takes priority over `AgentModel`:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from langchain_deepseek import ChatDeepSeek
|
|
108
|
+
from agent_core import AgentConfig, create_agent, AgentBackend
|
|
109
|
+
|
|
110
|
+
model = ChatDeepSeek(model="deepseek-v4-flash", api_key="sk-...")
|
|
111
|
+
config = AgentConfig(
|
|
112
|
+
system_prompt="You are a helpful assistant.",
|
|
113
|
+
tools=[AgentTool(name="search", handler=my_search_tool)],
|
|
114
|
+
extra={"model": model}, # ← pre-built ChatModel wins
|
|
115
|
+
)
|
|
116
|
+
agent = create_agent(AgentBackend.DEEPAGENTS, config)
|
|
117
|
+
response = agent.run("What is the weather in Paris?")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Alternatively let agent-switch build the model: `AgentModel(name=..., api_key=..., base_url=...)`
|
|
121
|
+
maps to `langchain.chat_models.init_chat_model`, while a bare `AgentModel(name="openai:gpt-4o-mini")`
|
|
122
|
+
passes the string straight through.
|
|
123
|
+
|
|
124
|
+
## Qcoder ↔ agent-switch mapping
|
|
125
|
+
|
|
126
|
+
The `qcoder` backend runs on the real `qoder-agent-sdk` (which drives the
|
|
127
|
+
`qodercli` CLI). It supports message format normalization, the unified hooks
|
|
128
|
+
lifecycle, streaming, tools / skills / MCP configuration, and session identity.
|
|
129
|
+
|
|
130
|
+
### Message normalization
|
|
131
|
+
|
|
132
|
+
Input direction (`AgentMessage` → qoder CLI wire format, via
|
|
133
|
+
`agent_core.backends.qcoder.mapping.agent_messages_to_qoder_wire`):
|
|
134
|
+
|
|
135
|
+
| agent-switch | qoder wire |
|
|
136
|
+
| ------------------------- | ------------------------------------------------------------- |
|
|
137
|
+
| `MessageRole.USER` | `{"type":"user","message":{"role":"user","content":<str>}}` |
|
|
138
|
+
| `MessageRole.ASSISTANT` | text + `tool_use` blocks (`{"type":"tool_use","id","name","input"}`) in one `user` message |
|
|
139
|
+
| `MessageRole.TOOL` | `{"type":"tool_result","tool_use_id","content","is_error"}` block |
|
|
140
|
+
| `MessageRole.SYSTEM` | not sent (mapped to `QoderAgentOptions.system_prompt`) |
|
|
141
|
+
| `thinking` / `meta` | not sent (input direction) |
|
|
142
|
+
|
|
143
|
+
Output direction (qoder SDK `Message` → `AgentMessage`):
|
|
144
|
+
|
|
145
|
+
| qoder SDK | agent-switch |
|
|
146
|
+
| ------------------------- | --------------------------------------- |
|
|
147
|
+
| `AssistantMessage` | `role=assistant`, `content` (joined `TextBlock`s) |
|
|
148
|
+
| `ThinkingBlock` | `thinking` |
|
|
149
|
+
| `ToolUseBlock` | `ToolCall(id, name, input→arguments)` |
|
|
150
|
+
| `UserMessage` | `role=user` |
|
|
151
|
+
| `SystemMessage` | `role=system` (only `meta`) |
|
|
152
|
+
| `ResultMessage` | terminal → `AgentResponse` (content, raw, backend) |
|
|
153
|
+
|
|
154
|
+
### Hooks mapping
|
|
155
|
+
|
|
156
|
+
Session-level events (`beforeAgent` / `beforePrompt` / `beforeLLM` / `afterLLM` /
|
|
157
|
+
`afterAgent` / `afterStop`) fire at the adapter level once per `run` / `stream`,
|
|
158
|
+
exactly as documented in the Hooks chapter. Call-level events are bridged into
|
|
159
|
+
the Qoder SDK native hook system:
|
|
160
|
+
|
|
161
|
+
| agent-switch hook | Qoder HookEvent | BLOCK / MODIFY mapping |
|
|
162
|
+
| -------------------- | -------------------- | --------------------------------------------------- |
|
|
163
|
+
| `beforeTool` | `PreToolUse` | BLOCK → `continue_:False, decision:"block"` + `permissionDecision:"deny"`; MODIFY(`updated_input`) → `updatedInput` |
|
|
164
|
+
| `afterTool` | `PostToolUse` | MODIFY(`updated_tool_output`) → `updatedToolOutput` |
|
|
165
|
+
| `afterToolError` | `PostToolUseFailure` | notification only |
|
|
166
|
+
| `beforePermission` | `PermissionRequest` | BLOCK → `permissionDecision:"deny"` |
|
|
167
|
+
| `beforeSubagent` | `SubagentStart` | notification only |
|
|
168
|
+
| `afterSubagent` | `SubagentStop` | notification only |
|
|
169
|
+
|
|
170
|
+
Only events whose hook class actually overrides the method are registered, so an
|
|
171
|
+
empty hooks list adds no callbacks to the CLI.
|
|
172
|
+
|
|
173
|
+
### Configuration mapping (`AgentConfig` → `QoderAgentOptions`)
|
|
174
|
+
|
|
175
|
+
| agent-switch | QoderAgentOptions |
|
|
176
|
+
| ----------------------- | ---------------------------------------------------- |
|
|
177
|
+
| `AgentModel.name` / `extra["model"]` (str) | `model` |
|
|
178
|
+
| `system_prompt` | `system_prompt` |
|
|
179
|
+
| `tools` (with `handler`) | in-process SDK MCP server via `create_sdk_mcp_server` + `allowed_tools` |
|
|
180
|
+
| `skills` | `skills` (`sources` list / `enable_all` → `"all"`) |
|
|
181
|
+
| `mcp` (`AgentMcpConfig`) | `mcp_servers` (stdio / http) + `allowed_mcp_server_names` |
|
|
182
|
+
| `extra` whitelist | `permission_mode, max_turns, session_id, cwd, auth, allowed_tools, disallowed_tools, can_use_tool, include_partial_messages, continue_conversation, resume, settings, agents, agent, user, env, cli_path` |
|
|
183
|
+
| default | `auth=qodercli_auth()` (reuse local login state) |
|
|
184
|
+
|
|
185
|
+
### Streaming
|
|
186
|
+
|
|
187
|
+
`stream()` iterates `qoder_agent_sdk.query(prompt=wire_messages, options=...)`;
|
|
188
|
+
each SDK `AssistantMessage` is mapped to one or more `AgentChunk`
|
|
189
|
+
(`delta_thinking` / `delta_content` / `delta_tool_call`), and the stream always
|
|
190
|
+
ends with a chunk carrying `is_finish=True`. `run()` wraps the same async flow
|
|
191
|
+
with `asyncio.run` and returns the terminal `ResultMessage` as `AgentResponse`
|
|
192
|
+
(falling back to the accumulated assistant text if no result message arrives).
|
|
193
|
+
Token-level partial messages (`StreamEvent`) are not enabled by default.
|
|
194
|
+
|
|
195
|
+
### Runtime requirements
|
|
196
|
+
|
|
197
|
+
- `pip install "agent-switch[qcoder]"` (pulls `qoder-agent-sdk`, `mcp`, `anyio`)
|
|
198
|
+
- Install the `qodercli` CLI and log in once (`qodercli auth`)
|
|
199
|
+
- Sync `run()` uses `asyncio.run` internally: calling it inside a running event
|
|
200
|
+
loop raises `RuntimeError` — use `stream()` in async code.
|
|
201
|
+
|
|
202
|
+
## Hooks
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from agent_core import (
|
|
206
|
+
AgentConfig, AgentHookEvent, BaseAgentHooks, HookOutcome, HookResult, create_agent,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
class AuditHooks(BaseAgentHooks):
|
|
210
|
+
async def before_llm(self, context) -> None:
|
|
211
|
+
print(f"[audit] beforeLLM model={context.model}")
|
|
212
|
+
|
|
213
|
+
class RateLimitHooks(BaseAgentHooks):
|
|
214
|
+
async def before_prompt(self, context):
|
|
215
|
+
if len(context.messages) > 10:
|
|
216
|
+
return HookResult(outcome=HookOutcome.BLOCK, reason="rate limit exceeded")
|
|
217
|
+
|
|
218
|
+
# single instance or a list — both are normalized
|
|
219
|
+
config = AgentConfig(hooks=[AuditHooks(), RateLimitHooks()])
|
|
220
|
+
# or: AgentConfig(hooks=AuditHooks())
|
|
221
|
+
agent = create_agent(AgentBackend.QCODER, config)
|
|
222
|
+
response = agent.run("hello")
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Run/stream trigger six lifecycle events in order:
|
|
226
|
+
`beforeAgent → beforePrompt → beforeLLM → [SDK] → afterLLM → afterAgent → afterStop`.
|
|
227
|
+
Hooks may return `HookResult(outcome=BLOCK, reason=...)` (raises `HookBlockedError`)
|
|
228
|
+
or `HookResult(outcome=MODIFY, data={"messages": [...]})` (replaces the prompt messages).
|
|
229
|
+
|
|
230
|
+
Hooks fire on two layers:
|
|
231
|
+
|
|
232
|
+
- **Agent level** (once per agent execution): `beforeAgent`, `beforePrompt`
|
|
233
|
+
and `afterAgent` — for the `deepagents` backend these fire inside the SDK,
|
|
234
|
+
on the graph's `before_agent` / `after_agent` entry/exit nodes.
|
|
235
|
+
- **Call level** (once per LLM / tool call inside the agent loop): `beforeLLM`,
|
|
236
|
+
`afterLLM`, `beforeTool`, `afterTool`, `afterToolError` — bridged through an
|
|
237
|
+
injected `AgentHooksMiddleware` (`wrap_model_call` / `wrap_tool_call`), so they
|
|
238
|
+
fire at the real model/tool call points (e.g. several times when the agent loops
|
|
239
|
+
over tools).
|
|
240
|
+
- `afterStop` (reason `complete` / `error`) is fired by the adapter at the
|
|
241
|
+
`run` / `stream` boundary — `after_agent` only runs on the success path, so the
|
|
242
|
+
adapter re-fires `afterAgent(error)` + `afterStop(error)` when the run fails.
|
|
243
|
+
|
|
244
|
+
For the `qcoder` backend, the six session-level events fire at the adapter level
|
|
245
|
+
(one per `run` / `stream`), while `beforeTool` / `afterTool` / `afterToolError` /
|
|
246
|
+
`beforePermission` / `beforeSubagent` / `afterSubagent` are bridged to the Qoder
|
|
247
|
+
SDK's native hooks (`PreToolUse` / `PostToolUse` / `PostToolUseFailure` /
|
|
248
|
+
`PermissionRequest` / `SubagentStart` / `SubagentStop`) and fire inside the CLI.
|
|
249
|
+
`beforePermission / beforeSubagent / afterSubagent` are declared but not yet
|
|
250
|
+
bridged for the `deepagents` backend.
|
|
251
|
+
|
|
252
|
+
### Hooks ↔ deepagents implementation mapping
|
|
253
|
+
|
|
254
|
+
| agent-switch hook | deepagents implementation | Level / timing |
|
|
255
|
+
| ------------------------ | ---------------------------------------------------------------- | ----------------------------------------------------- |
|
|
256
|
+
| `beforeAgent` | `AgentHooksMiddleware.before_agent` / `abefore_agent` (entry node) | agent, once per agent execution |
|
|
257
|
+
| `beforePrompt` | `AgentHooksMiddleware.before_agent` / `abefore_agent` (entry node) | agent, once per agent execution |
|
|
258
|
+
| `beforeLLM` | `AgentHooksMiddleware.wrap_model_call` / `awrap_model_call`, before `handler(request)` | call, once per LLM call |
|
|
259
|
+
| `afterLLM` | `AgentHooksMiddleware.wrap_model_call` / `awrap_model_call`, after `handler(request)` | call, once per LLM call |
|
|
260
|
+
| `beforeTool` | `AgentHooksMiddleware.wrap_tool_call` / `awrap_tool_call`, before executing the tool | call, once per tool call |
|
|
261
|
+
| `afterTool` | `AgentHooksMiddleware.wrap_tool_call` / `awrap_tool_call`, after the tool returned | call, once per tool call |
|
|
262
|
+
| `afterToolError` | `AgentHooksMiddleware.wrap_tool_call` exception branch, then re-raise | call, once per failed tool call |
|
|
263
|
+
| `afterAgent` | `AgentHooksMiddleware.after_agent` / `aafter_agent` (exit node); adapter re-fires `afterAgent(error)` on failure | agent, once per successful execution |
|
|
264
|
+
| `afterStop` | adapter (`_finalize_run_success_*` / `_finalize_run_error_*`, reason `complete` / `error`) | agent, once per `run` / `stream` |
|
|
265
|
+
| `beforePermission` / `beforeSubagent` / `afterSubagent` | not bridged yet | — |
|
|
266
|
+
|
|
267
|
+
Implementation details for the `deepagents` backend:
|
|
268
|
+
|
|
269
|
+
- `DeepAgentsAdapter._build_agent()` appends an `AgentHooksMiddleware` instance to
|
|
270
|
+
`create_deep_agent(middleware=[...])` whenever `AgentConfig.hooks` is non-empty;
|
|
271
|
+
it coexists with user middleware passed via `config.extra["middleware"]`.
|
|
272
|
+
- The middleware reads the current session ids through a `session_provider` closure
|
|
273
|
+
(bound to the adapter's `_session_id` / `_correlation_id`), so all contexts share
|
|
274
|
+
the same session (session_id / correlation_id) as the adapter-level ones.
|
|
275
|
+
- `before_agent` / `after_agent` are the graph's entry / exit nodes: each fires
|
|
276
|
+
exactly once per agent execution (sub-agents are separately compiled graphs and
|
|
277
|
+
do not trigger them). `beforePrompt` returning `MODIFY` rewrites the initial
|
|
278
|
+
state via `{"messages": [...]}`; `BLOCK` raises `HookBlockedError` inside the
|
|
279
|
+
SDK, aborting the whole run.
|
|
280
|
+
- `beforeLLM` returning `MODIFY` rewrites the real request via
|
|
281
|
+
`request.override(messages=...)`.
|
|
282
|
+
- Because these events fire inside the SDK, `DeepAgentsAdapter` sets
|
|
283
|
+
`call_hooks_via_middleware = True` and `agent_hooks_via_middleware = True` so
|
|
284
|
+
the adapter layer does not fire them a second time; `afterStop` (and the error
|
|
285
|
+
path) remain at the adapter, since `after_agent` never runs when the graph raises.
|
|
286
|
+
- The built graph is cached; the cache key includes a fingerprint of the configured
|
|
287
|
+
hooks, so changing hooks rebuilds the agent instead of reusing a stale graph.
|
|
288
|
+
|
|
289
|
+
## Streaming
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
async for chunk in agent.stream("hello"):
|
|
293
|
+
if chunk.delta_content:
|
|
294
|
+
print(chunk.delta_content, end="")
|
|
295
|
+
if chunk.delta_thinking:
|
|
296
|
+
print(f"\n[thinking] {chunk.delta_thinking}")
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`AgentChunk` fields: `delta_content`, `delta_thinking`, `delta_tool_call`, `is_finish`, `meta`.
|
|
300
|
+
The stream always ends with a chunk carrying `is_finish=True`.
|
|
301
|
+
|
|
302
|
+
## Message model
|
|
303
|
+
|
|
304
|
+
| Field | Type | Notes |
|
|
305
|
+
| ------------ | ---------------------- | -------------------------------------------- |
|
|
306
|
+
| `role` | `MessageRole` | `user` / `assistant` / `system` / `tool` |
|
|
307
|
+
| `content` | `str` | text content |
|
|
308
|
+
| `thinking` | `str \| None` | reasoning content (model dependent) |
|
|
309
|
+
| `tool_calls` | `list[ToolCall]` | `{id, name, arguments}` |
|
|
310
|
+
| `tool_result`| `ToolResult \| None` | `{tool_call_id, content}` |
|
|
311
|
+
| `meta` | `dict` | backend metadata (`langchain_type`, …) |
|
|
312
|
+
| `_raw` | `PrivateAttr` | adapter debugging only — never serialized |
|
|
313
|
+
|
|
314
|
+
## DeepAgents ↔ agent-switch mapping
|
|
315
|
+
|
|
316
|
+
| agent-switch | deepagents / LangChain |
|
|
317
|
+
| ---------------------- | --------------------------------------------------------- |
|
|
318
|
+
| `MessageRole.USER` | `HumanMessage` |
|
|
319
|
+
| `MessageRole.SYSTEM` | `SystemMessage` |
|
|
320
|
+
| `MessageRole.ASSISTANT`| `AIMessage` (with `tool_calls: [{id, name, args}]`) |
|
|
321
|
+
| `MessageRole.TOOL` | `ToolMessage` (`tool_call_id`) |
|
|
322
|
+
| `AgentMessage.thinking`| extracted with priority: `additional_kwargs.reasoning_content` → `additional_kwargs.thinking` → `content_blocks` of type `reasoning` / `thinking` |
|
|
323
|
+
| `AgentTool.handler` | deepagents `tools` (or resolved via `extra["tools"]`) |
|
|
324
|
+
| `AgentSkillsConfig.sources` | deepagents `skills` |
|
|
325
|
+
| `AgentSubagent` | deepagents `subagents` dicts |
|
|
326
|
+
| `AgentConfig.extra` | whitelisted passthrough: `middleware, memory, permissions, backend, interrupt_on, response_format, state_schema, context_schema, checkpointer, store, debug, name, cache` |
|
|
327
|
+
| `AgentResponse.raw` | raw graph `invoke` / `astream` result |
|
|
328
|
+
| streaming chunks | `graph.astream(stream_mode="messages")` → one LangChain chunk may produce several `AgentChunk`s (`delta_content` / `delta_thinking` / `delta_tool_call`) |
|
|
329
|
+
|
|
330
|
+
`thinking` / `meta` are **not** sent to the backend (input direction); they are only
|
|
331
|
+
extracted on the way back.
|
|
332
|
+
|
|
333
|
+
## Current status
|
|
334
|
+
|
|
335
|
+
- [x] Type system & unified API (`create_agent` / `run` / `stream`)
|
|
336
|
+
- [x] `deepagents` backend (real implementation, lazy import)
|
|
337
|
+
- [x] `qcoder` backend (real implementation on `qoder-agent-sdk`: message normalization, hooks bridging, streaming, tools / skills / MCP)
|
|
338
|
+
- [x] Hooks lifecycle (12 events, BLOCK / MODIFY)
|
|
339
|
+
- [x] deepagents call-level hook bridging via `AgentHooksMiddleware` (`beforeLLM` / `afterLLM` / `beforeTool` / `afterTool` / `afterToolError`)
|
|
340
|
+
- [x] qcoder call-level hook bridging via Qoder native hooks (`PreToolUse` / `PostToolUse` / `PostToolUseFailure` / `PermissionRequest` / `SubagentStart` / `SubagentStop`)
|
|
341
|
+
- [x] Structured logging (redaction, Dev / JSON formatters)
|
|
342
|
+
- [ ] `beforePermission` / `beforeSubagent` / `afterSubagent` bridging for the `deepagents` backend
|
|
343
|
+
- [ ] Token-level partial message streaming for `qcoder` (`StreamEvent`)
|
|
344
|
+
- [ ] Qoder `QoderSDKClient` bidirectional / interrupt support
|
|
345
|
+
|
|
346
|
+
## Roadmap
|
|
347
|
+
|
|
348
|
+
1. Bridge the remaining `beforePermission` / `beforeSubagent` / `afterSubagent` events for `deepagents`.
|
|
349
|
+
2. Add per-backend capability introspection (`supports_*` flags).
|
|
350
|
+
3. Officially type the public API against the dev extras' SDK versions.
|
|
351
|
+
|
|
352
|
+
## Development
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
python -m venv .venv && source .venv/bin/activate
|
|
356
|
+
pip install -e ".[dev]"
|
|
357
|
+
|
|
358
|
+
pytest -q # 45 passed
|
|
359
|
+
ruff check . # lint
|
|
360
|
+
mypy -p agent_core # strict type check
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Layout:
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
src/agent_core/
|
|
367
|
+
├── abc.py # AgentAdapter (abstract)
|
|
368
|
+
├── adapter_base.py # hooks lifecycle orchestration
|
|
369
|
+
├── factory.py # create_agent
|
|
370
|
+
├── registry.py # BackendRegistry
|
|
371
|
+
├── logging.py # configure_logging / summarize / formatters
|
|
372
|
+
├── hooks/ # enums, context, result, dispatcher, emitter, base
|
|
373
|
+
├── types/ # unified type system
|
|
374
|
+
├── utils/ # input normalization
|
|
375
|
+
└── backends/ # stub, qcoder, deepagents (adapter + mapping + hooks bridge)
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## License
|
|
379
|
+
|
|
380
|
+
MIT
|