DeepSeek Harness Hub
← 返回列表

CNSeniorious000/dsh-py-codeact

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
未验证

一个用于 DeepSeek Harness 的 CodeAct agent 循环:模型的动作空间是一个持久化的…

尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/9/17 · 已提供中文文档

基于Python的CodeAct,用于dsh,支持跨单元格的持久状态,取代Dynamic Workflows和code-mode

综合分
30.7
GitHub 分
30.7
用户评分
★ Stars
1
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add CNSeniorious000/dsh-py-codeact
该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/18(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-llm@deepseek-ai/dsh-tools@deepseek-ai/cordis@deepseek-ai/dsh-session@deepseek-ai/dsh-system-prompt@deepseek-ai/dsh-client-ui-primitives@deepseek-ai/dsh-client-ui-slots@deepseek-ai/dsh-client-ui-tool
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

dsh-py-codeact

一个用于 DeepSeek Harness 的 CodeAct agent 循环:模型的动作空间是一个持久化的 IPython 会话,而 harness 工具以 awaitable 的形式桥接进其中。

一个 cell
from __dsh__.tools import read, glob
import pandas as pd, io

paths = await glob(pattern="data/.csv")
frames = [pd.read_csv(io.StringIO(await read(file_path=p))) for p in paths]
df = pd.concat(frames)

稍后的一个 cell —— df 仍然绑定着,read、_、Out[1] 等等也都还在
df.groupby("region").revenue.sum().nlargest(3)

python 是模型唯一能直接调用的工具。其他一切都位于 __dsh__.tools 中,所以动作空间确实只有一个工具——这正是 CodeAct 的形态。

Cell 通过 InteractiveShell.run_cell_async 运行,因此 magics、transform_cell、顶层 await、执行历史以及 IPython 的 traceback 格式化器都免费获得。下面的 shell 设置以及针对 LLM 的调整均遵循 CNSeniorious000/temporary-mcp-servers:ipython-mcp.py。

它与 dsh 内置 Code Mode 的区别

同样的理念,相反的状态模型。

| | Code Mode (run_code) | 本插件 (python) |
|---|---|---|
| 语言 | TypeScript(或通过后端使用 Python) | Python,运行于 IPython 中 |
| 调用之间的状态 | 无——每次运行一个全新的 worker | 在会话期间持久化 |
| 层级 | 一个 CodeRuntime seam provider | 一个普通的工具插件 |
| 工具暴露面 | 生成的 tools 全局变量 | __dsh__.tools 模块 |
| 排他性 | tools.mode: code,由 registry 强制执行 | mode: code,通过 prompt 组装 + 一个 guard 实现 |
| 结果 | 程序日志 + 返回值 | 带标签的 stdout / return / traceback / note |

它刻意不是一个 CodeRuntime 后端。 该 seam 的契约规定“运行之间没有任何状态存活”,而 Code Mode 自己的 Agent Note 也记录了一个持久化 REPL kernel 在 MVP 阶段被否决,因为跨调用状态对会话日志而言将不可见。持久化 kernel 无法符合该契约——因此它拥有自己的进程,而不是实现那个接口。

这一权衡是真实存在且与生俱来的:解释器的实时状态无法从会话回放中重建。日志中确实存在的是每个 cell 的源码以及每一次桥接的工具调用,这足以审计发生了什么。

__dsh__ —— 这个 seam

__dsh__ 是 sys.modules 中的一个真实包,因此每种导入形式都能解析:

from __dsh__.tools import glob, grep  # 最常用的那种
import __dsh__.tools as T  # T.read(...)
from __dsh__ import tools  # tools.read(...)

一个以 dunder 命名的包读起来像是 harness 所拥有的,能在 %reset 后存活,并把显而易见的名称 tools 留给模型自己的变量使用。

每个工具都是一个真正的 async def。 dsh 的描述成为它的 __doc__,其参数成为仅限关键字的签名,因此 REPL 自身的自省就完成了说明工作:

In [1]: read?
签名:read(, file_path: 'str', offset: 'int' = ..., limit: 'int' = ...) -> 'ReadOutput'
文档字符串:
从工作区读取文件。结果包含行号……

参数:
file_path:要读取的路径,由文件系统后端解析。
offset:要返回的第一行,从 1 开始计数。默认为 1。
limit:要返回的最大行数。默认为 2000。

该代码块渲染出相同的画面:每行一个参数,其描述作为行尾注释,工具自身的描述作为文档字符串——因此模型在提示中读到的内容与它从 ? 得到的内容是同一段文本,来自同一来源。注解由 dsh 自己导出的渲染器在宿主侧渲染,因此不存在第二个会漂移的 JSON-Schema 映射器。

返回类型来自工具自身的 output schema,经由 ctx.tools.sdkSchemas(scope)——即承载它的投影。这是模型无法通过更仔细阅读来恢复的那一个注解:错误的参数会在调用时大声失败,而未知的返回形状只能通过调用一次并打印结果来发现,这会让每个工具都耗费整整一个回合。未声明输出 schema 的工具仍然渲染为 Any;声称一个没人声明过的类型比承认无知更糟。

该 schema 中的每个对象都在签名上方声明为具名的 TypedDict,而由多种形状构成的输出则成为具名分支的联合。对于桥接的 MCP 工具,这里描述的 schema 是载荷,而不是传输包装器——见下文;dict[str, Any] 只会说一个字典到达了,却不会说哪些键到达了,而后者正是返回注解存在的唯一意义。

class BashOutput1(TypedDict):
kind: Literal["background"]
jobId: str

class BashOutput2Stdout(TypedDict):
text: str
truncated: bool

class BashOutput2(TypedDict):
kind: Literal["foreground"]
exitCode: int | None
stdout: BashOutput2Stdout

async def bash(, command: str, run_in_background: bool = ...) -> BashOutput1 | BashOutput2: ...

jsonSchemaToPy 做不到这一点,而且它自己也这么说——它是无上下文的,而命名一个 TypedDict 需要 renderToolsSdkPy 提供的渲染上下文。由于无处挂载声明,它会把每个对象降级为 dict[str, Any],而在一个标准目录中,除一个工具外,这就是每个工具的返回类型。renderType 这个携带上下文的核心并未导出,因此改为借用上下文:调用 renderToolsSdkPy 时剥离参数——这是它唯一无需分配类即可渲染的输入,这使得它输出的每个类都是输出类——然后回读它返回的代码块,以获取声明和每个工具的返回文本。于是那些 Literal、嵌套类、冲突后缀和 Unicode 标识符规则都成为 dsh 自己的,而不是一个与之并行漂移的第二个映射器。绑定集合随每个单元格重新发送,因为限制和对话中途的工具变更可能会在调用之间让某个工具移入或移出。

MCP 包装器不会到达单元格
dsh 的 MCP 客户端将一次调用解析为 { content, structuredContent? }——即协议的块数组加上服务器自身的载荷——并恰好将其声明为工具的输出。这个包装器是传输层,而非 API:content 的文本重复了载荷,而其中的图像会被单独重新附加到对话中,因此单元格对它无能为力。拿到这个包装器后,模型会写出 r["structuredContent"]["result"]——并花费一次调用才发现它不得不如此。

因此,桥接层进行解包,而签名描述的是单元格实际接收到的内容:

mcp.review.search(, q: str) -> McpReviewSearchOutput   # 载荷,而非包装器
mcp.email.ping() -> str                                 # 未声明载荷:文本块,已拼接

在实际情况中,不声明输出模式的服务器是常见情形,其结果确实只是文本——在六次试验中测量的全部 4142 个 MCP 结果里,每个都只有一个文本块,这就是为什么它是 str 而不是 list[str]——后者会在每个调用点付出一次 r[0] 的代价,只为保留一个从未出现的边界。完全不携带文本块的结果会解析为空字符串:其中的图像会被重新附加到对话中并在下一步读取,因此单元格从来就没有任何东西可接收。存在一种可能的分歧,且属于服务器一方:一个声明了输出模式但在某次调用中省略了 structuredContent 的工具,会解析为该次调用的文本,而签名却承诺了载荷类型。

解包的依据是工具声明的输出模式,而非返回的值:一个自身载荷恰好是 {content: [...]} 的工具并没有包装任何东西,若将其值替换为拼接后的文本,就会改变该工具返回的内容,且无从说明。这有意区别于 Code Mode,后者将整个信封交给 tools.name(args)。

MCP 工具正常工作,无需任何特殊处理

MCP 服务器注册到与其他一切相同的 ctx.tools 注册表中,因此它们像任何其他绑定一样出现在 __dsh__.tools 中,并通过同一管道进行分发。它们被呈现为分组形式,归在一个 mcp 之下:

from __dsh__.tools import mcp

data = await mcp.gh.github_graphql(query="{ viewer { login } }")
dsh 将它们命名为 mcp____,而当挂载了上百个时,那行 import 就占了提示块的大部分,而每个调用点还要重新拼写其服务器名。扁平名称仍然保持绑定——mcp 只是它们的展示方式,而非它们的本质——所以在此改动之前编写的单元格仍可运行,import  仍会绑定它们,只是列表不再显示它们。分组无法服务的名称仍会显示:dsh 会对需要规范化的公开名称做哈希处理,而截断可能落在第二个 __ 之前,而真正的双下划线原始名称会被 __getattr__ 拒绝;无论哪种情况,扁平名称都是唯一可用的。它们被排除在 dir(__dsh__.tools) 之外,原因与提示块不再打印它们相同;分组无法触及的名称(dsh 会对需要规范化的公开名称做哈希处理,而截断可能落在第二个 __ 之前)仍会列出,因为 mcp 并不是它的另一种说法。提示块按每个模块一节列出它们——先是 # __dsh__.tools,然后是 # __dsh__.tools.mcp.——因为它们本就如此。它过去声明的是带 self 方法的 Protocol 桩,抄自 dsh 自己的 SDK 渲染器;那个渲染器描述的是单例对象(tools: Tools),而这个分组是一个真实的包,所以那个桩声称了一个永远不会发生的绑定。mcp.exa.web_search? 返回 (, query: str) -> str,提示块现在也这么说。

它是一个真实的包,所以服务器可以作为模块导入——当一个单元格主要依赖某个服务器时,这比在每个调用点写 mcp. 更易读:

from __dsh__.tools.mcp.calendar import list_events, create_event
from __dsh__.tools.mcp import calendar          # or the server itself

深层形式正是每个服务器都拥有自己的 sys.modules 条目的原因:__getattr__ 可以服务 from __dsh__.tools.mcp import calendar,但无法服务 from __dsh__.tools.mcp.calendar import list_events——导入机制会将后者作为模块来查找。不需要元路径查找器;注册就够了。

mcp 及其服务器模块是目录的实时视图,而非它们被导入时所在单元格的快照:限制或重连的服务器会在调用之间移入移出工具,而且与单个工具不同,模型没有理由去导入该命名空间两次。(用 from ... import 拉出的名称是快照,对任何 Python 导入都是如此。)该名称是保留的——名为 mcp 的原生工具不会被绑定,名为 ToolCallError 的也不会,任何以 _ 开头的名称同样不会。提示块镜像了这全部三种情况,因为一个导入了内核从未绑定的名称的提示块,会在模型复制的第一行就引发 ImportError。

dsh 的 MCP 客户端明确知晓这条路径——其规范值“为程序化调用者和 Code Mode 调用者保留完整的 JSON MCP 块和可选的结构化内容”——而子调用会像其他调用一样记录一行 SUBTOOL。
值得对比的是:Anthropic 的服务器端程序化工具调用与 MCP 工具不兼容。在宿主侧拥有这座桥,才能换来这一点。

原始的 MCP 名称是连字符经常出现的地方——dsh 在 [A-Za-z0-9_-] 上做规范化,所以 - 得以保留,而 - 在任何 Python 标识符中都不合法。这样的工具会以两种拼写绑定:它自己的拼写,以及列表展示和代码块中拼写所用的 -→_ 折叠形式。mcp.notion.API_patch_block_children(...) 会分派到 mcp__notion__API-patch-block-children,而现有的 getattr(mcp.notion, "API-patch-block-children") 仍然可用。这种折叠永远不会取代真正的工具:一个同时暴露 a-b 和 a_b 的服务器,a_b 仍然表示 a_b,两者都保持列出。

在添加它之前测量过:在一次 20 任务的基准运行中,17030 次分派中有 676 次(4.0%)发往十二个带连字符的工具,全部来自同一个服务器,而 API-patch-block-children 是整个目录中分派次数最多的单个工具——每个调用点都要付出一次 getattr 的代价,而代码块中没有与之对应的签名,因为一个无法拼写的名称被完全排除在列表之外。在这些运行所分派的 208 个不同工具名称中,没有任何折叠与另一个折叠冲突,也没有遮蔽现有名称。

(折叠只能修复替换所能触及的问题。关键字(class)或以数字开头的名称(123tool)无法拼写,原因不是任何重写能修复的,它们仍然走 getattr 路线——getattr(__dsh__.tools, "class")——当存在代码块时,代码块仍然指向它。)

独占模式

mode: code(默认值)使 python 成为唯一可直接调用的工具:

- 一个 system-prompt/assemble 瀑布监听器将 assembly.tools 过滤为仅剩 python,因此其他 schema 永远不会进入请求;
- ctx.tools.guard() 拒绝模型对其他任何内容的直接调用,并指明返回路径(from __dsh__.tools import )——仅靠呈现不是强制,而单纯的拒绝会被读作部署损坏。子分派携带 parent 令牌,因此豁免。

ctx.tools.restrict() 无法完成第一项工作:它只遮蔽全局工具,而在预设组合中,工具是作用域局部的——“作用域注册仍然可见”。assemble 瀑布才是拥有面向模型列表的那一层。

该规则作为自己的提示词部分发布,位于顺序 99,在 100–199 工具指导区间之前,原因与 Code Mode 将自己的仅代码规则放在那里相同:模型应当先读到它可以调用哪些工具,再读到每个工具的用途。(测量结果:将其移出该区间后,它从第 4129 个字符变为第 170 个字符。)

设置 mode: both 可保留原生 schema——在调试组合时有用,但不是 CodeAct 形态。

CodeAct 卡片

浏览器那一半在 key: 'python' 下注册 tool.call.toolview,为调用提供自己的卡片:

CodeAct · count the markdown files at the root
┌ python                                  copy ┐
│ from __dsh__.tools import glob                │
│ print(len(await glob(pattern=".md")))        │
└───────────────────────────────────────────────┘
Glob · .md          ← 原生 SUBTOOL 行,未改动

代码主体是随包提供的 CodeBlock 原语,带有 lang: "python",因此高亮、语言标签和复制按钮都是 dsh 自带的。

需要客户端一半,因为宿主侧的任何东西都无法影响这一行。 toolRowModel 完全忽略 presentCall 视图,并从工具名称加上原始参数推导出一切:

title   = TOOL_TITLES[toolName] ?? VARIANT_TITLES[classifyTool(toolName)]  // 未知 → "Tool call"
summary = variant === 'others' ? ${toolName} · ${base} : base
body    = deriveBody(variant, argsRaw)                                     // 未知 → 原始参数 JSON

而 classifyTool 读取一个硬编码的映射,其中 run_code: 'code' 是唯一进入语法高亮分支的条目(lang: "typescript" 也是硬编码的)。run_code 是插件不得占用的保留名称。上游修复方案已在 discussion #4724 中提出:让调用视图携带 kind: 'execute' 以及一种语言。

SUBTOOL 嵌套在构造上得以保留。 ToolCallBranch 将 block.subCalls 渲染为槽位占用者的兄弟节点:

children: [renderSlot('tool.call.toolview', owner, { entryKey: toolName, fallback }), children]

因此注册的 toolview 只替换卡片主体。槽位契约也是这么说的:“注册对你自己的工具是增量的”。

该卡片以客户端模块系统的工厂形式(一个注册 CJS 工厂的经典脚本)手写而成,而非打包——此包没有构建步骤,卡片只需要 React 加上一个随包提供的原语。

关键是字面名称 python;自定义的 toolName 会回退到通用行。

SUBTOOL 行免费获得

每个桥接调用都会追加与 Code Mode 使用的相同的两个会话事件:

session.append('tool/code-dispatch-start', { rootCallId, parentCallId, subCallId, name, arguments })
session.append('tool/code-dispatch',       { ...same, isError, content })

它们通过 subCallId(:py:)配对,且结算事件携带 tool/result 自己的词汇(content + isError)——因此轨迹 UI 通过它用于原生调用的完全相同代码路径来渲染它们,作为 python 调用下的 SUBTOOL 行。无需客户端改动。

这两种类型都在 KNOWN_SESSION_EVENT_TYPES 中,因此复用它们是被支持的。注意,仓库外的插件无法发明新的事件类型:持久化读取路径会拒绝包含该集合之外类型的日志,除非事件被标记为 ignorable,而下游事件的注册接口在上游被明确推迟了。
子调度携带外部执行的 parent 令牌,因此它们会重新进入完整的 pre-execute → guards → execute → post-execute → result 流水线。沙箱和审批栈仍然会拦截从单元格内部发起的每一次工具调用。

模型获得什么

- 魔术命令。 %whos 用于回忆它已绑定的内容,%timeit、%run script.py、%%writefile、obj? / obj??、%cd。在一个它已部分遗忘的会话中,以低成本进行自我定位。
- 历史。 store_history=True,因此 _、__、_i3、Out[n] 都能正常工作。
- 冗余导入提示。 当 import 将一个名称重新绑定到它已经持有的对象时,结果会带有  json is already imported in this session — no need to re-import it.  驱动持久 REPL 的模型会不断重复导入;告诉它比让它每个单元格都浪费一行更划算。(通过一个 dict 子类实现,该子类监视顶层 STORE_NAME 并检查前一个操作码是 IMPORT_NAME/IMPORT_FROM,因此 x = x 不会触发它。)
- 可读的 repr。 对于自身 __repr__ 为 object.__repr__ 的对象,使用 objprint + IPython 的 pretty —— 读取值的智能体需要结构,而不是 。
- 带标签的观察结果。 、、、、 —— 由于可能同时出现四种内容,模型需要知道哪个是哪个。一个普通的成功值保持裸值。
- 对提示文本的内省。 read? 用于查看一个工具的完整描述,dir(__dsh__.tools) 用于查看列表 —— mcp 而不是其下的一百个扁平名称,与代码块打印的内容一致 —— 然后 dir(mcp) 和 dir(mcp.) 用于查看那些,%whos 用于查看它自己的绑定。__all__ 保持原样:它是 import  绑定的内容,而不是向模型展示的内容 —— 因此它携带列表所丢弃的每一个扁平名称,以及 ToolCallError,没有任何列表会显示它,因为它不是工具,但 except ToolCallError 在 import  之后需要它。它内省的内容与代码块向它展示的内容一致:列表携带 mcp 而不是其下的一百个扁平名称,可选参数在那里渲染为 = ...,就像这里一样 —— inspect.signature 使用 repr,而 repr(...) 是 Ellipsis,这正是 read? 过去所说的。

取消

一个被中止的回合会发送一个带内 interrupt 帧,在任何 await 点取消正在运行的单元格。run_cell_async 自己捕获 CancelledError,因此模型会得到明确的告知(InterruptedError: the harness cancelled this cell. State is intact; the cell did not finish.),而不是收到一个它可能误读为自己代码中 bug 的原始 traceback。会话和每一个绑定都会保留。

一个纯 CPU 循环(while True: pass)永远不会到达 await 点。在 hardInterruptMs(默认 5 秒)之后,解释器会被 SIGKILL,下一次调用会重新生成它;该观察结果会以 [the interpreter was restarted; every earlier binding is gone] 为前缀,这样模型就不会继续引用已不存在的变量。

已知限制
- 原生写入不会被捕获。 stdout/stderr 是在 Python 层面捕获的,因此 print 会被捕获,但子进程写入 fd 1 的内容不会。请使用 subprocess.run(..., capture_output=True) 或 %run。(任何确实到达 fd 1/2 的内容——包括 IPython 自身刻意路由到那里的彩色 traceback——都只会为崩溃消息保留。)
- 作用域不是可选的。 可见的工具集来自 ctx.tools.sdkSchemas(scope)——该作用域就是 agent。省略它会得到全局视图,而在预设组合中,全局视图只包含宿主注册的工具;预设自身的 read/bash/edit 位于 agent 作用域中,会消失。提示词部分读取 assembly.scope,而内核的名称列表会随每个单元格重新发送(限制以及对话中途的工具变更可能会在调用之间让某个工具进入或移出)。
- 选择一个没有其他东西会响应的 toolName。 当同时挂载了 MCP IPython 服务器时,被要求“使用 python 工具”的模型会去找 mcp__py__ipython_execute_code——它没有 tools 绑定——然后报告你的工具不存在。
- Python 无法命名的形状会保持模糊。 键不是有效标识符的字段会将其自身的类降级回 dict[str, Any],而不是生成一个无法解析的主体——该工具仍可调用,只是那一个注解会静默。名为 self 的参数现在只是一个参数:每个工具都渲染为模块级函数,因此没有外层签名占用过这个名字。Python 拒绝的工具名会得到一个以可接受名称命名的类(123tool → Tool123toolOutput),而不是 class 123toolOutput——后者会导致 SyntaxError 并连带整个代码块一起失效;该工具没有 async def 行,但它仍然被绑定,并且 123tool? 指向同一个类。没有声明任何属性的 schema 仍会渲染为 Any;声称一个没人声明过的类型会更糟。
- 冷启动。 PEP 723 环境在首次使用时解析(uv python find --script)。之后就是热的;传入 python 可跳过它。

随后解释器会被直接启动,绝不会放在 uv run --script 后面。包装器会作为解释器的父进程留在进程树中:当它先退出时,解释器会被重新挂到 init 下,宿主持有的句柄报告退出,而一个完全存活的 kernel 看起来却像死了——于是下一个单元格会重新生成,会话状态随之消失,并出现一条 [the interpreter was restarted] 通知,而实际上没有任何东西导致它。alive 同样是根据退出事件来跟踪的,而不是从 proc.killed 读取——Node 在任何 kill() 调用时都会设置后者,包括进程幸存下来的信号。
- 是隔离,不是安全边界。 一个带有受控环境的独立进程,但模型代码可以 import os。请将此预设上的会话视为 shell 访问。
- 每个对话树一个解释器,每个 agent 一个 shell。 子 agent 复用父进程——共享其事件循环、sys.modules、已安装的包以及 __dsh__.shared——但拥有自己的全局变量和自己的工具目录。代价是一个 init 帧,而不是另一个解释器。
- __dsh__.shared 是这种隔离中唯一刻意留下的裂缝:一个进程内每个 agent 都可读写的模块,用于在一次扇出中传递活对象,无需序列化,也无需 token。
- shell 随其会话关闭而关闭;当最后一个指向它的 agent 消失时,进程也随之结束。

环境

默认情况下,内核获得一个允许列表——PATH、HOME、TMPDIR、LANG、LC_ALL、TERM——而不是 worker 线程运行时所用的空环境。运行 IPython 的 CPython 需要一个 profile 目录和一个用于 shell 转义的 PATH;关键在于排除环境中的凭据,而允许列表同样能做到这一点。inheritEnv: true 会传递整个 harness 环境。

COLUMNS/LINES 在内核内部被固定:在没有 TTY 的情况下,get_terminal_size() 会回退到 80×24,并以比 harness 渲染宽度窄得多的方式折行 traceback 和 pretty 输出。

安装

dsh plugin --profile  add dsh-py-codeact

然后将 example/agent.cordis.yml 中的那一行添加到你拥有的某个预设中。

内核会根据 py/kernel.py 中的 PEP 723 头信息自行准备 Python,因此除 dsh 本身之外,uv 是唯一的前置依赖;第一个 cell 会承担这次解析的开销,之后的 cell 则是热的。

对于卡片,还要在 profile 的 dsh.profile.bundles 中列出该包(~/.dsh/profiles//package.json)。浏览器 bundle 只会为被已启用的 Loader 条目命名的包提供服务,而预设的行是按会话生效的——它们在启动时不在 profile 的条目列表中,因此仅通过预设挂载的包永远不会获得其卡片。因此,本包自带的 cordis.patch.yml 会插入一个带有 uiOnly: true 的 profile 级行,这会使 host 部分在那里成为空操作:在 profile 级别挂载它会在全局注册 python,并将独占模式的守卫应用于每个预设。永远不要编辑随附的预设——先复制它(ctx.agentPresets.copy('standard', 'py-codeact')),然后用 standingKeyFor('py-codeact') 对结果进行挂载验证。

端到端验证

在真实的 dsh 会话中针对 Macaron V1 Venti 进行了验证。给定一个普通任务——“读取这个 CSV 并告诉我哪个地区赚得最多”,完全没有提及 Python 或该模块——模型从 __dsh__.tools 导入,运行了该 cell,并用表格作答。该请求只携带了一个工具 schema(python);其他 33 个工具只能从 cell 内部访问,而桥接的 read 在轨迹选项卡中显示为 python 调用下的 SUBTOOL 行。一次 headless 运行约 11 秒完成并退出。

测试

node test/smoke.js
直接驱动内核和通信协议,不使用任何测试框架:状态持久化、store_history、魔术命令、冗余导入提示、顶层 await、工具桥接、ToolCallError、asyncio.gather、回溯恢复(以及 ANSI 绝不会泄漏到捕获的 stderr 中)、带内中断,以及 SIGKILL 升级。

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群