Add abstract service interface packages

@deepseek-ai/dsh-llm: provider-neutral content-block vocabulary
(merge-extensible maps), raw StreamChunk protocol, ToolSchema,
abstract LlmAdapter, LlmService adapter registry, BlockAssembler.

@deepseek-ai/dsh-session: event-sourced Session (append-only log,
deriveMessages; context/steering render as tagged envelopes),
SessionStore, session/event + awaited session/flush durability seam.

@deepseek-ai/dsh-system-prompt: ordered sections + tool-schema
providers; assemble() through the system-prompt/assemble waterfall.
Tool schemas are part of the assembly by design.

@deepseek-ai/dsh-tools: tool registry feeding schemas into the
assembly; execute() through the tools/execute waterfall (the single
sandbox/permission/hook seam).

@deepseek-ai/dsh-agent: Agent interface (send/steer/inject/abort,
spawn/fork TODO seams), AgentRegistry, and the full agent/* event
taxonomy so plugins never depend on the concrete loop.
This commit is contained in:
Tianyi Cui
2026-06-11 10:54:06 +08:00
parent 72688a3888
commit d5a1d9bb75
25 changed files with 1703 additions and 0 deletions
@@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import SystemPrompt, { PromptAssembly, renderPrompt } from '@deepseek-ai/dsh-system-prompt'
describe('SystemPrompt', () => {
it('assembles sections in order with dynamic text and collected tools', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
ctx.systemPrompt.section({ name: 'persona', order: 0, text: 'You are DeepSeek Code.' })
ctx.systemPrompt.section({ name: 'cwd', order: 20, text: () => `cwd: /tmp` })
ctx.systemPrompt.section({ name: 'rules', order: 10, text: 'Be precise.' })
ctx.systemPrompt.tools(() => [{ name: 'echo', description: 'echo back', parameters: {} }])
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections.map(s => s.name)).toEqual(['persona', 'rules', 'cwd'])
expect(assembly.tools).toEqual([{ name: 'echo', description: 'echo back', parameters: {} }])
expect(renderPrompt(assembly)).toBe('You are DeepSeek Code.\n\nBe precise.\n\ncwd: /tmp')
})
it('removes contributions when the contributing fiber is disposed (HMR safety)', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
const fiber = await ctx.plugin(Object.assign((inner: Context) => {
inner.systemPrompt.section({ name: 'scoped', order: 0, text: 'scoped section' })
inner.systemPrompt.tools(() => [{ name: 'scoped-tool', description: '', parameters: {} }])
}, { inject: ['systemPrompt'] }))
expect((await ctx.systemPrompt.assemble()).sections).toHaveLength(1)
await fiber.dispose()
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections).toHaveLength(0)
expect(assembly.tools).toHaveLength(0)
})
it('composes multiple system-prompt/assemble waterfall listeners in order', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
ctx.systemPrompt.section({ name: 'base', order: 0, text: 'base' })
// Listener A appends a section, then delegates.
ctx.on('system-prompt/assemble', async (assembly: PromptAssembly, next) => {
assembly.sections.push({ name: 'from-a', order: 100, text: 'a' })
return next()
})
// Listener B (registered later, runs after A) sees A's contribution.
const seen: string[][] = []
ctx.on('system-prompt/assemble', async (assembly: PromptAssembly, next) => {
seen.push(assembly.sections.map(s => s.name))
return next()
})
const assembly = await ctx.systemPrompt.assemble()
expect(seen).toEqual([['base', 'from-a']])
expect(assembly.sections.map(s => s.name)).toEqual(['base', 'from-a'])
})
it('lets a waterfall listener short-circuit by not calling next()', async () => {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
ctx.systemPrompt.section({ name: 'real', order: 0, text: 'real' })
ctx.on('system-prompt/assemble', async () => {
return { sections: [], tools: [] } satisfies PromptAssembly
})
const assembly = await ctx.systemPrompt.assemble()
expect(assembly.sections).toHaveLength(0)
})
})