diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml b/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml index bf428d8ae2..bc05e497c5 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.md b/.agents/notes/implemented/feature/2026-06-15-code-mode.md index d06e4f470e..99bbed3eda 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.md +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.md @@ -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. diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md b/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md index a29000c185..ca1bbe9ed3 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md @@ -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 开销大且串行。模型无法组合工具——遍历结果集、根据中间值分支、扇出、后处理——每次调用都需要一次完整的模型往返,而每次往返都会把完整的中间结果拖回上下文,不管模型是否需要。 diff --git a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml index bffb432e93..3830e60848 100644 --- a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md index e2d063eb5e..d1fb598e22 100644 --- a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md +++ b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.md @@ -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". diff --git a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md index d911a43936..b5fc8b660c 100644 --- a/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-code-mode-language-dispatch.zh.md @@ -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」。 diff --git a/packages/code-runtime/README.i18n.yaml b/packages/code-runtime/README.i18n.yaml index d8eebec7aa..ebce8bc53a 100644 --- a/packages/code-runtime/README.i18n.yaml +++ b/packages/code-runtime/README.i18n.yaml @@ -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 diff --git a/packages/code-runtime/README.md b/packages/code-runtime/README.md index dbe6b37ffa..4ee441bf99 100644 --- a/packages/code-runtime/README.md +++ b/packages/code-runtime/README.md @@ -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 | |---|---|---| diff --git a/packages/code-runtime/README.zh.md b/packages/code-runtime/README.zh.md index a5acbad3cc..8a0d47fff4 100644 --- a/packages/code-runtime/README.zh.md +++ b/packages/code-runtime/README.zh.md @@ -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 键 | |---|---|---| diff --git a/packages/core/tools/README.i18n.yaml b/packages/core/tools/README.i18n.yaml index e27951bac0..fb90efa1db 100644 --- a/packages/core/tools/README.i18n.yaml +++ b/packages/core/tools/README.i18n.yaml @@ -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 diff --git a/packages/core/tools/README.md b/packages/core/tools/README.md index f561a08bbc..20df93e734 100644 --- a/packages/core/tools/README.md +++ b/packages/core/tools/README.md @@ -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). diff --git a/packages/core/tools/README.zh.md b/packages/core/tools/README.zh.md index 7318b13a66..d16a8a90c6 100644 --- a/packages/core/tools/README.zh.md +++ b/packages/core/tools/README.zh.md @@ -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)。 diff --git a/packages/core/tools/src/code-mode.ts b/packages/core/tools/src/code-mode.ts index 3c8e8ca024..7132ca3646 100644 --- a/packages/core/tools/src/code-mode.ts +++ b/packages/core/tools/src/code-mode.ts @@ -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 } diff --git a/packages/core/tools/src/py-types.ts b/packages/core/tools/src/py-types.ts index 25cb007fce..9f08d4dc3f 100644 --- a/packages/core/tools/src/py-types.ts +++ b/packages/core/tools/src/py-types.ts @@ -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]``. diff --git a/packages/core/tools/tests/code-mode.spec.ts b/packages/core/tools/tests/code-mode.spec.ts index bbec3e5b26..933881fd50 100644 --- a/packages/core/tools/tests/code-mode.spec.ts +++ b/packages/core/tools/tests/code-mode.spec.ts @@ -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 () => { diff --git a/packages/core/tools/tests/py-types.spec.ts b/packages/core/tools/tests/py-types.spec.ts index 4b42b1c630..c829801efd 100644 --- a/packages/core/tools/tests/py-types.spec.ts +++ b/packages/core/tools/tests/py-types.spec.ts @@ -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, + 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.