fix(subagent): finalize Codex provider composition

This commit is contained in:
pku-xht
2026-08-04 18:38:05 +08:00
parent 8a24a68333
commit c09b20f96b
31 changed files with 270 additions and 405 deletions
@@ -1,5 +1,5 @@
# Test-only composition: one real Codex app-server delegation through the
# Loader, fixed provider tool, common foreground settlement, and JSONL store.
# Test-only composition of the public opt-in provider and foreground tool.
# The owning e2e boots this tree but never invokes the model or Codex.
- id: fixture
name: './fixture.ts'
@@ -11,17 +11,6 @@
- id: subagent-codex
name: '@deepseek-ai/dsh-subagent-codex'
config:
env:
OPENAI_API_KEY: !!js process.env.DSH_TEST_OPENAI_API_KEY
CODEX_HOME: !!js process.env.DSH_TEST_CODEX_HOME
HOME: !!js process.cwd()
XDG_CONFIG_HOME: !!js process.cwd() + '/xdg'
PATH: !!js process.env.PATH
HTTP_PROXY: ''
HTTPS_PROXY: ''
ALL_PROXY: ''
NO_PROXY: '127.0.0.1,localhost'
- id: tool-subagent-codex
name: '@deepseek-ai/dsh-tool-subagent'
@@ -36,7 +25,5 @@
config:
provider: mock
model: mock-delegate
persona: 'Delegate the task through the fixed Codex tool.'
persistenceRoot: './.sessions'
persistenceCompression: 'none'
persona: 'This composition test must not start a model turn.'
workspaceContext: false
@@ -0,0 +1,51 @@
#!/usr/bin/env node
/** Inspect the public Codex provider composition without invoking the product. */
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
import type {} from '@deepseek-ai/dsh-subagent'
import type {} from '@deepseek-ai/dsh-tools'
const configPath = process.argv[2]
if (configPath === undefined) {
throw new Error('subagent-codex Loader composition driver requires a config path')
}
let starts = 0
const ctx = await boot(
'subagent-codex-loader-composition',
resolveConfigPath(configPath, undefined),
undefined,
(hostCtx) => {
hostCtx.on('subagent/start', () => {
starts += 1
})
},
)
try {
const provider = ctx.subagents.getProvider('codex')
if (provider === undefined) throw new Error('Codex provider was not registered')
const tool = ctx.tools.schemas().find(schema => schema.name === 'subagent_codex')
if (tool === undefined) throw new Error('subagent_codex tool was not registered')
const properties = tool.parameters.properties
if (typeof properties !== 'object' || properties === null || Array.isArray(properties)) {
throw new Error('subagent_codex tool has invalid parameter properties')
}
process.stdout.write(`${JSON.stringify({
providers: ctx.subagents.list(),
provider: {
name: provider.name,
capabilities: provider.capabilities,
inheritsParentContext: provider.inheritsParentContext,
},
tool: {
name: tool.name,
parameterNames: Object.keys(properties).sort(),
required: tool.parameters.required,
},
starts,
})}\n`)
} finally {
await ctx.fiber.dispose()
}
@@ -1,102 +1,22 @@
/** Deterministic parent model and process-quiescence observer for the Codex Loader snapshot. */
/** Parent adapter that fails if the composition-only Loader test starts a turn. */
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import type { Context } from 'cordis'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm'
import type {
SubprocessHandle,
SubprocessSpawnSpec,
} from '@deepseek-ai/dsh-subprocess'
import { LlmAdapter } from '@deepseek-ai/dsh-llm'
const CODEX_TASK = 'Return the Loader snapshot sentinel exactly.'
const QUIESCENCE_FILE = '.codex-quiescence.json'
function toolResultText(options: GenerateOptions): string {
return options.messages.at(-1)?.content
.filter(block => block.type === 'tool-result')
.flatMap(block => block.content)
.filter(block => block.type === 'text')
.map(block => block.text)
.join('') ?? ''
}
class CodexDelegatingAdapter extends LlmAdapter {
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
const result = toolResultText(options)
if (result.length === 0) {
const args = JSON.stringify({
description: 'Codex Loader snapshot',
prompt: CODEX_TASK,
})
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
yield {
type: 'tool-call-delta',
index: 0,
id: CallId('call-codex-loader'),
name: 'subagent_codex',
argumentsDelta: args,
}
yield {
type: 'block-end',
index: 0,
block: {
type: 'tool-call',
id: CallId('call-codex-loader'),
name: 'subagent_codex',
arguments: args,
},
}
yield { type: 'usage', usage: { inputTokens: 10, outputTokens: 5 } }
yield { type: 'finish', reason: { kind: 'tool-calls' } }
return
}
const reply = `Codex child returned: ${result}`
yield { type: 'block-start', index: 0, blockType: 'text' }
yield { type: 'text-delta', index: 0, text: reply }
yield { type: 'block-end', index: 0, block: { type: 'text', text: reply } }
yield { type: 'usage', usage: { inputTokens: 10, outputTokens: reply.length } }
yield { type: 'finish', reason: { kind: 'stop' } }
class CompositionOnlyAdapter extends LlmAdapter {
async * stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
throw new Error('subagent-codex Loader composition must not invoke a model')
}
}
interface ObservedProcess {
readonly spec: SubprocessSpawnSpec
readonly handle: SubprocessHandle
}
export const name = 'codex-loader-snapshot-fixture'
export const inject = ['llm', 'subprocess']
export const name = 'codex-loader-composition-fixture'
export const inject = ['llm']
/**
* Register the deterministic parent adapter and record whether every spawned
* product tree was already quiet when the assembled application disposed.
* @param ctx - Loader context supplying the LLM and subprocess seams.
* Register a parent adapter solely so the host composition is complete.
* @param ctx - Loader context supplying the LLM seam.
*/
export function apply(ctx: Context): void {
ctx.llm.registerAdapter(['mock'], new CodexDelegatingAdapter())
ctx.effect(() => {
const observed: ObservedProcess[] = []
const originalSpawn = ctx.subprocess.spawn.bind(ctx.subprocess)
ctx.subprocess.spawn = (spec: SubprocessSpawnSpec): SubprocessHandle => {
const handle = originalSpawn(spec)
observed.push({ spec, handle })
return handle
}
return async () => {
ctx.subprocess.spawn = originalSpawn
const alreadyExited = AbortSignal.abort()
const processes = await Promise.all(observed.map(async ({ spec, handle }) => ({
argv: [...spec.argv],
quiescent: await handle.waitForExit(alreadyExited),
outcome: await handle.done,
})))
await writeFile(
join(process.cwd(), QUIESCENCE_FILE),
`${JSON.stringify({ processes })}\n`,
)
}
}, 'codex Loader snapshot process observer')
ctx.llm.registerAdapter(['mock'], new CompositionOnlyAdapter())
}