feat(bash): generalize managed shell environment
This commit is contained in:
@@ -39,11 +39,11 @@ This is the [interface/implementation/consumer seam](../../../docs/rfc/implement
|
||||
|
||||
```ts
|
||||
import type { Config } from '@deepseek-ai/dsh-agent-core'
|
||||
// { agents?, persona?, toolOrder?, tools?, skills? } — the schema intersects the owner schemas,
|
||||
// { agents?, persona?, toolOrder?, tools?, dshHome?, skills? } — the schema intersects the owner schemas,
|
||||
// so validation and defaulting can never drift from the owners.
|
||||
```
|
||||
|
||||
The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loop` (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`) — `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; and `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer. Forwarding is exactly why the owners can live in the shared spine even though the apps disagree on what to configure.
|
||||
The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loop` (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`) — `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry; `dshHome` to tool-bash's managed environment and the local skill provider; and `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly.
|
||||
|
||||
## Why a code bundle, not a shared YAML include
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { resolve as resolvePath } from 'node:path'
|
||||
import Timer from '@cordisjs/plugin-timer'
|
||||
import z from 'schemastery'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
@@ -78,7 +79,8 @@ export interface SkillConfig {
|
||||
* bridge, simply omits it), `persona` and `toolOrder` to the system-prompt
|
||||
* plugin (the deployment's persona section and the explicit model-facing tool
|
||||
* order), the `tools` object to the tool registry (its presentation `mode`),
|
||||
* and `skills` to the skill registry/local provider/tool consumer. Every field
|
||||
* `dshHome` to the bash environment registry and local skill provider, and
|
||||
* `skills` to the skill registry/local provider/tool consumer. Every field
|
||||
* is optional INPUT here because each owner's schema supplies the default;
|
||||
* the schema is the INTERSECTION of the owners' own schemas (with registry
|
||||
* schemas nested under their bundle keys), so validation and defaulting can
|
||||
@@ -93,6 +95,8 @@ export interface Config {
|
||||
toolOrder?: SystemPromptConfig['toolOrder']
|
||||
/** The tool registry's config — its presentation `mode` (see dsh-tools' `Config`). */
|
||||
tools?: ToolsConfig
|
||||
/** DeepSeek Harness home directory shared by shell context and local skill discovery. */
|
||||
dshHome?: string
|
||||
/** Skill registry, local provider, and model-facing consumer config. */
|
||||
skills?: SkillConfig
|
||||
}
|
||||
@@ -108,7 +112,7 @@ export const SkillConfigSchema: z<SkillConfig> = z.object({
|
||||
export const Config = z.intersect([
|
||||
AgentLoop.Config,
|
||||
SystemPrompt.Config,
|
||||
z.object({ tools: ToolRegistry.Config, skills: SkillConfigSchema }),
|
||||
z.object({ tools: ToolRegistry.Config, dshHome: z.string(), skills: SkillConfigSchema }),
|
||||
]) as unknown as z<Config>
|
||||
|
||||
/**
|
||||
@@ -121,6 +125,13 @@ export const Config = z.intersect([
|
||||
* then the loop that drives them.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const nestedDshHome = config.skills?.local?.dshHome
|
||||
if (config.dshHome !== undefined && nestedDshHome !== undefined
|
||||
&& resolvePath(config.dshHome) !== resolvePath(nestedDshHome)) {
|
||||
throw new Error('agent-core: dshHome and skills.local.dshHome must resolve to the same directory')
|
||||
}
|
||||
const dshHome = config.dshHome ?? nestedDshHome
|
||||
|
||||
ctx.plugin(Timer)
|
||||
ctx.plugin(LlmService)
|
||||
ctx.plugin(SessionStore)
|
||||
@@ -136,10 +147,14 @@ export function apply(ctx: Context, config: Config): void {
|
||||
})
|
||||
ctx.plugin(ToolRegistry, config.tools ?? {})
|
||||
ctx.plugin(SkillService, config.skills?.registry ?? {})
|
||||
ctx.plugin(SkillLocal, config.skills?.local ?? {})
|
||||
ctx.plugin(SkillLocal, Object.assign(
|
||||
{},
|
||||
config.skills?.local,
|
||||
dshHome === undefined ? {} : { dshHome },
|
||||
))
|
||||
ctx.plugin(AgentRegistry)
|
||||
ctx.plugin(invariants)
|
||||
ctx.plugin(toolBash)
|
||||
ctx.plugin(toolBash, dshHome === undefined ? {} : { dshHome })
|
||||
ctx.plugin(toolSkill, config.skills?.tool ?? {})
|
||||
ctx.plugin(AgentLoop, { agents: config.agents ?? [] })
|
||||
}
|
||||
|
||||
@@ -2,12 +2,24 @@ import { describe, expect, it } from 'vitest'
|
||||
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { Context } from 'cordis'
|
||||
import { Context, Service } from 'cordis'
|
||||
import Loader from '@cordisjs/plugin-loader'
|
||||
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
|
||||
import * as agentCore from '../src/index.ts'
|
||||
import { AgentId } from '@deepseek-ai/dsh-agent'
|
||||
import type { Message } from '@deepseek-ai/dsh-llm'
|
||||
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
|
||||
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
/** Minimal service that lets the executor-less bundle activate tool-bash in config-forwarding tests. */
|
||||
class StubBashService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'bash')
|
||||
}
|
||||
|
||||
onTaskDone(): () => void {
|
||||
return () => undefined
|
||||
}
|
||||
}
|
||||
|
||||
async function composePrefix(ctx: Context, cwd: string): Promise<Message[]> {
|
||||
const empty: Message[] = []
|
||||
@@ -152,6 +164,39 @@ describe('dsh-agent-core bundle', () => {
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('shares top-level dshHome between local skills and the managed bash environment', async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'dsh-agent-core-shared-home-'))
|
||||
const agentsHome = await mkdtemp(join(tmpdir(), 'dsh-agent-core-shared-agents-'))
|
||||
await mkdir(join(home, 'skills'), { recursive: true })
|
||||
await writeFile(join(home, 'skills', 'shared-skill.md'), '---\nname: shared-skill\ndescription: Shared home skill\n---\n\nShared body.\n')
|
||||
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(StubBashService)
|
||||
await ctx.plugin(agentCore, {
|
||||
dshHome: home,
|
||||
skills: { local: { agentsHome } },
|
||||
})
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
|
||||
expect((await ctx.skills.list()).map(skill => skill.name)).toEqual(['shared-skill'])
|
||||
const execution: ToolExecution = {
|
||||
callId: CallId('agent-core-dsh-home'),
|
||||
name: 'bash',
|
||||
arguments: { command: 'true' },
|
||||
}
|
||||
expect(ctx.bashEnv.collect(execution)).toMatchObject({ DSH_HOME: home, DSH_SHELL: '1' })
|
||||
await ctx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('rejects conflicting global and nested DSH home directories', () => {
|
||||
expect(() => {
|
||||
agentCore.apply(new Context(), {
|
||||
dshHome: '/global-dsh-home',
|
||||
skills: { local: { dshHome: '/nested-dsh-home' } },
|
||||
})
|
||||
}).toThrow(/must resolve to the same directory/)
|
||||
})
|
||||
|
||||
it('uses the default skill config when apply is called directly without skills', async () => {
|
||||
await withIsolatedSkillHomes(async () => {
|
||||
const ctx = new Context()
|
||||
|
||||
Reference in New Issue
Block a user