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 .agents/notes/implemented/feature/2026-06-15-code-mode.md
2026-06-15-code-mode.md: d06e4f470e8155cf51b2127fe9b847f56ea2ff51
2026-06-15-code-mode.zh.md: a29000c18553e44842d20ebbec191a3e2fd3b9cc
2026-06-15-code-mode.md: 99bbed3edab32512f88ece9694d6519a1f89c2dd
2026-06-15-code-mode.zh.md: ca1bbe9ed3e412186763d1ed4fca9ed06669d4c3

View File

@@ -6,7 +6,7 @@ English | [中文](2026-06-15-code-mode.zh.md)
## Problem
In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. `ToolRegistry` contributes its schemas to the system-prompt assembly, the assembly's `tools` land on the wire (and in the logged request header), the model invokes one `tool-call` block per step, and the loop dispatches each call through `ctx.tools.execute()` **sequentially** parallel tool execution was an open TODO at the time of this note, and bounded parallel dispatch has since shipped (the [parallel tool-call note](2026-07-10-parallel-tool-call-execution.md); the rolling pool in [docs/architecture.md](../../../../docs/architecture.md)) — with **every** intermediate `tool-result` re-entering the model's context on the next request.
In the registry's native presentation, the agent loop advertises every visible capability as a JSON-schema function definition. `ToolRegistry` contributes its schemas to the system-prompt assembly, the assembly's `tools` land on the wire (and in the logged request header), the model invokes one `tool-call` block per step, and at the time of this note the loop dispatched each call through `ctx.tools.execute()` **sequentially** (parallel tool execution was an open TODO then; bounded parallel dispatch has since shipped the [parallel tool-call note](2026-07-10-parallel-tool-call-execution.md), the rolling pool in [docs/architecture.md](../../../../docs/architecture.md)) — with **every** intermediate `tool-result` re-entering the model's context on the next request.
For multi-step tool work this is token-heavy and serial. The model cannot compose tools — loop over a result set, branch on an intermediate value, fan out, post-process — without a full model round-trip per call, and each round-trip drags the entire intermediate result back into context whether the model needs it or not.

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
在注册表的原生呈现方式下agent loop智能体循环将每个可见能力以 JSON Schema 函数定义的形式通告给模型。`ToolRegistry` 将其 schema 贡献给系统提示词组装,组装结果中的 `tools` 落到协议格式wire format也记录在请求头日志中模型每步调用一个 `tool-call` 块,循环通过 `ctx.tools.execute()` **逐个**分发每次调用——并行工具执行在本 note 写作时还是 open TODO此后有界的并行分发已经交付见[并行工具调用 note](2026-07-10-parallel-tool-call-execution.md),以及 [docs/architecture.md](../../../../docs/architecture.md) 中的 rolling pool——且**每一个**中间 `tool-result` 都会在下一次请求时重新进入模型上下文。
在注册表的原生呈现方式下agent loop智能体循环将每个可见能力以 JSON Schema 函数定义的形式通告给模型。`ToolRegistry` 将其 schema 贡献给系统提示词组装,组装结果中的 `tools` 落到协议格式wire format也记录在请求头日志中模型每步调用一个 `tool-call` 块,而在本 note 写作时,循环通过 `ctx.tools.execute()` **逐个**分发每次调用并行工具执行时还是 open TODO此后有界的并行分发已经交付——见[并行工具调用 note](2026-07-10-parallel-tool-call-execution.md),以及 [docs/architecture.md](../../../../docs/architecture.md) 中的 rolling pool——且**每一个**中间 `tool-result` 都会在下一次请求时重新进入模型上下文。
对于多步工具操作,这种方式 token 开销大且串行。模型无法组合工具——遍历结果集、根据中间值分支、扇出、后处理——每次调用都需要一次完整的模型往返,而每次往返都会把完整的中间结果拖回上下文,不管模型是否需要。

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 .agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md
2026-07-31-code-mode-language-dispatch.md: e2d063eb5efc42f3079864479cf869ba4643bff1
2026-07-31-code-mode-language-dispatch.zh.md: d911a43936cb0865533951de3dee845d135a22ca
2026-07-31-code-mode-language-dispatch.md: d1fb598e22926eb017f7d3e2a3d1cb14870d4f4d
2026-07-31-code-mode-language-dispatch.zh.md: b5fc8b660c32b3ebdd8eef79439d4dedeb75b0c9

View File

@@ -23,7 +23,7 @@ Both tables are read with `Object.hasOwn` before use so a language named `toStri
### The Python SDK renderer
`py-types.ts` renders the same unified tool-schema vocabulary `jsonSchemaToTs` covers, targeting Python: `jsonSchemaToPy` emits a type expression per JSON-schema node, and `renderToolsSdkPy` assembles named `TypedDict`s for each visible tool's arguments and canonical output plus a `tools` object with usage instructions equivalent to the TypeScript flavor. Unsupported raw constructs degrade rather than throwing during assembly, matching the TypeScript renderer's contract. The output is deterministic — lexicographic tool order, byte-identical text for an unchanged tool set — so the prompt stays prefix-cache-friendly.
`py-types.ts` renders the same unified tool-schema vocabulary `jsonSchemaToTs` covers, targeting Python: `jsonSchemaToPy` emits a type expression per JSON-schema node, and `renderToolsSdkPy` assembles named `TypedDict`s for each visible tool's arguments and canonical output plus a `tools` object with usage instructions equivalent to the TypeScript flavor. Unsupported raw constructs degrade rather than throwing during assembly, matching the TypeScript renderer's contract. The output is deterministic — lexicographic tool order, byte-identical text for an unchanged tool set — so the prompt stays prefix-cache-friendly. Lexicographic means one ordered member stream: a tool whose name is not a legal attribute is listed as a `tools[name]` comment in its sorted position rather than partitioned to the end, matching how the TypeScript flavor quotes an exotic key in place. Two Python-specific placements follow from that: a description becomes the method's docstring emitted as the FIRST statement of its body (above the `async def` the first one would document the `Tools` class and the rest would be dead expressions, leaving every method undocumented), and because comment lines are not statements, a tool set with no method at all still needs an explicit `pass`.
`renderType` validates the whole schema once (`assertSupportedJsonSchema`) and then trusts it, wrapping the walk in one `try/catch` that degrades to `Any` — the same trusted-after-validation stance the sibling `ts-types` renderer takes at this typed same-process seam ([Trust TypeScript at typed same-process seams](../../../../AGENTS.md)). It deliberately carries NO defenses against a schema whose accessors mutate between reads (post-validation cycles, TOCTOU on `const`/`enum`, self-referential functions): the input is a first-party registration (a `defineTool` literal or a raw registration) or a wire-derived plain JSON schema — the former is trusted per AGENTS.md, the latter is a `JSON.parse` product that physically cannot carry accessors, and `renderType` re-validates the whole tree on every call regardless — so such inputs are unreachable, and adding per-shape guards here would break symmetry with `ts-types` (which has none) for values the static interface forbids. `jsonSchemaToPy(schema: unknown)` accepts `unknown` and returns `Any` on a malformed schema — the Python counterpart of the TS flavor's `unknown` — but its contract is "degrade an unsupported schema", not "survive an adversarial mutating one".

View File

@@ -23,7 +23,7 @@ Code Mode 只生成一种 SDK 形态TypeScript。`ToolRegistry` 为 `tools:sd
### Python SDK 渲染器
`py-types.ts` 渲染 `jsonSchemaToTs` 所覆盖的同一套统一工具 schema 词汇,目标为 Python`jsonSchemaToPy` 为每个 JSON-schema 节点发出一个类型表达式,`renderToolsSdkPy` 为每个可见工具的参数与规范输出装配具名 `TypedDict`,再加一个带用法说明的 `tools` 对象,与 TypeScript 形态等价。不支持的原始构造在装配时降级而非抛错,与 TypeScript 渲染器的契约一致。输出是确定性的——工具按字典序排列,工具集不变时文本逐字节相同——因此 prompt 保持 prefix-cache 友好。
`py-types.ts` 渲染 `jsonSchemaToTs` 所覆盖的同一套统一工具 schema 词汇,目标为 Python`jsonSchemaToPy` 为每个 JSON-schema 节点发出一个类型表达式,`renderToolsSdkPy` 为每个可见工具的参数与规范输出装配具名 `TypedDict`,再加一个带用法说明的 `tools` 对象,与 TypeScript 形态等价。不支持的原始构造在装配时降级而非抛错,与 TypeScript 渲染器的契约一致。输出是确定性的——工具按字典序排列,工具集不变时文本逐字节相同——因此 prompt 保持 prefix-cache 友好。字典序意味着单一有序的成员流:名字不是合法属性的工具以 `tools[name]` 注释出现在它排序后的位置上,而不是被分拣到末尾,与 TypeScript 形态就地为异常键加引号的做法一致。由此带来两处 Python 特有的位置约定:描述会成为方法的 docstring且必须作为方法体的**第一条语句**发出(放在 `async def` 之上,第一条会变成 `Tools` 的类文档、其余都是无效果表达式,导致每个方法都没有文档);而注释行不是语句,所以一个没有任何方法的工具集仍需显式 `pass`
`renderType` 先用 `assertSupportedJsonSchema` 整树校验一次、随后信任它,用单个 `try/catch` 把整个遍历兜住并降级为 `Any`——与姊妹渲染器 `ts-types` 在这个 typed 同进程 seam 上采取的「校验后信任」姿态一致([Trust TypeScript at typed same-process seams](../../../../AGENTS.md))。它有意不设任何针对「访问器在多次读取间变值」的防御(校验后成环、`const`/`enum` 的 TOCTOU、自引用函数输入是第一方注册`defineTool` 字面量或 raw 注册)或从 wire 桥接而来的纯 JSON——前者按 AGENTS.md 受信任,后者是 `JSON.parse` 产物、物理上不可能携带访问器,且每次调用 `renderType` 都会整树重新校验——这类输入不可达,而在此加逐形态守卫会为静态接口所禁止的值破坏与 `ts-types`(没有这类守卫)的对称。`jsonSchemaToPy(schema: unknown)` 接受 `unknown` 并对畸形 schema 返回 `Any`——TypeScript 形态 `unknown` 的对应物——但它的契约是「降级不支持的 schema」而非「扛住对抗性的可变 schema」。

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.