Merge origin/master into lsp

This commit is contained in:
Dudu-0223
2026-07-20 13:54:38 +08:00
1481 changed files with 93501 additions and 21297 deletions

View File

@@ -8,13 +8,12 @@ import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import ToolRegistry, { CodeRunFailedError, RUN_CODE_NAME, defineTool } from '@deepseek-ai/dsh-tools'
import type { Config, PostToolDecision, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
import { AgentId } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import type { SessionEventMap } from '@deepseek-ai/dsh-session'
/**
* Code Mode unit tier (per the RFC's plan): provider contribution per mode,
* Code Mode unit tier (per the Agent Note's plan): provider contribution per mode,
* 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
@@ -59,7 +58,7 @@ async function setup(options: SetupOptions = {}) {
/** Mint one production-shaped agent scope that can register scoped tool policy. */
async function mintAgentScope(ctx: Context, name = 'scoped'): Promise<{ scope: Scope; agent: Agent }> {
const agent = { id: AgentId(name) } as Agent
const agent = { id: SessionId(name) } as Agent
let scope!: Scope
await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, agent) },
{ inject: ['tools', 'systemPrompt'] }))
@@ -82,10 +81,11 @@ function registerEcho(ctx: Context, name = 'echo'): unknown[] {
}
/** A structural fake of the owning agent: captures session appends. */
function fakeAgent(): { agent: Agent; events: { type: string; data: unknown }[] } {
function fakeAgent(options: { cwd?: string } = { cwd: '/workspace' }): { agent: Agent; events: { type: string; data: unknown }[] } {
const events: { type: string; data: unknown }[] = []
const agent = {
session: {
header: options.cwd === undefined ? {} : { cwd: options.cwd },
append: (type: string, data: unknown) => { events.push({ type, data }) },
},
} as unknown as Agent
@@ -477,27 +477,71 @@ describe('the run_code dispatch bridge', () => {
expect(dispatch.arguments).toEqual({ value: 'x', when: '1970-01-01T00:00:00.000Z' })
})
it('suppresses sub-call additionalContext (deliberately; pinned)', async () => {
it('defers sub-call additionalContexts onto the outer run_code result', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
registerEcho(ctx)
ctx.on('tools/post-execute', (exec, _result, next): Promise<PostToolDecision> => {
if (exec.name === 'echo') {
return Promise.resolve({
kind: 'accept' as const,
additionalContext: { content: [{ type: 'text' as const, text: 'context for the next request' }], source: { kind: 'plugin' as const, plugin: 'test' } },
additionalContexts: [{
content: [{ type: 'text' as const, text: `context for ${exec.callId}` }],
source: { kind: 'plugin' as const, plugin: 'test' },
envelope: 'raw' as const,
meta: { callId: exec.callId },
}],
})
}
return next()
})
runtime.behavior = async (request) => {
await request.bindings[0]!.functions.echo!({ value: 'x' })
await request.bindings[0]!.functions.echo!({ value: 'y' })
return { logs: [], value: 'done' }
}
const result = await runCode(ctx, 'program')
expect(result.isError).toBe(false)
// The sub-call's context has no safe outlet mid-run; the parent result
// must not carry it either.
expect(result.additionalContext).toBeUndefined()
expect(result.additionalContexts).toEqual([
{
content: [{ type: 'text', text: 'context for call-1:code:1' }],
source: { kind: 'plugin', plugin: 'test' },
envelope: 'raw',
meta: { callId: 'call-1:code:1' },
},
{
content: [{ type: 'text', text: 'context for call-1:code:2' }],
source: { kind: 'plugin', plugin: 'test' },
envelope: 'raw',
meta: { callId: 'call-1:code:2' },
},
])
})
it('keeps sub-call contexts when run_code fails after the nested dispatch', async () => {
const { ctx, runtime } = await setup({ mode: 'both' })
registerEcho(ctx)
ctx.on('tools/post-execute', (exec, _result, next): Promise<PostToolDecision> => {
if (exec.name !== 'echo') return next()
return Promise.resolve({
kind: 'accept',
additionalContexts: [{
content: [{ type: 'text', text: 'nested context' }],
source: { kind: 'plugin', plugin: 'test' },
}],
})
})
runtime.behavior = async (request) => {
await request.bindings[0]!.functions.echo!({ value: 'x' })
return { logs: [], error: { kind: 'exception', message: 'program failed later' } }
}
const result = await runCode(ctx, 'program')
expect(result.isError).toBe(true)
expect(result.additionalContexts).toEqual([{
content: [{ type: 'text', text: 'nested context' }],
source: { kind: 'plugin', plugin: 'test' },
}])
})
it('converts a failed run into a structured isError result carrying kind, message, and captured logs', async () => {
@@ -671,6 +715,52 @@ describe('the run_code dispatch bridge', () => {
expect(dispatch.resultSummary.endsWith('…')).toBe(true)
})
it('normalizes the session workspace root before bounding durable result summaries', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
ctx.tools.register(defineTool({
name: 'workspace_path',
description: 'Return a path beneath the session workspace.',
parameters: {},
execute(_args, exec) {
const cwd = exec.agent?.session.header.cwd ?? ''
return Promise.resolve([{ type: 'text' as const, text: `<path>${cwd}/nested/task.txt</path>\n${'x'.repeat(240)}` }])
},
}))
runtime.behavior = async request => ({
logs: [],
value: await request.bindings[0]!.functions.workspace_path!({}),
})
const short = fakeAgent({ cwd: '/tmp/workspace' })
const long = fakeAgent({ cwd: `/tmp/${'long-segment/'.repeat(30)}workspace` })
const shortResult = await runCode(ctx, 'program', { agent: short.agent })
const longResult = await runCode(ctx, 'program', { agent: long.agent })
const shortDispatch = short.events[0]!.data as SessionEventMap['tool/code-dispatch']
const longDispatch = long.events[0]!.data as SessionEventMap['tool/code-dispatch']
expect(shortResult.content).not.toEqual(longResult.content)
expect(shortDispatch.resultSummary).toBe(longDispatch.resultSummary)
expect(shortDispatch.resultSummary).toHaveLength(201)
expect(shortDispatch.resultSummary).toMatch(/^<path>\.\/nested\/task\.txt<\/path>\n.+…$/)
})
it('leaves result summaries unchanged when a session cwd is absent or is the filesystem root', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
registerEcho(ctx)
runtime.behavior = async request => ({
logs: [],
value: await request.bindings[0]!.functions.echo!({ value: '/workspace/value' }),
})
const absent = fakeAgent({})
const root = fakeAgent({ cwd: '/' })
await runCode(ctx, 'program', { agent: absent.agent })
await runCode(ctx, 'program', { agent: root.agent })
expect((absent.events[0]!.data as SessionEventMap['tool/code-dispatch']).resultSummary).toBe('echo:/workspace/value')
expect((root.events[0]!.data as SessionEventMap['tool/code-dispatch']).resultSummary).toBe('echo:/workspace/value')
})
it('rejects undefined, JSON-throwing, and JSON-unrepresentable binding arguments BEFORE dispatch', async () => {
const { ctx, runtime } = await setup({ mode: 'code' })
const calls = registerEcho(ctx)

View File

@@ -0,0 +1,136 @@
/** Covers fail-closed per-call classification and model-schema isolation. */
import { describe, expect, expectTypeOf, it } from 'vitest'
import { Context } from 'cordis'
import { CallId } from '@deepseek-ai/dsh-llm'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, {
defineTool,
type ToolDefinition,
type ToolExecutionInput,
type ToolExecutionMode,
} from '@deepseek-ai/dsh-tools'
async function setup() {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
return ctx
}
function exec(name: string, args: unknown): ToolExecutionInput {
return { callId: CallId('c1'), name, arguments: args }
}
describe('ToolRegistry.executionMode', () => {
it('returns parallel only for an explicit true classifier', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({
name: 'safe',
description: 'parallel-safe',
parameters: {},
isConcurrencySafe: () => true,
async execute() { return [] },
}))
expect(ctx.tools.executionMode(exec('safe', {}))).toEqual({ kind: 'parallel' })
})
it('defaults to exclusive for a tool with no isConcurrencySafe declaration', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({
name: 'plain',
description: 'no declaration',
parameters: {},
async execute() { return [] },
}))
expect(ctx.tools.executionMode(exec('plain', {}))).toEqual({ kind: 'exclusive' })
})
it('returns exclusive for an unknown tool', async () => {
const ctx = await setup()
expect(ctx.tools.executionMode(exec('nonexistent', {}))).toEqual({ kind: 'exclusive' })
})
it('returns exclusive when the classifier returns false for these args', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({
name: 'rw',
description: 'read or write',
parameters: { mode: { type: 'string', required: true } },
isConcurrencySafe: args => args.mode === 'read',
async execute() { return [] },
}))
expect(ctx.tools.executionMode(exec('rw', { mode: 'read' }))).toEqual({ kind: 'parallel' })
expect(ctx.tools.executionMode(exec('rw', { mode: 'write' }))).toEqual({ kind: 'exclusive' })
})
it('classifies invalid defineTool arguments as exclusive without throwing', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({
name: 'needs-mode',
description: 'requires mode',
parameters: { mode: { type: 'string', required: true } },
isConcurrencySafe: () => true,
async execute() { return [] },
}))
expect(ctx.tools.executionMode(exec('needs-mode', {}))).toEqual({ kind: 'exclusive' })
})
it('treats a throwing raw classifier as exclusive', async () => {
const ctx = await setup()
const raw: ToolDefinition = {
name: 'thrower',
description: 'classifier throws',
parameters: { type: 'object', properties: {} },
isConcurrencySafe() { throw new Error('boom') },
async execute() { return [] },
}
ctx.tools.register(raw)
expect(ctx.tools.executionMode(exec('thrower', {}))).toEqual({ kind: 'exclusive' })
})
it('treats a truthy non-boolean raw result as exclusive', async () => {
const ctx = await setup()
const raw = {
name: 'truthy',
description: 'classifier returns a truthy string',
parameters: { type: 'object', properties: {} },
isConcurrencySafe() { return 'yes' },
async execute() { return [] },
} as unknown as ToolDefinition
ctx.tools.register(raw)
expect(ctx.tools.executionMode(exec('truthy', {}))).toEqual({ kind: 'exclusive' })
})
it('passes parsed arguments directly to a raw definition', async () => {
const ctx = await setup()
let seen: unknown
ctx.tools.register({
name: 'raw-safe',
description: 'raw',
parameters: { type: 'object', properties: {} },
isConcurrencySafe(args) { seen = args; return true },
async execute() { return [] },
})
expect(ctx.tools.executionMode(exec('raw-safe', { anything: 1 }))).toEqual({ kind: 'parallel' })
expect(seen).toEqual({ anything: 1 })
})
it('isConcurrencySafe never reaches the model-facing schemas() projection', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({
name: 'safe',
description: 'parallel-safe',
parameters: { x: { type: 'string', required: true } },
isConcurrencySafe: () => true,
async execute() { return [] },
}))
const schema = ctx.tools.schemas()[0] as unknown as Record<string, unknown>
expect(Object.keys(schema).sort()).toEqual(['description', 'name', 'parameters'])
expect(schema.isConcurrencySafe).toBeUndefined()
})
it('ToolExecutionMode is the object-tagged union', () => {
expectTypeOf<ToolExecutionMode>().toEqualTypeOf<{ kind: 'parallel' } | { kind: 'exclusive' }>()
})
})

View File

@@ -23,7 +23,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_kill', 'bash_output', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'edit', 'lsp', 'read', 'run_code', 'skill', 'subagent', 'todo_write', 'web_fetch', 'web_search', 'workflow', 'write'])
expect(names).toEqual(['ask_user_question', 'bash', 'cordis_inspect', 'cordis_mount', 'cordis_unmount', 'edit', 'glob', 'grep', 'lsp', 'read', 'run_code', 'skill', 'subagent', 'task_kill', 'task_list', 'task_output', 'todo_write', '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) {
@@ -49,6 +49,19 @@ describe('gen-tool-catalog collectToolCatalog', () => {
expect(bash?.source).toBe('packages/bash/tool-bash/src/index.ts')
})
it('harvests search tools without depending on the generator process PATH', async () => {
const oldPath = process.env.PATH
try {
process.env.PATH = ''
const catalog = await collectToolCatalog()
const search = catalog.find(entry => entry.pkg === '@deepseek-ai/dsh-tool-fs-search')
expect(search?.schemas.map(s => s.name).sort()).toEqual(['glob', 'grep'])
} finally {
if (oldPath === undefined) delete process.env.PATH
else process.env.PATH = oldPath
}
})
it('records the shipped `subagent_fork` alias in a note (config-driven tool name)', async () => {
// `tool-subagent`'s registered name is the load-time `toolName` config, so the shipped
// agents surface this one package as both `subagent` and `subagent_fork`.

View File

@@ -1,8 +1,8 @@
/**
* Property-based tests for the tool-schema DSL (the property-testing RFC), including
* Property-based tests for the tool-schema DSL (the property-testing Agent Note), including
* the the property-testing ↔ runtime-validation composition composition: generated args that satisfy a SchemaSpec must
* pass validateArgs, and targeted corruptions must be rejected. This closes the
* validator/InferArgs drift risk noted in the arg-validation RFC.
* validator/InferArgs drift risk noted in the arg-validation Agent Note.
*/
import { describe, expect, it } from 'vitest'

View File

@@ -6,9 +6,11 @@ import type { Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import type { PreToolDecision, ToolDefinition, ToolExecution, ToolExecutionInput, ToolExecutionToken } from '@deepseek-ai/dsh-tools'
import type { Agent, AgentId } from '@deepseek-ai/dsh-agent'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { CallId } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { SessionId } from '@deepseek-ai/dsh-session'
/** Mount the registry (with its systemPrompt dependency) on a fresh context. */
async function mount(): Promise<Context> {
@@ -20,7 +22,7 @@ async function mount(): Promise<Context> {
/** Mint a scope whose key doubles as a minimal Agent-like object. */
async function mintAgentScope(ctx: Context, name: string): Promise<{ scope: Scope; key: Agent }> {
const key = { id: name as AgentId } as Agent
const key = { id: name as SessionId } as Agent
let scope!: Scope
// The scoped context resolves services through the MINTING plugin's
// dependency chain — the minter must inject what scope holders will reach
@@ -62,7 +64,7 @@ describe('scoped tool registration', () => {
it('files a scoped tool in its layer: visible/executable for that scope only', async () => {
const ctx = await mount()
const { scope, key } = await mintAgentScope(ctx, 'a')
const other = { id: 'other' as AgentId } as Agent
const other = { id: 'other' as SessionId } as Agent
ctx.tools.register(tool('shared'))
scope.ctx.tools.register(tool('mine'))
@@ -195,7 +197,7 @@ describe('scoped execution dispatch', () => {
it('an agent.ctx pre-execute listener gates only its own agent (and never subject-less calls)', async () => {
const ctx = await mount()
const { scope, key } = await mintAgentScope(ctx, 'a')
const other = { id: 'other' as AgentId } as Agent
const other = { id: 'other' as SessionId } as Agent
ctx.tools.register(tool('t'))
const seen: (string | undefined)[] = []
@@ -213,7 +215,7 @@ describe('scoped execution dispatch', () => {
it('applies scoped guards after pre-execute and unwinds duplicate registrations independently', async () => {
const ctx = await mount()
const { scope, key } = await mintAgentScope(ctx, 'a')
const other = { id: 'other' as AgentId } as Agent
const other = { id: 'other' as SessionId } as Agent
let bodyCalls = 0
ctx.tools.register({
...tool('t'),
@@ -428,7 +430,7 @@ describe('scoped execution dispatch', () => {
it('uses one input snapshot for the normalized error shell', async () => {
const ctx = await mount()
const { scope, key } = await mintAgentScope(ctx, 'accepted')
const driftAgent = { id: 'drift' as AgentId } as Agent
const driftAgent = { id: 'drift' as SessionId } as Agent
ctx.tools.register(tool('parent'))
ctx.tools.register(tool('t'))
let parent!: ToolExecutionToken

View File

@@ -348,7 +348,7 @@ describe('ToolRegistry', () => {
expect(result.content[0]).toMatchObject({ text: 'output rejected: try again' })
})
it('a block decision can ALSO attach additionalContext', async () => {
it('a block decision can ALSO attach additionalContexts', async () => {
const ctx = await setup()
ctx.tools.register(echoTool)
@@ -356,24 +356,95 @@ describe('ToolRegistry', () => {
({
kind: 'block',
feedback: [{ type: 'text', text: 'rejected' }],
additionalContext: { content: [{ type: 'text', text: 'why it was rejected' }], source: { kind: 'plugin', plugin: 'test' } },
additionalContexts: [{ content: [{ type: 'text', text: 'why it was rejected' }], source: { kind: 'plugin', plugin: 'test' } }],
}))
const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } })
expect(result.isError).toBe(true)
expect(result.content[0]).toMatchObject({ text: 'rejected' })
expect(result.additionalContext).toMatchObject({ content: [{ text: 'why it was rejected' }], source: { kind: 'plugin', plugin: 'test' } })
expect(result.additionalContexts).toMatchObject([{ content: [{ text: 'why it was rejected' }], source: { kind: 'plugin', plugin: 'test' } }])
})
it('a post-execute additionalContext rides on the result for the loop to buffer', async () => {
it('post-execute additionalContexts ride on the result for the loop to buffer', async () => {
const ctx = await setup()
ctx.tools.register(echoTool)
ctx.on('tools/post-execute', async (_exec, _result, _next): Promise<PostToolDecision> =>
({ kind: 'accept', additionalContext: { content: [{ type: 'text', text: 'fyi' }], source: { kind: 'plugin', plugin: 'test' } } }))
({ kind: 'accept', additionalContexts: [{ content: [{ type: 'text', text: 'fyi' }], source: { kind: 'plugin', plugin: 'test' } }] }))
const result = await ctx.tools.execute({ callId: CallId('c1'), name: 'echo', arguments: { text: 'hi' } })
expect(result.additionalContext).toMatchObject({ content: [{ text: 'fyi' }], source: { kind: 'plugin', plugin: 'test' } })
expect(result.additionalContexts).toMatchObject([{ content: [{ text: 'fyi' }], source: { kind: 'plugin', plugin: 'test' } }])
})
it('preserves tool-deferred, execute-wrapper, and post-execute contexts in order', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({
name: 'composite',
description: 'composite',
parameters: {},
async execute(_args, exec) {
exec.deferContext({ content: [{ type: 'text', text: 'nested-1' }], source: { kind: 'plugin', plugin: 'nested-1' }, meta: { n: 1 } })
exec.deferContext({ content: [{ type: 'text', text: 'nested-2' }], source: { kind: 'plugin', plugin: 'nested-2' }, envelope: 'raw' })
return [{ type: 'text', text: 'done' }]
},
}))
ctx.on('tools/execute', async (_exec, next) => {
const result = await next()
return {
...result,
additionalContexts: [
...result.additionalContexts ?? [],
{ content: [{ type: 'text', text: 'wrapper' }], source: { kind: 'plugin', plugin: 'wrapper' } },
],
}
})
ctx.on('tools/post-execute', async (_exec, _result, next): Promise<PostToolDecision> => {
const downstream = await next()
return {
...downstream,
additionalContexts: [
{ content: [{ type: 'text', text: 'post' }], source: { kind: 'plugin', plugin: 'post' } },
...downstream.additionalContexts ?? [],
],
}
})
const result = await ctx.tools.execute({ callId: CallId('composite'), name: 'composite', arguments: {} })
expect(result.additionalContexts?.map(context => context.source)).toEqual([
{ kind: 'plugin', plugin: 'nested-1' },
{ kind: 'plugin', plugin: 'nested-2' },
{ kind: 'plugin', plugin: 'wrapper' },
{ kind: 'plugin', plugin: 'post' },
])
expect(result.additionalContexts?.[0]?.meta).toEqual({ n: 1 })
expect(result.additionalContexts?.[1]?.envelope).toBe('raw')
})
it('keeps deferred contexts when a composite tool throws, but drops them when the outer call is blocked', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({
name: 'failing-composite',
description: 'failing composite',
parameters: {},
async execute(_args, exec) {
exec.deferContext({ content: [{ type: 'text', text: 'nested' }], source: { kind: 'plugin', plugin: 'nested' } })
throw new Error('outer failure')
},
}))
const failed = await ctx.tools.execute({ callId: CallId('failed'), name: 'failing-composite', arguments: {} })
expect(failed.isError).toBe(true)
expect(failed.additionalContexts?.map(context => context.source)).toEqual([{ kind: 'plugin', plugin: 'nested' }])
ctx.on('tools/post-execute', async (): Promise<PostToolDecision> => ({
kind: 'block',
feedback: [{ type: 'text', text: 'blocked' }],
additionalContexts: [{ content: [{ type: 'text', text: 'block-only' }], source: { kind: 'plugin', plugin: 'blocker' } }],
}))
const blocked = await ctx.tools.execute({ callId: CallId('blocked'), name: 'failing-composite', arguments: {} })
expect(blocked.isError).toBe(true)
expect(blocked.additionalContexts?.map(context => context.source)).toEqual([{ kind: 'plugin', plugin: 'blocker' }])
})
it('composes pre + post waterfalls around dispatch (sandbox-wrap pattern)', async () => {
@@ -532,25 +603,25 @@ describe('ToolRegistry', () => {
expect(result.content[0]).toMatchObject({ text: 'short-circuited' })
})
it('preserves additionalContext supplied by an around-dispatch result', async () => {
it('preserves additionalContexts supplied by an around-dispatch result', async () => {
const ctx = await setup()
ctx.tools.register(echoTool)
ctx.on('tools/execute', async () => ({
content: [{ type: 'text', text: 'short-circuited with context' }],
isError: false,
additionalContext: {
additionalContexts: [{
content: [{ type: 'text', text: 'from around dispatch' }],
source: { kind: 'plugin', plugin: 'test' },
},
}],
}))
const result = await ctx.tools.execute({
callId: CallId('around-context'), name: 'echo', arguments: {},
})
expect(result.additionalContext).toEqual({
expect(result.additionalContexts).toEqual([{
content: [{ type: 'text', text: 'from around dispatch' }],
source: { kind: 'plugin', plugin: 'test' },
})
}])
})
it('returns an isError result when a tools/execute listener throws', async () => {
@@ -1103,7 +1174,7 @@ describe('ToolRegistry.get', () => {
})
})
describe('validateArgs (the runtime-validation RFC, part 1)', () => {
describe('validateArgs (the runtime-validation Agent Note, part 1)', () => {
it('returns [] for valid args and is total over malformed input', () => {
const spec = {
path: { type: 'string', required: true },
@@ -1203,7 +1274,7 @@ describe('validateArgs (the runtime-validation RFC, part 1)', () => {
})
})
describe('defineTool validation (the runtime-validation RFC, part 1)', () => {
describe('defineTool validation (the runtime-validation Agent Note, part 1)', () => {
it('returns an isError result with the violations when the model sends bad args', async () => {
const ctx = await setup()
ctx.tools.register(defineTool({