feat(agent-presets): add Codex and Claude Code subagent tools

This commit is contained in:
pku-xht
2026-08-10 12:45:05 +08:00
parent e637f95d78
commit b1d67a6935
45 changed files with 1708 additions and 45 deletions

View File

@@ -7,7 +7,7 @@ import {
rmSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { delimiter, dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import { Context } from 'cordis'
@@ -87,6 +87,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)(
]) mkdirSync(directory)
const env = {
PATH: `${dirname(claudeBin)}${delimiter}${process.env.PATH ?? ''}`,
ANTHROPIC_AUTH_TOKEN: apiKey,
ANTHROPIC_BASE_URL: `${deepSeekBaseUrl()}/anthropic`,
ANTHROPIC_MODEL: 'deepseek-v4-pro[1m]',

View File

@@ -1,13 +1,15 @@
import { execFile } from 'node:child_process'
import {
copyFileSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { delimiter, dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { promisify } from 'node:util'
import type {
@@ -19,7 +21,7 @@ import { Context } from 'cordis'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Agent } from '@deepseek-ai/dsh-agent'
import SubagentService from '@deepseek-ai/dsh-subagent'
import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import * as claudeCode from '../src/index.ts'
import {
@@ -98,9 +100,11 @@ afterEach(async () => {
interface RealHarness {
readonly ctx: Context
readonly handles: SubprocessHandle[]
readonly spawnSpecs: SubprocessSpawnSpec[]
readonly parent: Agent
readonly workspace: string
readonly env: Record<string, string>
readonly executable: string
}
async function realHarness(behavior: MessagesBehavior): Promise<{
@@ -112,9 +116,14 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
const workspace = join(root, 'workspace')
const claudeConfig = join(root, 'claude-config')
const xdgConfig = join(root, 'xdg')
const nativeBin = join(root, 'native-bin')
mkdirSync(workspace)
mkdirSync(claudeConfig)
mkdirSync(xdgConfig)
mkdirSync(nativeBin)
const executable = join(nativeBin, process.platform === 'win32' ? 'claude.exe' : 'claude')
if (process.platform === 'win32') copyFileSync(claudeBin, executable)
else symlinkSync(claudeBin, executable)
writeFileSync(
join(claudeConfig, 'settings.json'),
`${JSON.stringify({ model: settingsModel }, null, 2)}\n`,
@@ -122,6 +131,7 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
const fixture = await startMessagesFixture(behavior)
fixtures.push(fixture)
const env = {
PATH: `${nativeBin}${delimiter}${process.env.PATH ?? ''}`,
ANTHROPIC_API_KEY: fakeKey,
ANTHROPIC_BASE_URL: fixture.baseUrl,
CLAUDE_CONFIG_DIR: claudeConfig,
@@ -141,8 +151,10 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
await ctx.plugin(SubagentService)
await ctx.plugin(LocalSubprocessService)
const handles: SubprocessHandle[] = []
const spawnSpecs: SubprocessSpawnSpec[] = []
const spawn = ctx.subprocess.spawn.bind(ctx.subprocess)
vi.spyOn(ctx.subprocess, 'spawn').mockImplementation((spec) => {
spawnSpecs.push(spec)
const handle = spawn(spec)
handles.push(handle)
return handle
@@ -153,7 +165,7 @@ async function realHarness(behavior: MessagesBehavior): Promise<{
session: { header: { cwd: workspace } },
} as unknown as Agent
return {
harness: { ctx, handles, parent, workspace, env },
harness: { ctx, handles, spawnSpecs, parent, workspace, env, executable },
fixture,
}
}
@@ -195,7 +207,7 @@ describe('real Claude Agent SDK 0.3.220 and Claude Code 2.1.220', {
expect(sdkPackage.version).toBe('0.3.220')
expect(sdkPackage.claudeCodeVersion).toBe('2.1.220')
expect(sdkPackage.optionalDependencies[platformPackage]).toBe('0.3.220')
const version = await execFileAsync(claudeBin, ['--version'], {
const version = await execFileAsync(harness.executable, ['--version'], {
env: { ...process.env, ...harness.env },
})
expect(version.stdout.trim()).toBe('2.1.220 (Claude Code)')
@@ -212,6 +224,7 @@ describe('real Claude Agent SDK 0.3.220 and Claude Code 2.1.220', {
message.type === 'system' && message.subtype === 'init',
)
expect(initMessage?.claude_code_version).toBe('2.1.220')
expect(harness.spawnSpecs[0]?.argv[0]).toBe(harness.executable)
expect(fixture.requests).toHaveLength(1)
const recorded = fixture.requests[0]!

View File

@@ -248,6 +248,7 @@ function fakeRun(
const options: FakeRun['options'] = []
const spec: ClaudeCodeRunSpec = {
cwd: '/workspace',
executable: '/native/claude',
env: { ANTHROPIC_API_KEY: 'fake-key' },
disposeGraceMs: 5,
spawn: (spawnSpec) => {
@@ -331,6 +332,8 @@ describe('task admission and package contracts', () => {
const child = fakeChild()
const spawn = vi.spyOn(ctx.subprocess, 'spawn')
.mockImplementation(() => child.handle)
const resolveExecutable = vi.spyOn(ctx.subprocess, 'resolveExecutable')
.mockResolvedValue('/native/claude')
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
await ctx.plugin(claudeCode, {
env: {
@@ -352,6 +355,11 @@ describe('task admission and package contracts', () => {
)
expect(queryMock).not.toHaveBeenCalled()
resolveExecutable.mockRejectedValueOnce(new Error('claude missing from PATH'))
await expect(ctx.subagents.start('claude-code', request()))
.rejects.toThrow('claude missing from PATH')
expect(queryMock).not.toHaveBeenCalled()
const run = await ctx.subagents.start('claude-code', request())
child.settle({ exitCode: 9, signal: null })
child.stdout.end()
@@ -362,6 +370,13 @@ describe('task admission and package contracts', () => {
expect(warn).toHaveBeenCalledWith(expect.stringContaining(
'subagent-claude-code: child run failed (error):',
))
expect(resolveExecutable).toHaveBeenCalledWith(
'claude',
expect.objectContaining({ ANTHROPIC_API_KEY: 'provider-fake-key' }),
expect.any(AbortSignal),
)
expect(queryMock.mock.calls[0]?.[0].options.pathToClaudeCodeExecutable)
.toBe('/native/claude')
expect(spawn).toHaveBeenCalledWith(expect.objectContaining({
cwd: process.cwd(),
graceMs: 29,
@@ -441,6 +456,19 @@ describe('official spawn projection', () => {
)).toThrow('SDK spawn request omitted its workspace')
})
it.each(['cmd', 'bat'])('routes a Windows .%s shim through cmd.exe', (extension) => {
const command = String.raw`C:\Program Files\Claude\claude.${extension}`
const spec = claudeSpawnSpec(sdkSpawnOptions({
command,
args: ['--output-format', 'stream-json'],
}), 7, 'win32')
expect(spec.argv).toEqual([
'cmd.exe', '/d', '/s', '/c', command,
'--output-format', 'stream-json',
])
})
it('projects streams, exit facts, listeners, and idempotent tree termination', async () => {
const child = fakeChild({ exitOnTerminate: false })
const process = new ManagedClaudeCodeProcess(child.handle)
@@ -508,6 +536,7 @@ describe('query options and result mapping', () => {
const captured: SubprocessHandle[] = []
const spec: ClaudeCodeRunSpec = {
cwd: '/workspace',
executable: '/native/claude',
env: {
HOST_VISIBLE: 'overridden',
ANTHROPIC_API_KEY: 'explicit-fake-key',
@@ -523,6 +552,7 @@ describe('query options and result mapping', () => {
expect(options).toMatchObject({
abortController: controller,
cwd: '/workspace',
pathToClaudeCodeExecutable: '/native/claude',
persistSession: false,
disallowedTools: ['AskUserQuestion'],
})
@@ -670,6 +700,7 @@ describe('run publication, cancellation, and settlement', () => {
let index = 0
const spec: ClaudeCodeRunSpec = {
cwd: '/workspace',
executable: '/native/claude',
env: {},
disposeGraceMs: 5,
spawn: () => children[index++]!.handle,
@@ -720,6 +751,7 @@ describe('run publication, cancellation, and settlement', () => {
request(undefined, parentAbort.signal),
{
cwd: '/workspace',
executable: '/native/claude',
env: {},
disposeGraceMs: 5,
spawn: () => child.handle,