fix(tools): attach Python SDK docstrings to their own methods

A description was emitted above the `async def`, where Python treats the
first string as the `Tools` class docstring and every later one as a dead
expression — leaving each method undocumented in the model's only source of
tool semantics. Emit it as the first statement of the method body instead.

Also names the known languages in the run_code flavor guard (the reachable
rejection, symmetric with the SDK_RENDERERS guard) and corrects three doc
claims: the code-runtime group README no longer calls the generated SDK
TypeScript, the base Code Mode note states its serial dispatch in past
tense, and the tools README points at the rationale the language-dispatch
note actually carries.
This commit is contained in:
Chinesezjc
2026-08-05 14:02:47 +08:00
parent 975350c7b8
commit 7a178951d6
16 changed files with 60 additions and 23 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/code-runtime/README.md
README.md: dbe6b37ffa01d07c6902672a06ebf6f88548ff99
README.zh.md: a5acbad3cce19366ca9ca4729f5285905ab026eb
README.md: 4ee441bf99ddd59c2cf6e088cae6921ffebf7c75
README.zh.md: 8a0d47fff43a9f894e8919d40a2934e20d47d62d

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
The code-execution capability seam (see [capability seams](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract runtime interface for executing one model-written program against host-provided async bindings, capturing what it printed and returned. The consumer is the tool registry's [Code Mode](../core/tools/README.md) (`tools: { mode: code }` — the `run_code` tool and the generated TypeScript SDK); design in the [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md). **Product** packages.
The code-execution capability seam (see [capability seams](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract runtime interface for executing one model-written program against host-provided async bindings, capturing what it printed and returned. The consumer is the tool registry's [Code Mode](../core/tools/README.md) (`tools: { mode: code }` — the `run_code` tool and the SDK generated in the loaded runtime's `language`); design in the [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md). **Product** packages.
| Package | Role | ctx key |
|---|---|---|

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文
代码执行能力 seam(参见[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):一个抽象运行时接口,用于针对宿主提供的异步绑定执行一段模型编写的程序,并捕获程序打印和返回的内容。消费方是工具注册表的 [Code Mode](../core/tools/README.md)(`tools: { mode: code }`,即 `run_code` 工具与生成的 TypeScript SDK);设计记录在 [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) 中。这些都是**产品**包。
代码执行能力 seam(参见[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):一个抽象运行时接口,用于针对宿主提供的异步绑定执行一段模型编写的程序,并捕获程序打印和返回的内容。消费方是工具注册表的 [Code Mode](../core/tools/README.md)(`tools: { mode: code }`,即 `run_code` 工具与按所加载运行时 `language` 生成的 SDK);设计记录在 [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) 中。这些都是**产品**包。
| 包 | 职责 | ctx 键 |
|---|---|---|

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/core/tools/README.md
README.md: f561a08bbc9645ea1bc127eedb04d2249a60a156
README.zh.md: 7318b13a6640060176bb42af032f42456dd0d984
README.md: 20df93e734afb9e7f4280d3aa208af2c8338001c
README.zh.md: d16a8a90c626c746b8629d148e432302f72b5f30

View File

@@ -190,6 +190,6 @@ Append-only; newly visible content follows the reusable request prefix and does
- **`tools/pre-execute` deliberately cannot rewrite `exec.arguments`** — logged and rendered args would desync from what ran; the rewrite design is [a proposed Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md).
- **Caller-defined subagent and workflow structured outputs remain object-rooted** — this is a consumer-level guard; the shared schema vocabulary and tool outputs support every JSON root.
- **`timeoutMs` on a definition is declarative only** — the registry never enforces deadlines; enforcement requires the `@deepseek-ai/dsh-timeout-policy` wrapper.
- **Code Mode's SDK language follows the one loaded runtime and the presentation mode is service-wide** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language` has a registered SDK renderer (`typescript` via the worker backend, `python` for any runtime reporting that language); scoped restrictions/shadows still choose each agent's visible bindings, but one tool cannot be native-only while another is code-only, and a single runtime fixes the language service-wide (the [language-dispatch Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md) owns why per-agent language switching is deferred).
- **Code Mode's SDK language follows the one loaded runtime and the presentation mode is service-wide** — `mode: code`/`both` rejects prompt assembly unless `ctx.codeRuntime.language` has a registered SDK renderer (`typescript` via the worker backend, `python` for any runtime reporting that language); scoped restrictions/shadows still choose each agent's visible bindings, but one tool cannot be native-only while another is code-only, and a single runtime fixes the language service-wide (the [language-dispatch Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md) owns the lookup, and why the registry reads the loaded runtime instead of carrying a language field of its own).
- **Code Mode intermediate values are execution-local and unbounded by bytes** — the canonical typed values cannot be reconstructed from session replay and may exhaust process or worker memory; only the outer `run_code` output has the worker's configurable hard cap. The durable log copy of each sub-call IS bounded: the `tools/code-dispatch-log` waterfall lets the spill policy replace an oversized `tool/code-dispatch` content with a preview + locator ([rationale](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md)).
- **`run_code` state is fresh per run** — a persistent REPL-style kernel is rejected for the MVP (cross-call state would be invisible to the log); see [the Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md).

View File

@@ -190,6 +190,6 @@ The available tools:
- **`tools/pre-execute` 有意不允许改写 `exec.arguments`**:否则日志记录和呈现的参数会与实际运行内容失去同步;改写设计记录在[拟议的 Agent Note](../../../.agents/notes/proposed/feature/2026-06-30-pre-tool-input-rewrite.md)中。
- **调用方定义的 subagent 与工作流结构化输出仍要求对象根**:这是消费方层面的守卫;共享 schema 词汇和工具输出支持任意 JSON 根。
- **定义上的 `timeoutMs` 仅为声明**:注册表绝不会强制执行截止时间;要强制执行,必须使用 `@deepseek-ai/dsh-timeout-policy` 包装层。
- **Code Mode 的 SDK 语言跟随唯一加载的运行时,且呈现模式在服务内统一**:`mode: code`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language` 有已注册的 SDK 渲染器(`typescript` 经 worker 后端,`python` 用于任何报告该语言的运行时);作用域限制/遮蔽仍会选择每个 agent 的可见绑定,但不能让一个工具仅使用 Native、另一个仅使用 Code,且单个运行时把语言固定为服务级([语言分发 Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md) 负责说明为何暂缓逐 agent 切换语言)。
- **Code Mode 的 SDK 语言跟随唯一加载的运行时,且呈现模式在服务内统一**:`mode: code`/`both` 会拒绝组装提示词,除非 `ctx.codeRuntime.language` 有已注册的 SDK 渲染器(`typescript` 经 worker 后端,`python` 用于任何报告该语言的运行时);作用域限制/遮蔽仍会选择每个 agent 的可见绑定,但不能让一个工具仅使用 Native、另一个仅使用 Code,且单个运行时把语言固定为服务级([语言分发 Agent Note](../../../.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md) 负责这次查表,以及注册表为何读取所加载的运行时而不自带 language 字段)。
- **Code Mode 中间值只存在于执行局部,且没有字节上限**:这些规范的类型化值无法从会话回放重建,并可能耗尽进程或 worker 内存;只有外层 `run_code` 输出受 worker 可配置的硬上限约束。每个子调用的持久日志副本则确实有上限:`tools/code-dispatch-log` waterfall 允许 spill 策略把过大的 `tool/code-dispatch` 内容替换为预览加定位符([原理](../../../.agents/notes/implemented/feature/2026-07-26-code-dispatch-log-spill.md))。
- **每次运行都会获得全新的 `run_code` 状态**:MVP 不采用持久 REPL 风格内核(跨调用状态不会出现在日志中);参见 [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)。

View File

@@ -138,7 +138,8 @@ function resolveFlavor(peekRuntime: () => CodeRuntime | undefined): RunCodeFlavo
// resolve an inherited Object.prototype member as a flavor.
const flavor = RUN_CODE_FLAVORS[runtime.language]
if (!Object.hasOwn(RUN_CODE_FLAVORS, runtime.language) || flavor === undefined) {
throw new Error(`dsh-tools: no run_code schema flavor registered for runtime language ${JSON.stringify(runtime.language)}`)
const known = Object.keys(RUN_CODE_FLAVORS).map(name => JSON.stringify(name)).join(', ')
throw new Error(`dsh-tools: no run_code schema flavor registered for runtime language ${JSON.stringify(runtime.language)} (known: ${known})`)
}
return flavor
}

View File

@@ -482,8 +482,18 @@ export function renderToolsSdkPy(schemas: ToolSdkSchema[]): string {
const argType = renderType(schema.parameters, `${camelCase(schema.name)}Args`, state)
const outputType = renderType(schema.output, `${camelCase(schema.name)}Output`, state)
if (IDENTIFIER.test(schema.name) && !RESERVED.has(schema.name) && !schema.name.startsWith('_')) {
members.push(...docLines(schema.description, 1))
members.push(`${pad(1)}async def ${schema.name}(self, args: ${argType}) -> ${outputType}: ...`)
// A docstring only documents its method when it is the FIRST statement
// of that method's body. Emitted before the `async def` it would instead
// become the `Tools` class docstring (for the first tool) or a dead
// expression (for every later one), leaving every method undocumented —
// and this SDK is the model's only description of what a tool does. A
// docstring is a complete body, so the `...` stub is only for the
// description-less case.
const doc = docLines(schema.description, 2)
members.push(doc.length > 0
? `${pad(1)}async def ${schema.name}(self, args: ${argType}) -> ${outputType}:`
: `${pad(1)}async def ${schema.name}(self, args: ${argType}) -> ${outputType}: ...`)
members.push(...doc)
statements += 1
} else {
// Not a legal attribute name — the model reaches it via ``tools[name]``.

View File

@@ -381,7 +381,10 @@ describe('mode-aware wire contribution', () => {
// rejects such a language earlier; this reaches the guard on its own.
const { ctx } = await setup({ mode: 'code', runtime: { language: 'ruby' } })
const definition = ctx.tools.get(RUN_CODE_NAME)
expect(() => definition?.description).toThrow(/no run_code schema flavor registered for runtime language "ruby"/)
// Names the known languages, symmetric with the SDK_RENDERERS guard: this
// is the reachable rejection, so it must be at least as diagnosable.
expect(() => definition?.description)
.toThrow(/no run_code schema flavor registered for runtime language "ruby" \(known: "typescript", "python"\)/)
})
it('degrades the run_code flavor to TypeScript when no runtime is mounted (doc-catalog schema harvest)', async () => {

View File

@@ -100,7 +100,7 @@ describe('renderToolsSdkPy', () => {
expect(text).toContain('class Tools(Protocol):')
// The argument object is a named TypedDict, not an opaque dict.
expect(text).toContain('class BashArgs(TypedDict):')
expect(text).toContain('async def bash(self, args: BashArgs) -> str: ...')
expect(text).toContain('async def bash(self, args: BashArgs) -> str:')
// Empty-property tools keep the opaque dict (nothing to name).
expect(text).toContain('# tools["my-mcp.tool"](args: dict[str, Any]) -> str')
expect(text).toContain('# tools["class"](args: dict[str, Any]) -> str')
@@ -130,7 +130,7 @@ describe('renderToolsSdkPy', () => {
expect(text).toContain(' query: str')
expect(text).toContain(' # Max results.')
expect(text).toContain(' limit: NotRequired[float]')
expect(text).toContain('async def search(self, args: SearchArgs) -> str: ...')
expect(text).toContain('async def search(self, args: SearchArgs) -> str:')
// NotRequired is imported because an optional field used it; Any is NOT,
// since every type here is concrete — the import line lists only what ran.
expect(text).toContain('from typing import NotRequired, Protocol, TypedDict')
@@ -327,7 +327,7 @@ describe('renderToolsSdkPy', () => {
output: { type: 'string' },
}
const text = renderToolsSdkPy([tool])
expect(text).toContain('async def weird_fields(self, args: dict[str, Any]) -> str: ...')
expect(text).toContain('async def weird_fields(self, args: dict[str, Any]) -> str:')
expect(text).not.toContain('WeirdFieldsArgs')
})
@@ -394,6 +394,29 @@ describe('renderToolsSdkPy', () => {
expect(text.indexOf('async def bash')).toBeLessThan(text.indexOf('# tools["my-mcp.tool"]'))
})
it('places a docstring as the first statement of its own method body', () => {
// Python attaches a docstring to a function only when it is that
// function's first statement. Above the `async def` the first one would
// document the `Tools` class and every later one would be a dead
// expression, so each method must open its body with its own docstring.
const second: ToolSdkSchema = {
name: 'zzz',
description: 'Second by name.',
parameters: parameterSchemaSpecToJsonSchema({}) as unknown as Record<string, unknown>,
output: { type: 'string' },
}
const lines = renderToolsSdkPy([bash, second]).split('\n')
for (const [name, doc] of [['bash', 'Run a shell command.'], ['zzz', 'Second by name.']]) {
const signature = lines.findIndex(line => line.startsWith(`${' '.repeat(4)}async def ${name}(`))
expect(signature).toBeGreaterThan(-1)
// Ends in `:`, not the `: ...` stub — a docstring IS the whole body.
expect(lines[signature].endsWith(':')).toBe(true)
expect(lines[signature + 1]).toBe(`${' '.repeat(8)}"""${doc}"""`)
}
// No docstring is left floating at class-body indentation.
expect(lines.filter(line => line.startsWith(`${' '.repeat(4)}"""`))).toEqual([])
})
it('orders subscript entries against methods by name, not by member kind', () => {
// `a-tool` sorts before `z`, so the subscript comment must precede the
// method: one ordered stream, not methods-then-comments.