feat: add canonical typed tool outputs

This commit is contained in:
Tianyi Cui
2026-07-21 03:08:35 +08:00
parent 8500974fd4
commit 66c36e7325
173 changed files with 3298 additions and 954 deletions

View File

@@ -16,7 +16,7 @@ The plugin contributes one user-role `<system-reminder>` catalog through `agent/
|---|---|---|
| `name` | string (required) | Exact kebab-case skill name from the available skills listing. |
Execution uses the calling agent's `session.header.cwd` so workspace-sensitive providers resolve the winning skill. A successful call returns one text result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`.
Execution uses the calling agent's `session.header.cwd` so workspace-sensitive providers resolve the winning skill. A successful call returns canonical `{ name, provider, resourceBase?, content }`, excluding catalog ranking and provider-internal machinery; its Native renderer produces one text result containing `<skill_content name="...">`, `<skill_resources>`, and `<skill_instructions>`.
Resource guidance resolves only paths or URLs explicitly referenced by the instructions against `resourceBase`; scripts, references, and assets load on demand, and the result does not enumerate a skill directory. Local providers may supply a directory, while remote or embedded providers may supply a URL or opaque loading guidance.

View File

@@ -42,6 +42,46 @@ export function apply(ctx: Context, config: Config = {}): void {
parameters: {
name: { type: 'string', required: true, description: 'The exact skill name from the available skills list.' },
},
output: {
schema: {
type: 'object',
additionalProperties: false,
properties: {
name: { type: 'string', required: true },
provider: { type: 'string', required: true },
resourceBase: {
oneOf: [
{
type: 'object',
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, const: 'directory' },
path: { type: 'string', required: true },
},
},
{
type: 'object',
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, const: 'url' },
url: { type: 'string', required: true },
},
},
{
type: 'object',
additionalProperties: false,
properties: {
kind: { type: 'string', required: true, const: 'opaque' },
description: { type: 'string', required: true },
},
},
],
},
content: { type: 'string', required: true },
},
},
render: (_args, value) => [{ type: 'text', text: renderSkillContent(value) }],
},
async execute(args, exec) {
if (!isSkillName(args.name)) {
throw new Error(`invalid skill name "${args.name}"`)
@@ -53,7 +93,14 @@ export function apply(ctx: Context, config: Config = {}): void {
if (skill.disableModelInvocation === true) {
throw new Error(`skill "${args.name}" is not available for model invocation`)
}
return [{ type: 'text', text: renderSkillContent(skill) }]
return {
name: skill.name,
provider: skill.provider,
...skill.resourceBase !== undefined ? {
resourceBase: { ...skill.resourceBase },
} : {},
content: skill.content,
}
},
presentCall(args) {
return { card: 'generic', title: `Load skill ${args.name}`, kind: 'read', rawInput: args.name }
@@ -77,7 +124,7 @@ export function apply(ctx: Context, config: Config = {}): void {
})
}
function renderSkillContent(skill: SkillDefinition): string {
function renderSkillContent(skill: Pick<SkillDefinition, 'name' | 'provider' | 'resourceBase' | 'content'>): string {
const resourceHint = renderResourceHint(skill)
return [
`<skill_content name="${escapeAttr(skill.name)}">`,
@@ -92,7 +139,7 @@ function renderSkillContent(skill: SkillDefinition): string {
].join('\n')
}
function renderResourceHint(skill: SkillDefinition): string[] {
function renderResourceHint(skill: Pick<SkillDefinition, 'provider' | 'resourceBase'>): string[] {
const base = skill.resourceBase
if (base === undefined) {
return [
@@ -116,8 +163,10 @@ function renderResourceHint(skill: SkillDefinition): string[] {
`Resources for this skill: ${escapeText(base.description)}`,
'Load referenced resources only as needed.',
]
/* v8 ignore start -- SkillResourceBase is a closed union; a future kind must fail compilation here. */
default:
return assertNever(base, 'SkillResourceBase.kind')
/* v8 ignore stop */
}
}

View File

@@ -6,7 +6,7 @@ import { Context } from 'cordis'
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
import { createScope, type Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
import { agentEvents, type Agent } from '@deepseek-ai/dsh-agent'
import SkillService from '@deepseek-ai/dsh-skill'
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
@@ -185,7 +185,7 @@ describe('dsh-tool-skill', () => {
const ctx = await setup(home)
ctx.skills.register({ name: 'listed-skill', description: 'Listed', source: 'runtime', content: 'body' })
const { agent, scope } = await mintAgentScope(ctx, '/workspace')
scope.ctx.tools.register(defineTool({
scope.ctx.tools.register(defineContentToolFixture({
name: 'skill',
description: 'A scoped tool with unrelated semantics.',
parameters: {},
@@ -226,6 +226,13 @@ describe('dsh-tool-skill', () => {
})
expect(result.isError).toBe(false)
if (result.isError) throw new Error('expected skill success')
expect(result.value).toEqual({
name: 'project-skill',
provider: 'local',
resourceBase: { kind: 'directory', path: join(project, '.dsh/skills/project-skill') },
content: 'Project instructions.',
})
const block = result.content[0]
expect(block?.type).toBe('text')
if (block?.type !== 'text') throw new Error('expected text skill result')
@@ -283,7 +290,7 @@ describe('dsh-tool-skill', () => {
expect(provider.content[0].text).toContain('<skill_resources>\nResources for this skill are managed by provider "runtime".\nLoad referenced resources only as needed.\n</skill_resources>')
})
it('fails loud on an unknown resource base kind', async () => {
it('rejects an unknown resource-base kind at the canonical output boundary', async () => {
const home = await tempDir('tool-resource-assert-never')
const ctx = await setup(home)
ctx.skills.register({
@@ -298,9 +305,10 @@ describe('dsh-tool-skill', () => {
const result = await ctx.tools.execute({ callId: CallId('c5'), name: 'skill', arguments: { name: 'rogue-resource-skill' } })
expect(result.isError).toBe(true)
expect(result.error?.info?.code).toBe('INVALID_TOOL_OUTPUT')
const block = result.content[0]
if (block?.type !== 'text') throw new Error('expected text tool result')
expect(block.text).toContain('unreachable variant')
expect(block.text).toContain('value.resourceBase')
})
it('returns isError for unknown, invalid, and model-disabled skills', async () => {