Deep Agents Code(dcode)架构解析:Textual 终端客户端 + 回环 LangGraph 服务器 + ACP 双运行时设计
【免费下载链接】deepagentsThe batteries-included agent harness.项目地址: https://gitcode.com/GitHub_Trending/de/deepagents
deepagents-code(命令名dcode)是构建在deepagentsSDK 之上的参考型终端编码 Agent 产品,它把 SDK 的 Agent 编排能力与终端体验、持久化会话、工具、技能和可选沙箱执行整合在一起。本文基于 openwiki/architecture/code-agent.md 及其引用的源码,完整拆解 dcode 的"普通客户端-服务器"与"ACP stdio"两条刻意分离的运行时路径,说明启动装配、配置交接、工作区绑定、图构建与清理机制。读完你将掌握 dcode 进程模型的边界划分、DEEPAGENTS_CODE_SERVER_*环境变量协议、持久化工作区绑定的校验逻辑,以及 ACP 模式下为什么"不启动 langgraph dev、不使用 RemoteAgent"。
一个产品,两条刻意分离的运行时路径
dcode 是deepagentsSDK 的"batteries-included"参考实现:SDK 提供 Agent harness,dcode 展示如何把 harness 与终端体验、持久化、工具、技能和可选沙箱组合成一个可用的编码 Agent 产品。仓库中的 ARCHITECTURE.md 给出了大图:产品被切成"终端客户端"和"Agent 服务器"两个运行半区——客户端负责呈现与输入收集,服务器负责运行编码 Agent 图、连接模型、工具、内存、技能和 backend。
与"一个大进程里什么都干"的方案不同,dcode 在进程模型上就有两条有意分开的路径(见 code-agent.md):
- 普通交互模式与 headless(非交互)模式:运行一个终端客户端 + 一个由客户端自己拥有的本地
langgraph dev服务器进程。客户端拥有呈现、输入与审批(approval);服务器拥有模型、图、工具、内存、技能、backend 与 checkpoint。 dcode --acp模式:在 stdio 上运行一个进程内 ACP 服务器。它构建本地会话图,既不启动langgraph dev,也不使用RemoteAgent。
关键判断在于:这不是一种可以互换的传输层,而是一条所有权边界(ownership boundary)。对普通服务器路径的改动,必须独立地针对 ACP 路径重新评估;两条路径共享底层 SDK harness,但进程拓扑、配置输入、checkpointer 生命周期完全不同。
普通客户端-服务器运行路径
交互与 headless 的同一套服务端运行时
交互模式使用 Textual 应用负责渲染与用户交互;headless 模式复用同一个服务器运行时和同一个RemoteAgent,只为单个用户任务服务——它把 UI 替换为 stdout 流式输出,--quiet会抑制工具与文件操作提示,让 stdout 只包含响应文本(见 code-agent.md 与 non_interactive.py 对应的行为)。
一次请求在两种模式下遵循相同形状(ARCHITECTURE.md):客户端接收用户输入 → 发送给 Agent 服务器 → 服务器运行 Agent 并流式返回事件 → 客户端渲染事件并收集需要的人工响应 → 会话状态被持久化以便后续继续。
完整时序
普通本地路径的完整流程如下(该图仅描述普通路径,ACP 单独描述;执行前的"工作区绑定"让服务器端图选择具备权威性):
启动装配:项目上下文、临时工作区与配置交接
start_server_and_get_agent 的启动顺序
server_manager.py 中的start_server_and_get_agent是普通路径的启动入口,它依次完成:
- 捕获项目上下文:用显式
cwd(ProjectContext.from_user_cwd(Path(cwd)))或捕获当前项目上下文(_capture_project_context()); - 预检显式 MCP 配置:在 spawn 任何子进程之前调用
_preflight_validate_mcp_config,校验--mcp-config路径的合法性; - 解析
ServerConfig:通过ServerConfig.from_cli_args(...)把所有 CLI 参数归一化为一个类型化配置; - 脚手架临时 LangGraph 工作区:
tempfile.mkdtemp(prefix="deepagents_server_")创建临时目录,_scaffold_workspace在其中生成pyproject.toml、langgraph.json和一个生成的 checkpointer 模块; - 启动服务器:
ServerProcess(host, port, ...)+server.start()+server.wait_for_graph_ready("agent")等待图就绪; - 返回已配置的 RemoteAgent:创建
RemoteAgent(url, graph_name="agent"),调用agent.set_workspace(cwd, session_workspace_claim, config_fingerprint)后返回(agent, server, None)。
临时工作区里发生了什么
脚手架生成的 checkpointer 模块有一个精妙之处(见 code-agent.md 的 "Startup, generated workspace, and configuration handoff"):它从环境变量读取应用会话数据库路径并产生AsyncSqliteSaver,而不是把路径硬编码进生成的源码。这保证了临时目录里的生成文件不携带任何机器相关的绝对路径。
生成的图引用是deepagents_code.server_graph:make_graph(见 server_graph.py)。当使用这个内置引用时,langgraph.json还会追加 dcode 的offload HTTP 应用并启用自定义路由鉴权;而自定义图引用(custom graph reference)不会提供/offload服务——这也是客户端在aoffload中把 404 明确报告为"This server does not provide dcode's /offload operation"的原因(见 remote_client.py)。
本地启动默认绑定127.0.0.1和端口0,由操作系统挑选临时端口(ephemeral port),而不是占用langgraph dev惯例的 2024 端口(server_manager.py中port: int = _EPHEMERAL_PORT)。
DEEPAGENTS_CODE_SERVER_*:一份共享的配置模式
客户端通过DEEPAGENTS_CODE_SERVER_*环境变量把解析后的启动配置导出给服务器子进程。ServerConfig 是一个 frozen dataclass,其文档字符串明确了设计意图:"应用 spawn 一个langgraph dev子进程,并通过带DEEPAGENTS_CODE_SERVER_前缀的环境变量传递配置;这个模块提供双方共享的单一ServerConfig,变量集合、序列化格式和默认值都只在一处定义。应用用to_env()写入,服务器图用from_env()读回。"
to_env()与from_env()是客户端/服务器共享的同一套 schema(to_env、from_env),序列化、默认值与变量集合在一处维护。核心字段包括:
| 环境变量后缀 | 含义 | 默认值 |
|---|---|---|
MODEL | 模型规格串(如'anthropic:claude-opus-4-7'),None由服务器选默认 | None |
SUMMARIZATION_MODEL | 仅用于上下文压缩摘要的模型,None复用主模型 | None |
MODEL_PARAMS | 传给 chat model 构造器的额外 kwargs(temperature、max_tokens 等),JSON 编码 | None |
MAX_RETRIES | 显式--max-retries | None |
ASSISTANT_ID | 服务器上调用的 Agent 图标识 | DEFAULT_AGENT_NAME |
SYSTEM_PROMPT | 系统提示词覆盖,None用默认 | None |
AUTO_APPROVE | 是否自动批准所有工具调用 | false |
INTERRUPT_SHELL_ONLY | 仅 shell 工具走 HITL,其余走中间件校验 | false |
SHELL_ALLOW_LIST | shell 命令白名单(逗号分隔),None禁用白名单 | None |
INTERACTIVE | 是否交互会话 | true |
ENABLE_SHELL/ENABLE_ASK_USER/ENABLE_MEMORY/ENABLE_SKILLS | 各子系统开关 | true/false/true/true |
ENABLE_INTERPRETER | 是否启用CodeInterpreterMiddleware(js_eval),本地模式专属 | false |
INTERPRETER_PTC | interpreter.ptc的调用级覆盖:"safe"/"all"/工具名列表 | None |
ALLOW_FS_TOOLS | FilesystemMiddleware的文件系统工具白名单(JSON 数组),必须包含"read_file" | None(全部) |
RUBRIC_MODEL/RUBRIC_MAX_ITERATIONS | 评分中间件的 grader 模型与迭代数 | None |
AUTO_CLASSIFIER_MODEL | Auto 模式的分类器模型 | None(回退链) |
RECURSION_LIMIT | 主 Agent 的 LangGraphrecursion_limit(图步骤预算) | None(由运行时配置解析) |
SANDBOX_TYPE/SANDBOX_ID/SANDBOX_SNAPSHOT_NAME/SANDBOX_SETUP | 沙箱后端、复用 ID、快照/蓝图名、setup 脚本 | None |
CWD/PROJECT_ROOT | 用户的原始工作目录与检测到的项目根 | None |
MCP_CONFIG_PATH/NO_MCP/TRUST_PROJECT_MCP | MCP 配置路径与信任控制 | None/false/None |
TRUST_PROJECT_EXTENSIONS/EXTENSION_PATHS | 项目 Python 扩展信任与单次运行扩展 | false/[] |
有几个值得展开的细节:
- 布尔值约定:
_read_env_bool使用'true'/'false'(大小写不敏感),缺失回退默认值; - JSON 读取失败即关闭(fail closed):
_read_env_json对格式错误的 JSON 直接抛ValueError; ALLOW_FS_TOOLS的安全语义:_read_env_allow_fs_tools在服务器子进程内执行——该变量是安全控制,任何无法识别的形状([]、未知工具名)都必须抛错关闭而非退化为"不受限文件系统";ServerConfig.__post_init__还强制要求列表非空且包含"read_file"(post_init);- 值校验:
shell_allow_list非空、rubric_max_iterations/recursion_limit必须为正整数、不能是布尔值,这些不变量都在__post_init__集中维护。
配置解析层级
解析器数字越小的 rank 越优先:managed policy(受管配置)→ CLI 参数 → 保留的 reload 值 → 环境变量 → 用户config.toml→ 类型化默认值(见 configuration/resolver.py 与 config-layering 概念)。
配置还有一个"单次生成(generation)"模型(ARCHITECTURE.md):配置文件被读取进一个进程范围的共享 generation,首次读取构建后复用;运行期间编辑config.toml不会影响读者,直到 generation 前进(in-app 写默认配置路径会刷新 generation,或/reload)。环境层始终是活性的——EnvProvider在解析时读取os.environ,因为进程在 dotenv 引导和每次 cwd 切换时会改变它。
子进程环境即安全边界
start_server_and_get_agent走完配置后,_apply_server_config(config)把DEEPAGENTS_CODE_SERVER_*变量写入子进程环境。子进程环境同时是一道安全边界:启动敏感的继承变量(包括PYTHONPATH)在服务器解释器启动前会被剥离;原始的PYTHONPATH仅通过独立通道转发给"审批门控的 shell 执行"使用;子进程 profile 被钉在客户端启动 profile 上(见 code-agent.md)。
远程边界与持久化工作区身份
RemoteAgent:RemoteGraph 的薄适配层
remote_client.py 中的RemoteAgent是 LangGraphRemoteGraph的薄 dcode 适配器。底层客户端负责:HTTP/SSE 解析、messages-tuple流模式协商、命名空间提取和中断检测。dcode 在此基础上:
- 归一化线程 ID;
- 把流式返回的消息字典转换为 Textual 适配器需要的消息对象;
- 让状态快照保持服务器的序列化形式(不做反序列化,见
aget_state的实现); - 提供
aensure_thread:在 LangGraph dev 服务器中,checkpoint 持久化与 HTTP 线程注册是分离的——服务器重启后,磁盘上可能仍有 checkpoint 状态但 live 线程行不存在,该方法用if_exists='do_nothing'做幂等的 HTTP 侧注册(aensure_thread)。
首次使用前的工作区绑定
在第一次使用某个线程之前,RemoteAgent会把它自己的 cwd、工作区策略与配置指纹 POST 到/dcode/threads/{thread_id}/workspace,并缓存返回的描述符(abind_workspace/_request_workspace,见 remote_client.py)。它还单独确保远程 HTTP 线程记录存在——因为持久化的 checkpoint 数据可能在没有 live 线程行的服务器重启后存活。
服务器侧做三件事(workspace.py):
- 规范化:
_canonical_directory要求绝对路径、禁止..穿越、resolve(strict=True)且必须是目录(workspace.py); - 计算身份与资源键:
workspace_id = canonical_fingerprint({cwd, project_root}),resource_key = canonical_fingerprint({workspace_id, config_fingerprint})(resolve_workspace)——资源键把"目录身份"和"资源策略指纹"绑定在一起,成为运行时的不可变选择依据; - 原子持久化:
_bind用BEGIN IMMEDIATE+INSERT OR IGNORE把绑定写入会话 SQLite 的dcode_thread_workspaces表(workspace.py),列包含thread_id、schema_version、workspace_id、cwd、project_root、generation、resource_key、config_fingerprint、workspace_config_json。
绑定不可变:冲突即拒绝
绑定是不可变的:后续的 bind 或执行上下文必须匹配该工作区的workspace_id与配置指纹,否则服务器抛WorkspaceConflictError(_binding_conflict区分"线程已绑定到不同工作区"、"配置漂移"、"项目策略漂移"三种原因,见 workspace.py)。
策略被分成两组(_server_config.py):
SESSION_WORKSPACE_FIELDS(会话字段,客户端可从自己的 CLI 标志声明):allow_fs_tools、auto_approve、enable_shell、sandbox_type、shell_allow_list等,声明它们只证明双方一致;PROJECT_WORKSPACE_FIELDS(项目字段,服务器必须按项目目录解析、绝不接受客户端声明):extension_paths、mcp_config_path、sandbox_setup、trust_project_extensions、trust_project_mcp——每项都授予"限定在某个 checkout 范围内的代码执行"(MCP 服务器、沙箱 setup 命令、Python 扩展),可声明的客户端就能用一个目录的配置去执行另一个目录的信任决策。
resolve_workspace(_server_config.py)处理跨目录策略:启动项目保留其策略原样;任何其他项目从零开始——MCP 与沙箱 setup 被丢弃(drop)而不是重新发现,扩展信任从该项目的信任存储重新读取。_same_workspace_project按设备与 inode 比较目录,失败即关闭。
对于携带上下文的调用,make_graph要求非空thread_id与匹配的工作区负载,读取持久化绑定,并选择该绑定的运行时,而不是信任调用方选定的 cwd(见 server_graph.py 的make_graph:有execution时经require_thread_workspace校验后返回绑定运行时;无执行上下文时回退到get_server_runtime)。这使"工作区绑定先于执行"成为服务器图选择的权威来源。
图装配与运行时作用域
create_cli_agent:组合入口
create_cli_agent(agent.py)是组合入口,它把以下要素组装为编译后的图与复合 backend(server_graph.py 展示了服务端的实际调用参数):
- 解析后的模型(含
model_params、profile_overrides、cli_max_retries); - 内置工具与 MCP 工具;
- 可选沙箱(
sandbox_backend+sandbox_type); - 文件系统与审批策略(
fs_tools、auto_approve、interrupt_shell_only、shell_allow_list); - 内存、技能、解释器配置与子代理(
enable_memory、enable_skills、interpreter_config、async_subagents); - 评分上下文(
goal_criteria_tools、rubric_grader_tools); - 凭据与环境(
credentials_snapshot、environ)。
它返回编译图与复合 backend;服务器从同一个 backend 派生它的offload 操作(offload_operation_from(composite_backend)),因此图和 offload 共享资源所有权——图与 offload HTTP 路由解析到同一个 agent、backend 和压缩策略(server_graph.py)。
评分上下文的"只读工具"规则
criteria 创建与 rubric 评分获得内置的外部上下文工具,以及注解被显式且一致地声明为只读的 MCP 工具(_criteria_context_tools+_mcp_tool_is_explicitly_read_only,见 server_graph.py)。MCPToolAnnotations.readOnlyHint被适配器序列化为 camel-case 的readOnlyHint元数据键;实现要求字面量布尔True并拒绝矛盾的破坏性 hint——缺失、畸形、矛盾或可变的注解一律失败关闭(fail closed)。
make_graph 的选择规则
make_graph有两条明确的选择规则(见 server_graph.py 与 code-agent.md):
- 带执行上下文:校验持久化线程绑定,获得以该绑定持久化资源键为 key 的运行时(
_workspace_runtime→_resolve_bound_workspace_config→_make_graphs); - 不带执行上下文:使用配置的 launch workspace(若存在),否则使用锁保护的进程级运行时(
get_server_runtime→_get_runtime)。
进程级运行时缓存是"承重"的
进程运行时缓存是承重(load-bearing)的,而非单纯的优化(server_graph.py):它防止重复的 MCP 发现、沙箱创建和atexit清理注册——每请求重建会重复发现 MCP 服务器、泄漏沙箱会话、堆叠重复的atexit处理器。两个消费者(交互图和 offload HTTP 路由)共享这个缓存。
工作区运行时存放在共享锁 LRU 缓存中,上限32 项(_MAX_WORKSPACE_RUNTIMES = 32,server_graph.py)。在构建工作区运行时之前,服务器为其绑定 cwd 重建配置,并要求其策略与指纹等于持久化绑定(_resolve_bound_workspace_config对项目策略漂移和配置指纹漂移分别抛出命名了漂移字段的WorkspaceConflictError,见 server_graph.py)。配置的沙箱是进程级的,只能被一个工作区认领;第二个工作区会被拒绝而不是共享(_claim_sandbox_workspace)。
失败处理与清理
启动屏障与 DEEPAGENTS_STARTUP_ERROR 标记
运行时构造是一道启动屏障(startup barrier):构造失败会发出DEEPAGENTS_STARTUP_ERROR:标记并以退出码 1 退出(emit_startup_failure+sys.exit(1),见 server_graph.py 与 _startup_error.py)。父进程从子进程输出中抓取该标记,而不是把失败降级为"就绪超时"——这让失败原因(如"Sandbox provider 'X' is not installed")能精确传回用户界面。
server_manager在以下任一失败时会停掉它拥有的服务器:启动失败、图就绪失败、远程客户端创建失败、工作区设置失败。其finally清理是取消安全的(asyncio.CancelledError是BaseException,finally而非except Exception才能保证取消时也清理,见 server_manager.py)。
会话拆除与 POSIX 进程组
正常会话拆除时,server_session的finally停止拥有的服务器并发出为 debug 保留日志排队的通知(server_session)。在 POSIX 上,子进程拥有专用进程组:优雅发信号、等待和硬杀升级(hard-kill escalation)都包含后代进程。Windows 升级只能硬杀根进程句柄,因此存活的子孙进程可能成为孤儿——这是跨平台清理行为的一个已知不对称点(见 code-agent.md 的 "Failure handling and teardown")。
ACP 集成冒烟测试
ACP 集成冒烟测试(test_acp_mode.py)启动deepagents --acp --no-mcp,执行协议初始化和new_session,并断言返回的会话有 ID。这保护 ACP 的启动与会话创建路径,而不经过普通回环路径——两条路径由此获得独立的回归保护。
ACP stdio 生命周期
--acp 在启动进程内运行
--acp在启动进程中调用_run_acp_cli_async(main.py),它:
- 解析初始模型(
create_model,失败写 stderr 并返回退出码 1); - 持久化解析出的模型到
[models].recent(尽力而为,失败不拖垮运行中的会话); - 加载工具与 MCP 配置(
fetch_url、get_current_thread_id,有 Tavily 时加web_search;MCP 经resolve_and_load_mcp_tools); - 在服务生命周期内保持 dcode checkpointer 打开。
其build_agent(context)回调使用 ACP 会话选择的模型(或解析出的默认)和 cwd 构造ProjectContext,然后调用create_cli_agent并传入共享的 checkpointer(见 code-agent.md 的 "ACP stdio lifecycle")。ACP 图是会话局部的(session-local),而不是普通工作区运行时缓存中的条目——这正是"不启动 langgraph dev、不使用 RemoteAgent"的直接体现:没有回环服务器、没有 HTTP/SSE、没有/dcode/threads/...绑定路由。
Auto 模式下的 AgentServerACP
在 Auto 模式下,dcode 使用AgentServerACP(acp.py),它包装本地图流式输出,以写入受信任的 Auto 审批状态、附加 prompt 元数据并提供 CLI 上下文。_AutoGraph.astream在每次运行前把ApprovalMode.AUTO写入 store(approval_mode_payload),并在最后一个消息上附加USER_PROMPT_METADATA_KEY元数据(含turn_id),然后以CLIContextSchema(approval_mode=AUTO, auto_approve=True, ...)运行图(acp.py)。
两个使用约束(见 code-agent.md):
- YOLO 需要事先确认(prior acknowledgement);
--auto-classifier-model仅在解析出的审批模式是 Auto 时被 ACP 接受。
ACP 失败写入 stderr 并返回非零状态码,不使用普通子进程的启动标记;服务结束后finally块清理 MCP 会话管理器。
扩展与运维指引
组合点(composition points)
技能与子代理、内置与 MCP 工具、沙箱提供方、hooks 与 commands、以及经授权的 Python 扩展都是组合点(ARCHITECTURE.md):项目可以提供共享默认与集成,每个用户在顶层叠加个人配置。
在普通服务器模式下,影响资源的设置属于工作区策略/指纹的一部分:一个已绑定的线程不能被热重配置为不同的资源策略(见 code-agent.md 的 "Extension and operations guidance")。preserve_bound_extension_trust还保留既有线程的扩展信任:当线程绑定时策略为False而新授予为True时,新授予被推迟给新线程,撤销则始终可见以便绑定与运行时校验拒绝(_server_config.py)。
调试时的归属判断
由于存在客户端/服务器边界,调试时的第一件事是判断失败属于哪一侧(ARCHITECTURE.md):呈现与输入通常属于客户端;模型执行、工具、内存和图启动通常属于服务器。
小结
dcode 的架构价值在于把"呈现/审批"与"图执行"之间的边界做成进程级所有权边界,并用一套机制让这条边界既清晰又可审计:
ServerConfig.to_env()/from_env()保证客户端与服务器子进程对配置的理解永远一致;- 持久化工作区绑定(SQLite + SHA-256 指纹 + 不可变冲突拒绝)让"哪个目录、什么策略、哪个运行时"成为服务器权威,客户端无法越权声明项目策略;
- 进程运行时缓存与共享锁 LRU(上限 32)保证 MCP 发现、沙箱创建、
atexit注册恰好一次; - 启动失败用
DEEPAGENTS_STARTUP_ERROR:标记回传精确原因,避免退化为无意义的超时; --acp则完全绕过回环服务器,用进程内会话图 + stdio 提供一条独立、可测试的集成路径。
想要深入体验,可以阅读 运行 dcode 会话指南、运行时行为、配置分层 与 ACP 集成;需要本地构建与调试时,可参考 DEVELOPMENT.md 与 COMMANDS.md。
【免费下载链接】deepagentsThe batteries-included agent harness.项目地址: https://gitcode.com/GitHub_Trending/de/deepagents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考