Merge commit '70396085b141370ce32de1be4e225b4384eaf46d' into HEAD
# Conflicts: # .agents/notes/implemented/feature/2026-07-07-mcp-client-plugin.i18n.yaml # .agents/notes/implemented/feature/2026-07-20-code-mode-typed-tool-returns.i18n.yaml # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.i18n.yaml # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md # .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md # .agents/notes/implemented/simplification/2026-07-23-acp-automation-only-protocol.i18n.yaml # docs/config-catalog.i18n.yaml # docs/module-graph.i18n.yaml # docs/module-graph.md # docs/module-graph.zh.md # docs/tool-catalog.i18n.yaml # docs/tool-catalog.md # docs/tool-catalog.zh.md # examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.expected.json # examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.expected.json # examples/acp-agent/tests/snapshots/code-mode-turn/tool-schemas.expected.json # packages/core/tools/README.i18n.yaml # packages/core/tools/README.zh.md # packages/core/tools/src/code-mode.ts # packages/host/apiproxy/tests/api-proxy-models.spec.ts # packages/host/plugin-inventory/tests/inventory.spec.ts # packages/mcp/mcp-client/tests/mcp-client.e2e.ts # packages/mcp/mcp-client/tests/mcp-client.spec.ts # packages/self-modification/tool-cordis/src/api-catalog.ts # packages/test-support/acp-snapshot/README.i18n.yaml # pnpm-lock.yaml
This commit is contained in:
@@ -6,7 +6,7 @@ import type { Scope } from '@deepseek-ai/dsh-scope'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
|
||||
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
|
||||
import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineContentToolFixture, defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime, { CodeRunFailedError, RUN_CODE_NAME, TOOL_ABORTED_BEFORE_DISPATCH, defineContentToolFixture, defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { Config, JsonSchemaNode, PostToolDecision, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -19,7 +19,7 @@ const testToolSignal = new AbortController().signal
|
||||
* misconfiguration rejections, the run_code dispatch bridge (serialization,
|
||||
* abort, JSON normalization, error mapping, events, quiescence), and HMR
|
||||
* safety — all against an in-repo fake runtime, exactly the
|
||||
* Service Definition / Service provider / Consumer roles the seam promises.
|
||||
* Service Definition / Service Provider / Consumer roles the seam promises.
|
||||
*/
|
||||
|
||||
/** A scriptable in-repo CodeRuntime: each test sets `behavior` to drive the bindings however it needs. */
|
||||
@@ -50,7 +50,7 @@ interface SetupOptions {
|
||||
async function setup(options: SetupOptions = {}) {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, { ...options.toolOrder ? { toolOrder: options.toolOrder } : {} })
|
||||
await ctx.plugin(ToolRegistry, { mode: options.mode ?? 'code', ...options.maxParallelSubCalls !== undefined ? { maxParallelSubCalls: options.maxParallelSubCalls } : {} })
|
||||
await ctx.plugin(ToolRuntime, { mode: options.mode ?? 'code', ...options.maxParallelSubCalls !== undefined ? { maxParallelSubCalls: options.maxParallelSubCalls } : {} })
|
||||
let runtime: FakeRuntime | undefined
|
||||
if (options.runtime !== false) {
|
||||
await ctx.plugin(FakeRuntime, options.runtime ?? {})
|
||||
@@ -399,6 +399,10 @@ describe('mode-aware wire contribution', () => {
|
||||
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
|
||||
expect(runCodeSchema?.description).toContain('Execute a TypeScript program')
|
||||
expect(runCodeSchema?.description).toContain('BODY of an')
|
||||
// Both required arguments are named here, not only in the parameter
|
||||
// schema: prose that describes the call as "pass the program" is what
|
||||
// leads a model to emit `{code}` alone and fail INVALID_ARGS.
|
||||
expect(runCodeSchema?.description).toContain('`description`')
|
||||
const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code
|
||||
expect(codeParam.description).toBe('The program: the body of an async TypeScript function.')
|
||||
})
|
||||
@@ -410,6 +414,7 @@ describe('mode-aware wire contribution', () => {
|
||||
const runCodeSchema = assembly.tools.find(tool => tool.name === RUN_CODE_NAME)
|
||||
expect(runCodeSchema?.description).toContain('Execute a Python program')
|
||||
expect(runCodeSchema?.description).toContain('`return <value>`')
|
||||
expect(runCodeSchema?.description).toContain('`description`')
|
||||
expect(runCodeSchema?.description).not.toContain('TypeScript')
|
||||
const codeParam = (runCodeSchema?.parameters as { properties: { code: { description: string } } }).properties.code
|
||||
expect(codeParam.description).toBe('The program: the body of an async Python function.')
|
||||
@@ -456,7 +461,7 @@ describe('mode-aware wire contribution', () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, {})
|
||||
await ctx.plugin(FakeRuntime, {})
|
||||
const fiber = await ctx.plugin(ToolRegistry, { mode: 'code' })
|
||||
const fiber = await ctx.plugin(ToolRuntime, { mode: 'code' })
|
||||
expect(ctx.tools.get(RUN_CODE_NAME)).toBeDefined()
|
||||
await fiber.dispose()
|
||||
const assembly = await ctx.systemPrompt.assemble()
|
||||
@@ -1283,7 +1288,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
it('executing run_code under a missing runtime is a structured isError, not a crash', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, {})
|
||||
await ctx.plugin(ToolRegistry, { mode: 'code' })
|
||||
await ctx.plugin(ToolRuntime, { mode: 'code' })
|
||||
const result = await runCode(ctx, 'program')
|
||||
expect(result.isError).toBe(true)
|
||||
expect((result.content[0] as { text: string }).text).toContain('requires a code runtime')
|
||||
@@ -1635,21 +1640,21 @@ describe('the run_code dispatch bridge', () => {
|
||||
it('direct construction rejects a non-positive parallel sub-call cap at load', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, {})
|
||||
expect(() => new ToolRegistry(ctx, { mode: 'code', maxParallelSubCalls: 0 }))
|
||||
expect(() => new ToolRuntime(ctx, { mode: 'code', maxParallelSubCalls: 0 }))
|
||||
.toThrow('maxParallelSubCalls must be a positive integer')
|
||||
})
|
||||
|
||||
it('direct construction in code mode defaults the parallel sub-call cap', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, {})
|
||||
const registry = new ToolRegistry(ctx, { mode: 'code' })
|
||||
const registry = new ToolRuntime(ctx, { mode: 'code' })
|
||||
expect(registry.get(RUN_CODE_NAME)).toBeDefined()
|
||||
})
|
||||
|
||||
it('defaults to native mode under direct construction with no config', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, {})
|
||||
const registry = new ToolRegistry(ctx)
|
||||
const registry = new ToolRuntime(ctx)
|
||||
expect(registry.get(RUN_CODE_NAME)).toBeUndefined()
|
||||
const assembly = await ctx.systemPrompt.assemble()
|
||||
expect(assembly.sections.some(section => section.name === 'tools:sdk')).toBe(false)
|
||||
@@ -1657,7 +1662,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
it('denies a model-direct native-tool call under code mode as UNKNOWN_TOOL', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, {})
|
||||
const registry = new ToolRegistry(ctx, { mode: 'code' })
|
||||
const registry = new ToolRuntime(ctx, { mode: 'code' })
|
||||
registerEcho(ctx, 'write')
|
||||
const result = await registry.execute({
|
||||
signal: testToolSignal,
|
||||
@@ -1677,7 +1682,7 @@ describe('the run_code dispatch bridge', () => {
|
||||
it('routes a pre-aborted collapsed call through ABORTED_BEFORE_DISPATCH', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, {})
|
||||
const registry = new ToolRegistry(ctx, { mode: 'code' })
|
||||
const registry = new ToolRuntime(ctx, { mode: 'code' })
|
||||
registerEcho(ctx, 'write')
|
||||
const aborted = new AbortController()
|
||||
aborted.abort()
|
||||
@@ -1748,7 +1753,7 @@ describe('per-agent presentation', () => {
|
||||
// `native` here, so a collapse predicate reading it instead of this
|
||||
// scope's effective mode would announce [run_code] and still execute the
|
||||
// native call — the bypass, reopened for exactly the preset composition
|
||||
// `dsh-agent-tool-mode` produces.
|
||||
// `dsh-agent-tool-presentation` produces.
|
||||
expect(ctx.tools.executionMode({
|
||||
signal: testToolSignal,
|
||||
callId: CallId('preset-coded-schedule'),
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, expectTypeOf, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, {
|
||||
import ToolRuntime, {
|
||||
defineContentToolFixture,
|
||||
type ToolDefinition,
|
||||
type ToolExecutionInput,
|
||||
@@ -16,7 +16,7 @@ const testToolSignal = new AbortController().signal
|
||||
async function setup() {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ function exec(name: string, args: unknown): ToolExecutionInput {
|
||||
return { signal: testToolSignal, callId: CallId('c1'), name, arguments: args }
|
||||
}
|
||||
|
||||
describe('ToolRegistry.executionMode', () => {
|
||||
describe('ToolRuntime.executionMode', () => {
|
||||
it('returns parallel only for an explicit true classifier', async () => {
|
||||
const ctx = await setup()
|
||||
ctx.tools.register(defineContentToolFixture({
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
assertManifestComplete,
|
||||
assertToolsHarvested,
|
||||
collectToolCatalog,
|
||||
render,
|
||||
type ToolCatalog,
|
||||
type ToolPackage,
|
||||
} from '../../../../scripts/gen-tool-catalog.ts'
|
||||
|
||||
/** JSON Schema shape enough to reach the values AST extraction can't. */
|
||||
@@ -23,7 +25,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
|
||||
it('boots every shipped tool package and harvests its model-facing schemas', async () => {
|
||||
const catalog = await collectToolCatalog()
|
||||
const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort()
|
||||
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'task_kill', 'task_list', 'task_output', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
|
||||
expect(names).toEqual(['ask_user_question', 'bash', 'bash', 'cordis_define', 'cordis_inspect_list', 'cordis_inspect_query', 'cordis_inspect_self', 'cordis_run', 'cordis_stop', 'cordis_undefine', 'create_goal', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'lsp', 'pwsh', 'ralph', 'read', 'read_image', 'report', 'run_code', 'schedule_create', 'schedule_delete', 'schedule_list', 'send_message', 'session_event_read', 'session_event_search', 'session_event_trace', 'session_search', 'session_trace', 'skill', 'str_replace_editor', 'subagent', 'terminal_close', 'terminal_list', 'terminal_open', 'terminal_read', 'terminal_send', 'terminal_signal', 'todo_write', 'update_goal', 'web_fetch', 'web_search', 'workflow', 'write'])
|
||||
// Every tool carries a JSON-Schema `parameters` object (what the model sees).
|
||||
for (const entry of catalog) {
|
||||
for (const schema of entry.schemas) {
|
||||
@@ -46,7 +48,7 @@ describe('gen-tool-catalog collectToolCatalog', () => {
|
||||
it('attributes each harvested tool with its registering plugin source', async () => {
|
||||
const catalog = await collectToolCatalog()
|
||||
const bash = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-bash')
|
||||
expect(bash?.sources.bash).toBe('packages/bash/tool-bash/src/index.ts')
|
||||
expect(bash?.sources.bash).toBe('packages/shell/tool-bash/src/index.ts')
|
||||
const control = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-subagent-control')
|
||||
expect(control?.sources).toEqual({
|
||||
interrupt_agent: 'packages/subagent/tool-subagent-control/src/index.ts',
|
||||
@@ -91,6 +93,29 @@ describe('gen-tool-catalog assertManifestComplete', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('gen-tool-catalog assertToolsHarvested', () => {
|
||||
const entry: ToolPackage = {
|
||||
pkg: '@deepseek-ai/dsh-tool-demo',
|
||||
dir: 'tool-demo',
|
||||
source: 'packages/demo/tool-demo/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.somethingUnmounted'],
|
||||
writes: ['tool/result'],
|
||||
mount: () => Promise.resolve(),
|
||||
}
|
||||
|
||||
it('accepts a boot that registered at least one tool', () => {
|
||||
expect(() => { assertToolsHarvested(entry, 1) }).not.toThrow()
|
||||
})
|
||||
|
||||
it('throws, naming the package and its requirements, when a boot registers nothing', () => {
|
||||
// The failure this guards is silent by construction: the package is in the
|
||||
// manifest, its plugin merely stays PENDING on an unmounted service, and the
|
||||
// catalog would ship without its tools while every gate stays green.
|
||||
expect(() => { assertToolsHarvested(entry, 0) }).toThrow(/@deepseek-ai\/dsh-tool-demo booted without registering a single tool/)
|
||||
expect(() => { assertToolsHarvested(entry, 0) }).toThrow(/ctx.somethingUnmounted/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('gen-tool-catalog render', () => {
|
||||
it('emits a package heading, a tool heading, and a json schema fence', () => {
|
||||
const catalog: ToolCatalog = [
|
||||
|
||||
@@ -5,14 +5,14 @@ import { CallId } from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type { ToolExecution, ToolExecutionResult, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
|
||||
import * as ToolsInvariant from '@deepseek-ai/dsh-tools/invariant'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
import InvariantRegistry from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const testToolSignal = new AbortController().signal
|
||||
|
||||
async function setup(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SessionStore)
|
||||
await ctx.plugin(InvariantService)
|
||||
await ctx.plugin(InvariantRegistry)
|
||||
await ctx.plugin(ToolsInvariant)
|
||||
return ctx
|
||||
}
|
||||
@@ -219,7 +219,7 @@ describe('tool-pipeline invariants', () => {
|
||||
content: [{ type: 'text', text: 'ok' }],
|
||||
})
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
await ctx.plugin(InvariantService)
|
||||
await ctx.plugin(InvariantRegistry)
|
||||
await expect(ctx.plugin(ToolsInvariant).then(() => undefined)).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -233,7 +233,7 @@ describe('tool-pipeline invariants', () => {
|
||||
name: 'echo',
|
||||
arguments: {},
|
||||
})
|
||||
await ctx.plugin(InvariantService)
|
||||
await ctx.plugin(InvariantRegistry)
|
||||
await expect(ctx.plugin(ToolsInvariant).then(() => undefined)).rejects.toThrow(/outside any open turn/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -166,6 +166,15 @@ describe('renderToolsSdkPy', () => {
|
||||
expect(text).toContain('tools: Tools')
|
||||
})
|
||||
|
||||
it('names both required call arguments, not just the program', () => {
|
||||
// The schema requires `code` AND `description`; instructions that mention
|
||||
// only the program let a model emit `{code}` alone and fail INVALID_ARGS.
|
||||
const text = renderToolsSdkPy([bash])
|
||||
expect(text).toContain('`code`')
|
||||
expect(text).toContain('`description`')
|
||||
expect(text).toContain('two required arguments')
|
||||
})
|
||||
|
||||
it('renders required as plain fields and optional as NotRequired, with per-field description comments', () => {
|
||||
const tool: ToolSdkSchema = {
|
||||
name: 'search',
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Events } from '@deepseek-ai/cordis'
|
||||
import { bindScopeParent, createScope } from '@deepseek-ai/dsh-scope'
|
||||
import type { Scope } from '@deepseek-ai/dsh-scope'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry from '@deepseek-ai/dsh-tools'
|
||||
import ToolRuntime from '@deepseek-ai/dsh-tools'
|
||||
import type { PreToolDecision, ToolDefinition, ToolExecution, ToolExecutionInput, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
|
||||
@@ -17,7 +17,7 @@ const testToolSignal = new AbortController().signal
|
||||
async function mount(): Promise<Context> {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt, {})
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
return ctx
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createUserMessage, CallId, HarnessError, type ContentBlock } from '@de
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import ApprovalService, { type ApprovalOutcome, type ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
|
||||
import ToolRegistry, {
|
||||
import ToolRuntime, {
|
||||
defineContentToolFixture, defineTool, JsonSchemaError, parameterSchemaSpecToJsonSchema, validateArgs, ToolArgsError, ToolNotFoundError,
|
||||
TOOL_ABORTED, TOOL_ABORTED_BEFORE_DISPATCH,
|
||||
type InferArgs, type JsonValue, type ParameterSchemaSpec, type PreToolDecision, type PostToolDecision,
|
||||
@@ -16,7 +16,7 @@ const testToolSignal = new AbortController().signal
|
||||
async function setup() {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(SystemPrompt)
|
||||
await ctx.plugin(ToolRegistry)
|
||||
await ctx.plugin(ToolRuntime)
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const echoTool = defineTool({
|
||||
},
|
||||
})
|
||||
|
||||
describe('ToolRegistry', () => {
|
||||
describe('ToolRuntime', () => {
|
||||
it('registers tools, exposes schemas, and feeds the system-prompt assembly', async () => {
|
||||
const ctx = await setup()
|
||||
ctx.tools.register(echoTool)
|
||||
@@ -2449,7 +2449,7 @@ describe('schema DSL optional and nested contracts', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('ToolRegistry.get', () => {
|
||||
describe('ToolRuntime.get', () => {
|
||||
it('get() returns the registered tool definition', async () => {
|
||||
const ctx = await setup()
|
||||
ctx.tools.register(echoTool)
|
||||
|
||||
@@ -148,6 +148,15 @@ describe('renderToolsSdk', () => {
|
||||
expect(text).toContain('lossless JSON')
|
||||
})
|
||||
|
||||
it('names both required call arguments, not just the program', () => {
|
||||
// The schema requires `code` AND `description`; instructions that mention
|
||||
// only the program let a model emit `{code}` alone and fail INVALID_ARGS.
|
||||
const text = renderToolsSdk([bash])
|
||||
expect(text).toContain('`code`')
|
||||
expect(text).toContain('`description`')
|
||||
expect(text).toContain('two required arguments')
|
||||
})
|
||||
|
||||
it('is deterministic: same tool set, byte-identical text regardless of input order', () => {
|
||||
expect(renderToolsSdk([bash, exotic])).toBe(renderToolsSdk([exotic, bash]))
|
||||
// Equal names sort stably (the comparator's equal arm).
|
||||
|
||||
Reference in New Issue
Block a user