fix: harden skill discovery

This commit is contained in:
Yichen Jiang
2026-07-05 18:33:27 +08:00
parent f626e569a4
commit dca2cc257d
29 changed files with 555 additions and 61 deletions
+13
View File
@@ -49,6 +49,15 @@ import * as uiStdio from './stdio-chat.ts'
export const name = 'stdio-agent'
const SkillConfigSchema: z<agentCore.SkillConfig> = z.object({
dshHome: z.string(),
agentsHome: z.string(),
extraRoots: z.array(z.string()).default([]),
installSystemSkills: z.boolean().default(true),
promptFieldMaxLength: z.number().default(500),
collectCacheMaxEntries: z.number().default(128),
})
/**
* App config: the swappable per-demo values, each routed to where the app wires
* it. `model`/`systemPrompt`/`resumeSessionId` configure the pre-created `main`
@@ -66,6 +75,8 @@ export interface Config {
persistenceRoot?: string
/** stdin-chat banner printed once on start. Defaults to `'ready.'`. */
welcome?: string
/** Skill discovery config forwarded to the shared agent-core spine. */
skills?: agentCore.SkillConfig
/**
* If set, the `main` agent RESUMES this persisted session id instead of
* starting fresh. Sourced from an env var in the leaf `cordis.yml`
@@ -79,6 +90,7 @@ export const Config: z<Config> = z.object({
systemPrompt: z.string().required(),
persistenceRoot: z.string().default('./.sessions'),
welcome: z.string().default('ready.'),
skills: SkillConfigSchema,
resumeSessionId: z.string(),
})
@@ -99,6 +111,7 @@ export function apply(ctx: Context, config: Config): void {
cwd: process.cwd(),
...config.resumeSessionId !== undefined ? { resumeSessionId: SessionId(config.resumeSessionId) } : {},
}],
...config.skills !== undefined ? { skills: config.skills } : {},
})
ctx.plugin(SessionPersistenceJsonl, { root: config.persistenceRoot ?? './.sessions' })
ctx.plugin(uiStdio, { welcome: config.welcome ?? 'ready.', agent: 'main' })
@@ -110,7 +110,7 @@ function runBuiltBin(cwd: string, configArg: string, line: string): Promise<{ st
const child = spawn(process.execPath, ['--expose-internals', stdioBin, configArg], {
cwd,
// Mock model: never calls the network, so no key needed.
env: { ...process.env },
env: { ...process.env, DSH_HOME: join(cwd, '.dsh'), DSH_AGENTS_HOME: join(cwd, '.agents') },
stdio: ['pipe', 'pipe', 'pipe'],
})
let stdout = ''
@@ -1,4 +1,7 @@
import { describe, it, expect } from 'vitest'
import { mkdtemp } from 'node:fs/promises'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { AgentId } from '@deepseek-ai/dsh-agent'
@@ -28,9 +31,14 @@ async function mount(config: stdioAgent.Config): Promise<Context> {
return ctx
}
async function isolatedSkillsConfig(): Promise<NonNullable<stdioAgent.Config['skills']>> {
const home = await mkdtemp(join(tmpdir(), 'dsh-stdio-agent-skills-'))
return { dshHome: join(home, '.dsh'), agentsHome: join(home, '.agents'), installSystemSkills: false }
}
describe('dsh-stdio-agent app', () => {
it('composes the spine + front-door cluster and pre-creates the main agent', async () => {
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', persistenceRoot: '/tmp/dsh-stdio-agent-spec' })
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', persistenceRoot: '/tmp/dsh-stdio-agent-spec', skills: await isolatedSkillsConfig() })
// The spine services (brought up by the agent-core bundle) are all present.
expect(ctx.get('agents')).toBeDefined()
expect(ctx.get('agentLoop')).toBeDefined()
@@ -48,7 +56,7 @@ describe('dsh-stdio-agent app', () => {
// apply()'s last two lines are the ones that fire — covering a
// schema-bypassing direct-mount caller.
const ctx = new Context()
stdioAgent.apply(ctx, { model: 'mock', systemPrompt: 'hi' })
stdioAgent.apply(ctx, { model: 'mock', systemPrompt: 'hi', skills: await isolatedSkillsConfig() })
await new Promise(resolve => setTimeout(resolve, 80))
expect(ctx.get('sessionPersistence')).toBeDefined()
expect(ctx.get('agents')?.get(AgentId('main'))).toBeDefined()
@@ -64,11 +72,18 @@ describe('dsh-stdio-agent app', () => {
systemPrompt: 'hi',
persistenceRoot: '/tmp/dsh-stdio-agent-spec-resume',
resumeSessionId: 'no-such-session',
skills: await isolatedSkillsConfig(),
})
expect(ctx.get('agents')?.get(AgentId('main'))).toBeUndefined()
await ctx.fiber.dispose()
})
it('forwards skill config into agent-core', async () => {
const ctx = await mount({ model: 'mock', systemPrompt: 'hi', skills: await isolatedSkillsConfig() })
expect(await ctx.skills.list()).toEqual([])
await ctx.fiber.dispose()
})
it('exposes its name and Config schema', () => {
expect(stdioAgent.name).toBe('stdio-agent')
expect(stdioAgent.Config).toBeDefined()