feat(subagent): catalog one-shot child sessions

This commit is contained in:
Dudu-0223
2026-07-27 16:58:37 +08:00
committed by Tianyi Cui
parent de6e572e51
commit 774ee34b9a
85 changed files with 1034 additions and 539 deletions

View File

@@ -11,7 +11,12 @@ import { accessSync, constants, statSync } from 'node:fs'
import { isAbsolute, resolve } from 'node:path'
import type { Context } from 'cordis'
import z from 'schemastery'
import type { SubagentCapabilities, SubagentProvider, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import type {
ResolvedSubagentStartRequest,
SubagentCapabilities,
SubagentProvider,
SubagentStartRequest,
} from '@deepseek-ai/dsh-subagent'
import { type AcpRunSpec, DEFAULT_DISPOSE_EOF_GRACE_MS, DEFAULT_DISPOSE_GRACE_MS, type PermissionPolicy, startAcpRun } from './run.ts'
export const name = 'subagent-acp'
@@ -143,7 +148,7 @@ class AcpProvider implements SubagentProvider {
constructor(readonly name: string, private readonly ctx: Context, private readonly config: ResolvedConfig) {}
start(request: SubagentStartRequest) {
start(request: ResolvedSubagentStartRequest) {
const spec: AcpRunSpec = {
command: this.config.command,
args: this.config.args,

View File

@@ -64,6 +64,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('ACP backend with-key e2e (drive
})
const run = await ctx.subagents.start('acp', {
label: 'reply pong',
prompt: [{ type: 'text', text: 'Reply with exactly the word PONG and nothing else. Do not use any tools.' }],
parent: fakeParent,
signal: new AbortController().signal,
@@ -95,6 +96,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('ACP backend with-key e2e (drive
})
const run = await ctx.subagents.start('acp', {
label: 'write proof file',
prompt: [{ type: 'text', text:
'Use the bash tool to write the text ACP_CHILD_WAS_HERE into a file named proof.txt '
+ 'in the current directory. Then reply DONE.' }],

View File

@@ -27,7 +27,7 @@ const mockServer = fileURLToPath(new URL('./mock-acp-server.ts', import.meta.url
const fakeParent = { id: 'parent', session: { header: { cwd: process.cwd() } } } as unknown as Agent
function request(text = 'p', signal = new AbortController().signal) {
return { prompt: [{ type: 'text' as const, text }], parent: fakeParent, signal }
return { label: text, prompt: [{ type: 'text' as const, text }], parent: fakeParent, signal }
}
interface SetupEnv {
@@ -126,7 +126,9 @@ describe('child env layering (through the subprocess seam)', () => {
// explicit entry merges after it and the child must see the value.
const ctx = await setup({ MOCK_ECHO_ENV: 'DSH_ACP_TEST_FACT', DSH_ACP_TEST_FACT: 'managed' })
const parent = { id: 'parent', session: { header: { cwd: process.cwd() } } } as unknown as Agent
const run = await ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal })
const run = await ctx.subagents.start('acp', {
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
})
const result = await run.result
await run.dispose()
const text = result.output.filter(b => b.type === 'text').map(b => (b as { text: string }).text).join('')
@@ -208,7 +210,9 @@ describe('cwd resolution', () => {
try {
const ctx = await setup({ MOCK_ECHO_CWD: '1' })
const parent = { id: 'parent', session: { header: { cwd: workdir } } } as unknown as Agent
const run = await ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal })
const run = await ctx.subagents.start('acp', {
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
})
const result = await run.result
await run.dispose()
// Line 1: where the child process actually ran; line 2: the workspace the
@@ -229,7 +233,9 @@ describe('cwd resolution', () => {
// A command that would create the sentinel if the child were ever spawned.
await ctx.plugin(acp, { providerName: 'acp', command: 'touch', args: [sentinel], permission: 'reject', env: {} })
const parent = { id: 'parent', session: { header: {} } } as unknown as Agent
await expect(ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal }))
await expect(ctx.subagents.start('acp', {
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
}))
.rejects.toThrow('no working directory')
// Resolution failed BEFORE the process boundary — nothing was launched.
expect(existsSync(sentinel)).toBe(false)
@@ -254,7 +260,9 @@ describe('cwd resolution', () => {
env: { MOCK_ECHO_CWD: '1' },
})
const parent = { id: 'parent', session: { header: { cwd: parentDir } } } as unknown as Agent
const run = await ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal })
const run = await ctx.subagents.start('acp', {
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
})
const result = await run.result
await run.dispose()
expect(text(result.output)).toBe(`${configured}\n${configured}`)
@@ -350,7 +358,9 @@ describe('cwd resolution', () => {
// re-introduce the launch-directory dependency this resolution removes.
const ctx = await setup({})
const parent = { id: 'parent', session: { header: { cwd: 'relative/workspace' } } } as unknown as Agent
await expect(ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal }))
await expect(ctx.subagents.start('acp', {
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
}))
.rejects.toThrow('must be an absolute path')
})
@@ -361,7 +371,9 @@ describe('cwd resolution', () => {
try {
const ctx = await setup({})
const parent = { id: 'parent', session: { header: { cwd: file } } } as unknown as Agent
await expect(ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal }))
await expect(ctx.subagents.start('acp', {
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
}))
.rejects.toThrow('not an accessible directory')
} finally {
rmSync(tmp, { recursive: true, force: true })
@@ -377,7 +389,9 @@ describe('cwd resolution', () => {
await ctx.plugin(LocalSubprocessService)
await ctx.plugin(acp, { providerName: 'acp', command: 'touch', args: [sentinel], permission: 'reject', env: {} })
const parent = { id: 'parent', session: { header: { cwd: join(tmp, 'vanished') } } } as unknown as Agent
await expect(ctx.subagents.start('acp', { prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal }))
await expect(ctx.subagents.start('acp', {
label: 'p', prompt: [{ type: 'text' as const, text: 'p' }], parent, signal: new AbortController().signal,
}))
.rejects.toThrow('not an accessible directory')
expect(existsSync(sentinel)).toBe(false)
} finally {