Document the codebase thoroughly and tighten type safety
Docs: per-folder README.md for packages/ (family overview + one per package: service, events, API, extension points, TODOs), examples/, and examples/echo-agent/; folder-level AGENTS.md (+ CLAUDE.md symlinks) for packages/ and vendor/; module-level doc comments in every packages/*/src file; richer JSDoc on all exported API (event side effects, disposal contracts, error behavior). Root AGENTS.md gains a "Type Safety and Documentation" policy section: the codebase aims to be very type-safe and well documented; type gymnastics are acceptable in core packages when they improve plugin-author DX; verbose docs are fine as long as they stay strictly in sync with the code. Type safety: removed the upstream-inherited "noImplicitAny": false from tsconfig.base.json — packages/* now compile under full strict mode; vendor/loader and vendor/include set it locally (vendor/cordis already did). Eliminated every `: any` / `as any` from packages and examples (catch clauses use unknown + a CodedError narrowing type; event data access uses discriminated-union narrowing). Typed tool schemas: new @deepseek-ai/dsh-tools schema DSL — SchemaSpec with per-property `required: true` booleans, type-level InferArgs<S>, a runtime SchemaSpec → JSON Schema converter, and defineTool() so first-party tools get typed execute(args) with zero casts (raw JSON Schema still accepted for MCP interop; chosen over schemastery because it targets JSON Schema generation directly). echo-tool and all test tools migrated; +7 tests.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { ToolExecutionResult } from '@deepseek-ai/dsh-tools'
|
||||
import ToolRegistry, { defineTool, schemaSpecToJsonSchema, ToolExecutionResult } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
async function setup() {
|
||||
const ctx = new Context()
|
||||
@@ -10,14 +10,14 @@ async function setup() {
|
||||
return ctx
|
||||
}
|
||||
|
||||
const echoTool = {
|
||||
const echoTool = defineTool({
|
||||
name: 'echo',
|
||||
description: 'echo arguments back',
|
||||
parameters: { type: 'object', properties: { text: { type: 'string' } } },
|
||||
async execute(args: any) {
|
||||
return [{ type: 'text' as const, text: String(args?.text ?? '') }]
|
||||
parameters: { text: { type: 'string' } },
|
||||
async execute(args) {
|
||||
return [{ type: 'text' as const, text: String(args.text ?? '') }]
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('ToolRegistry', () => {
|
||||
it('registers tools, exposes schemas, and feeds the system-prompt assembly', async () => {
|
||||
@@ -29,8 +29,9 @@ describe('ToolRegistry', () => {
|
||||
description: 'echo arguments back',
|
||||
parameters: { type: 'object', properties: { text: { type: 'string' } } },
|
||||
}])
|
||||
// schemas() result must not leak execute
|
||||
expect((ctx.tools.schemas()[0] as any).execute).toBeUndefined()
|
||||
// schemas() result must not leak execute — as any intentional: 'execute'
|
||||
// is deliberately absent from ToolSchema, we're testing it's not there
|
||||
expect((ctx.tools.schemas()[0] as Record<string, unknown>).execute).toBeUndefined()
|
||||
|
||||
const assembly = await ctx.systemPrompt.assemble()
|
||||
expect(assembly.tools.map(t => t.name)).toEqual(['echo'])
|
||||
@@ -118,3 +119,183 @@ describe('ToolRegistry', () => {
|
||||
expect(ctx.tools.schemas().map(t => t.name)).toEqual(['echo'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('defineTool / schema DSL', () => {
|
||||
it('converts SchemaSpec to standard JSON Schema with required array', () => {
|
||||
const spec = {
|
||||
path: { type: 'string', required: true as const, description: 'Absolute path' },
|
||||
offset: { type: 'number' },
|
||||
limit: { type: 'number', description: 'Max lines' },
|
||||
}
|
||||
const jsonSchema = schemaSpecToJsonSchema(spec)
|
||||
expect(jsonSchema).toEqual({
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Absolute path' },
|
||||
offset: { type: 'number' },
|
||||
limit: { type: 'number', description: 'Max lines' },
|
||||
},
|
||||
required: ['path'],
|
||||
})
|
||||
})
|
||||
|
||||
it('handles empty spec (no properties, no required)', () => {
|
||||
expect(schemaSpecToJsonSchema({})).toEqual({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
})
|
||||
})
|
||||
|
||||
it('handles nested object spec', () => {
|
||||
const spec = {
|
||||
config: {
|
||||
type: 'object' as const,
|
||||
required: true as const,
|
||||
properties: {
|
||||
host: { type: 'string', required: true as const },
|
||||
port: { type: 'number' },
|
||||
},
|
||||
},
|
||||
}
|
||||
const jsonSchema = schemaSpecToJsonSchema(spec)
|
||||
expect(jsonSchema).toEqual({
|
||||
type: 'object',
|
||||
properties: {
|
||||
config: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
host: { type: 'string' },
|
||||
port: { type: 'number' },
|
||||
},
|
||||
required: ['host'],
|
||||
},
|
||||
},
|
||||
required: ['config'],
|
||||
})
|
||||
})
|
||||
|
||||
it('defineTool returns a valid ToolDefinition with typed execute', async () => {
|
||||
const ctx = await setup()
|
||||
const tool = defineTool({
|
||||
name: 'typed-echo',
|
||||
description: 'A typed echo tool',
|
||||
parameters: {
|
||||
text: { type: 'string', required: true },
|
||||
uppercase: { type: 'boolean' },
|
||||
},
|
||||
async execute(args) {
|
||||
// args is typed: { text: string; uppercase?: boolean }
|
||||
const result = args.uppercase ? args.text.toUpperCase() : args.text
|
||||
return [{ type: 'text', text: result }]
|
||||
},
|
||||
})
|
||||
|
||||
ctx.tools.register(tool)
|
||||
expect(ctx.tools.schemas()).toEqual([{
|
||||
name: 'typed-echo',
|
||||
description: 'A typed echo tool',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string' },
|
||||
uppercase: { type: 'boolean' },
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
}])
|
||||
|
||||
const result = await ctx.tools.execute({
|
||||
callId: 'c1',
|
||||
name: 'typed-echo',
|
||||
arguments: { text: 'hello', uppercase: true },
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'HELLO' }])
|
||||
})
|
||||
|
||||
it('type-level: InferArgs maps required properties to non-optional', () => {
|
||||
// Compile-time check: if this compiles, InferArgs is correct.
|
||||
// args.a is string (required), args.b is number|undefined (optional).
|
||||
const tool = defineTool({
|
||||
name: 'type-check',
|
||||
description: '',
|
||||
parameters: { a: { type: 'string' as const, required: true as const }, b: { type: 'number' as const } },
|
||||
async execute(args) {
|
||||
// Verify types at runtime via typeof
|
||||
expect(typeof args.a).toBe('string')
|
||||
// args.b should be undefined when not provided
|
||||
void args
|
||||
return [{ type: 'text', text: args.a }]
|
||||
},
|
||||
})
|
||||
void tool
|
||||
})
|
||||
|
||||
it('registry round-trips a defineTool definition (register→schemas→execute)', async () => {
|
||||
const ctx = await setup()
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'roundtrip',
|
||||
description: 'Round-trip test',
|
||||
parameters: {
|
||||
req: { type: 'string', required: true },
|
||||
opt: { type: 'number', description: 'Optional number' },
|
||||
},
|
||||
async execute(args) {
|
||||
return [{ type: 'text', text: `${args.req}:${args.opt ?? 'none'}` }]
|
||||
},
|
||||
}))
|
||||
|
||||
// Schema round-trip: schemas() returns standard JSON Schema
|
||||
const schemas = ctx.tools.schemas()
|
||||
expect(schemas).toHaveLength(1)
|
||||
expect(schemas[0].parameters).toEqual({
|
||||
type: 'object',
|
||||
properties: {
|
||||
req: { type: 'string' },
|
||||
opt: { type: 'number', description: 'Optional number' },
|
||||
},
|
||||
required: ['req'],
|
||||
})
|
||||
|
||||
// Execution round-trip
|
||||
const result = await ctx.tools.execute({
|
||||
callId: 'c1',
|
||||
name: 'roundtrip',
|
||||
arguments: { req: 'hello' },
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'hello:none' }])
|
||||
})
|
||||
|
||||
it('still accepts raw JSON-Schema ToolDefinition directly (MCP interop)', async () => {
|
||||
const ctx = await setup()
|
||||
ctx.tools.register({
|
||||
name: 'raw-tool',
|
||||
description: 'Raw JSON Schema tool (like an MCP adapter would register)',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: { path: { type: 'string' } },
|
||||
required: ['path'],
|
||||
},
|
||||
async execute(args: unknown) {
|
||||
const p = args as { path: string }
|
||||
return [{ type: 'text', text: p.path }]
|
||||
},
|
||||
})
|
||||
|
||||
const schemas = ctx.tools.schemas()
|
||||
expect(schemas[0].parameters).toEqual({
|
||||
type: 'object',
|
||||
properties: { path: { type: 'string' } },
|
||||
required: ['path'],
|
||||
})
|
||||
|
||||
const result = await ctx.tools.execute({
|
||||
callId: 'c1',
|
||||
name: 'raw-tool',
|
||||
arguments: { path: '/tmp' },
|
||||
})
|
||||
expect(result.isError).toBe(false)
|
||||
expect(result.content).toEqual([{ type: 'text', text: '/tmp' }])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user