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.
Files changed (65) hide show
  1. agent_switch-0.1.0/.env.example +6 -0
  2. agent_switch-0.1.0/.gitignore +31 -0
  3. agent_switch-0.1.0/PKG-INFO +380 -0
  4. agent_switch-0.1.0/README.md +344 -0
  5. agent_switch-0.1.0/README.zh-CN.md +332 -0
  6. agent_switch-0.1.0/agent_core/__init__.py +80 -0
  7. agent_switch-0.1.0/agent_core/abc.py +27 -0
  8. agent_switch-0.1.0/agent_core/adapter_base.py +437 -0
  9. agent_switch-0.1.0/agent_core/backends/__init__.py +14 -0
  10. agent_switch-0.1.0/agent_core/backends/deepagents/__init__.py +7 -0
  11. agent_switch-0.1.0/agent_core/backends/deepagents/adapter.py +134 -0
  12. agent_switch-0.1.0/agent_core/backends/deepagents/hooks_middleware.py +403 -0
  13. agent_switch-0.1.0/agent_core/backends/deepagents/mapping.py +426 -0
  14. agent_switch-0.1.0/agent_core/backends/qcoder/__init__.py +7 -0
  15. agent_switch-0.1.0/agent_core/backends/qcoder/adapter.py +113 -0
  16. agent_switch-0.1.0/agent_core/backends/qcoder/hooks_bridge.py +201 -0
  17. agent_switch-0.1.0/agent_core/backends/qcoder/mapping.py +371 -0
  18. agent_switch-0.1.0/agent_core/backends/stub.py +52 -0
  19. agent_switch-0.1.0/agent_core/exceptions.py +54 -0
  20. agent_switch-0.1.0/agent_core/factory.py +27 -0
  21. agent_switch-0.1.0/agent_core/hooks/__init__.py +53 -0
  22. agent_switch-0.1.0/agent_core/hooks/base.py +78 -0
  23. agent_switch-0.1.0/agent_core/hooks/context.py +167 -0
  24. agent_switch-0.1.0/agent_core/hooks/dispatcher.py +123 -0
  25. agent_switch-0.1.0/agent_core/hooks/emitter.py +161 -0
  26. agent_switch-0.1.0/agent_core/hooks/enums.py +34 -0
  27. agent_switch-0.1.0/agent_core/hooks/result.py +41 -0
  28. agent_switch-0.1.0/agent_core/logging.py +194 -0
  29. agent_switch-0.1.0/agent_core/registry.py +43 -0
  30. agent_switch-0.1.0/agent_core/types/__init__.py +30 -0
  31. agent_switch-0.1.0/agent_core/types/config.py +87 -0
  32. agent_switch-0.1.0/agent_core/types/enums.py +21 -0
  33. agent_switch-0.1.0/agent_core/types/mcp.py +31 -0
  34. agent_switch-0.1.0/agent_core/types/message.py +76 -0
  35. agent_switch-0.1.0/agent_core/types/model.py +19 -0
  36. agent_switch-0.1.0/agent_core/types/response.py +38 -0
  37. agent_switch-0.1.0/agent_core/types/skill.py +14 -0
  38. agent_switch-0.1.0/agent_core/types/subagent.py +27 -0
  39. agent_switch-0.1.0/agent_core/types/tool.py +18 -0
  40. agent_switch-0.1.0/agent_core/utils/__init__.py +7 -0
  41. agent_switch-0.1.0/agent_core/utils/input.py +24 -0
  42. agent_switch-0.1.0/config/deepseek_flash.py +44 -0
  43. agent_switch-0.1.0/examples/__init__.py +11 -0
  44. agent_switch-0.1.0/examples/__main__.py +8 -0
  45. agent_switch-0.1.0/examples/basic_usage.py +55 -0
  46. agent_switch-0.1.0/examples/deepseek_flash_usage.py +40 -0
  47. agent_switch-0.1.0/examples/entry_demo.py +171 -0
  48. agent_switch-0.1.0/examples/hooks.py +154 -0
  49. agent_switch-0.1.0/main.py +16 -0
  50. agent_switch-0.1.0/pyproject.toml +75 -0
  51. agent_switch-0.1.0/tests/backends/test_deepagents_adapter.py +119 -0
  52. agent_switch-0.1.0/tests/backends/test_deepagents_hooks_middleware.py +244 -0
  53. agent_switch-0.1.0/tests/backends/test_deepagents_mapping.py +137 -0
  54. agent_switch-0.1.0/tests/backends/test_qcoder_adapter.py +100 -0
  55. agent_switch-0.1.0/tests/backends/test_qcoder_hooks_bridge.py +110 -0
  56. agent_switch-0.1.0/tests/backends/test_qcoder_mapping.py +199 -0
  57. agent_switch-0.1.0/tests/conftest.py +60 -0
  58. agent_switch-0.1.0/tests/hooks/test_adapter_hooks.py +126 -0
  59. agent_switch-0.1.0/tests/hooks/test_base_hooks.py +65 -0
  60. agent_switch-0.1.0/tests/hooks/test_context.py +38 -0
  61. agent_switch-0.1.0/tests/hooks/test_enums.py +37 -0
  62. agent_switch-0.1.0/tests/test_factory.py +61 -0
  63. agent_switch-0.1.0/tests/test_logging.py +122 -0
  64. agent_switch-0.1.0/tests/test_registry.py +48 -0
  65. agent_switch-0.1.0/tests/test_types.py +35 -0
@@ -0,0 +1,6 @@
1
+ # DeepSeek Flash 环境变量示例
2
+ # 复制为 .env 并填入真实值:cp .env.example .env
3
+
4
+ DEEPSEEK_API_KEY=REPLACE_WITH_YOUR_DEEPSEEK_API_KEY
5
+ DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
6
+ DEEPSEEK_MODEL=deepseek-v4-flash
@@ -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