feat(subagent): add Claude Code provider
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Fixed Claude Code one-shot subagent provider. Every accepted run invokes
|
||||
* the official Agent SDK in the delegating Session's workspace and places
|
||||
* the SDK-spawned real CLI under the shared subprocess owner.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent-claude-code
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import {
|
||||
assertPositiveFinite,
|
||||
NO_START_CAPABILITIES,
|
||||
resolveChildCwd,
|
||||
type ResolvedSubagentStartRequest,
|
||||
type SubagentCapabilities,
|
||||
type SubagentProvider,
|
||||
} from '@deepseek-ai/dsh-subagent'
|
||||
import {
|
||||
DEFAULT_DISPOSE_GRACE_MS,
|
||||
startClaudeCodeRun,
|
||||
type ClaudeCodeRunSpec,
|
||||
} from './run.ts'
|
||||
|
||||
export const name = 'subagent-claude-code'
|
||||
export const inject = ['subagents', 'subprocess']
|
||||
|
||||
/* jscpd:ignore-start -- sibling product providers intentionally expose the
|
||||
* same two deployment-owned fields without adding a shared config owner. */
|
||||
/** Deployment-owned environment and process-release bound. */
|
||||
export interface Config {
|
||||
/**
|
||||
* Explicit environment entries layered over the subprocess seam's
|
||||
* credential-scrubbed parent environment.
|
||||
*/
|
||||
env?: Record<string, string>
|
||||
/** Grace in milliseconds for Claude Code process-tree termination. */
|
||||
disposeGraceMs?: number
|
||||
}
|
||||
|
||||
export const Config: z<Config> = z.object({
|
||||
env: z.dict(z.string()).default({}),
|
||||
disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS),
|
||||
})
|
||||
|
||||
type ResolvedConfig = Required<Config>
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
/* jscpd:ignore-start -- Cordis registration and shared-seam plumbing mirror
|
||||
* the Codex sibling; each product's lifecycle remains package-private. */
|
||||
class ClaudeCodeProvider implements SubagentProvider {
|
||||
readonly name = 'claude-code'
|
||||
readonly capabilities: SubagentCapabilities = NO_START_CAPABILITIES
|
||||
readonly inheritsParentContext = false
|
||||
|
||||
constructor(
|
||||
private readonly ctx: Context,
|
||||
private readonly config: ResolvedConfig,
|
||||
) {}
|
||||
|
||||
start(request: ResolvedSubagentStartRequest) {
|
||||
const spec: ClaudeCodeRunSpec = {
|
||||
cwd: resolveChildCwd(
|
||||
'subagent-claude-code',
|
||||
undefined,
|
||||
request.parent.session.header.cwd,
|
||||
),
|
||||
env: this.config.env,
|
||||
disposeGraceMs: this.config.disposeGraceMs,
|
||||
spawn: spawnSpec => this.ctx.subprocess.spawn(spawnSpec),
|
||||
onError: (error, stopReason) => {
|
||||
this.ctx.logger.warn(
|
||||
`subagent-claude-code: child run failed (${stopReason}): ${error.message}`,
|
||||
)
|
||||
},
|
||||
}
|
||||
return startClaudeCodeRun(request, spec)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the fixed `claude-code` provider.
|
||||
* @param ctx - context carrying shared subagent and subprocess services.
|
||||
* @param config - explicit child environment and disposal grace.
|
||||
*/
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const resolved = config as ResolvedConfig
|
||||
assertPositiveFinite(
|
||||
'subagent-claude-code',
|
||||
'disposeGraceMs',
|
||||
resolved.disposeGraceMs,
|
||||
)
|
||||
ctx.subagents.registerProvider(new ClaudeCodeProvider(ctx, resolved))
|
||||
}
|
||||
/* jscpd:ignore-end */
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Package-owned invariant companion for
|
||||
* `@deepseek-ai/dsh-subagent-claude-code`.
|
||||
* @module @deepseek-ai/dsh-subagent-claude-code/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-subagent-claude-code'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'subagent-claude-code-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: lifecycle pairing belongs to the shared subagent
|
||||
* service and process-tree ownership belongs to the subprocess service.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - plugin context carrying the invariant registry.
|
||||
* @returns the installed registration's disposer.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Projection from the shared managed-process handle to the official Claude
|
||||
* Agent SDK's custom-spawn process interface.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent-claude-code/process
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events'
|
||||
import type {
|
||||
SpawnedProcess,
|
||||
SpawnOptions,
|
||||
} from '@anthropic-ai/claude-agent-sdk'
|
||||
import type {
|
||||
SubprocessHandle,
|
||||
SubprocessSpawnSpec,
|
||||
} from '@deepseek-ai/dsh-subprocess'
|
||||
|
||||
function thrown(value: unknown): Error {
|
||||
/* v8 ignore next -- the subprocess seam rejects with Error. */
|
||||
return value instanceof Error ? value : new Error(String(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the SDK environment to the shared subprocess seam's defined-value
|
||||
* overlay without changing the effective child environment.
|
||||
* @param env - SDK-composed child environment.
|
||||
* @returns entries whose values survive Node's subprocess environment.
|
||||
*/
|
||||
export function definedEnvironment(
|
||||
env: SpawnOptions['env'],
|
||||
): Record<string, string> {
|
||||
const defined: Record<string, string> = {}
|
||||
for (const [name, value] of Object.entries(env)) {
|
||||
if (value !== undefined) defined[name] = value
|
||||
}
|
||||
return defined
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate one official SDK spawn request to the shared process owner.
|
||||
* @param options - command, arguments, workspace, environment, and forwarded signal from the SDK.
|
||||
* @param graceMs - process-tree termination grace.
|
||||
* @returns the fully explicit shared subprocess request.
|
||||
*/
|
||||
export function claudeSpawnSpec(
|
||||
options: SpawnOptions,
|
||||
graceMs: number,
|
||||
): SubprocessSpawnSpec {
|
||||
if (options.cwd === undefined || options.cwd.length === 0) {
|
||||
throw new Error('subagent-claude-code: SDK spawn request omitted its workspace')
|
||||
}
|
||||
return {
|
||||
argv: [options.command, ...options.args],
|
||||
cwd: options.cwd,
|
||||
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'inherit' },
|
||||
graceMs,
|
||||
signal: options.signal,
|
||||
env: definedEnvironment(options.env),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SDK-facing view of one shared managed process. Protocol transport remains
|
||||
* in the official SDK; this adapter only projects streams and exit events.
|
||||
*/
|
||||
export class ManagedClaudeCodeProcess implements SpawnedProcess {
|
||||
readonly stdin
|
||||
readonly stdout
|
||||
private readonly events = new EventEmitter()
|
||||
private exitCodeValue: number | null = null
|
||||
private signalCodeValue: NodeJS.Signals | null = null
|
||||
private killRequested = false
|
||||
|
||||
/**
|
||||
* Project a managed process with piped stdin and stdout.
|
||||
* @param child - shared handle that remains the process-tree authority.
|
||||
*/
|
||||
constructor(private readonly child: SubprocessHandle) {
|
||||
if (child.stdin === undefined || child.stdout === undefined) {
|
||||
throw new Error('subagent-claude-code: SDK child requires piped stdin and stdout')
|
||||
}
|
||||
this.stdin = child.stdin
|
||||
this.stdout = child.stdout
|
||||
// EventEmitter gives `error` special throw semantics without a listener.
|
||||
// The SDK attaches its listener synchronously after custom spawn returns,
|
||||
// while this no-op also contains an already-rejected spawn handle.
|
||||
this.events.on('error', () => {})
|
||||
void child.done.then(
|
||||
(outcome) => {
|
||||
this.exitCodeValue = outcome.exitCode
|
||||
this.signalCodeValue = outcome.signal
|
||||
this.events.emit('exit', outcome.exitCode, outcome.signal)
|
||||
},
|
||||
(error: unknown) => {
|
||||
this.events.emit('error', thrown(error))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Whether the SDK has requested managed tree termination. */
|
||||
get killed(): boolean {
|
||||
return this.killRequested
|
||||
}
|
||||
|
||||
/** Direct-child exit code, or null while running or after signal exit. */
|
||||
get exitCode(): number | null {
|
||||
return this.exitCodeValue
|
||||
}
|
||||
|
||||
/** Direct-child terminating signal, if any. */
|
||||
get signalCode(): NodeJS.Signals | null {
|
||||
return this.signalCodeValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Route the SDK's termination request to the tree-scoped process owner.
|
||||
* @param _signal - SDK-selected signal; the shared seam owns its escalation ladder.
|
||||
* @returns false only after exit or a previous termination request.
|
||||
*/
|
||||
kill(_signal: NodeJS.Signals): boolean {
|
||||
if (
|
||||
this.killRequested
|
||||
|| this.exitCodeValue !== null
|
||||
|| this.signalCodeValue !== null
|
||||
) {
|
||||
return false
|
||||
}
|
||||
this.killRequested = true
|
||||
this.child.terminate()
|
||||
return true
|
||||
}
|
||||
|
||||
/** Register a persistent process lifecycle listener. */
|
||||
on(
|
||||
event: 'exit' | 'error',
|
||||
listener: ((code: number | null, signal: NodeJS.Signals | null) => void)
|
||||
| ((error: Error) => void),
|
||||
): void {
|
||||
this.events.on(event, listener)
|
||||
}
|
||||
|
||||
/** Register a one-shot process lifecycle listener. */
|
||||
once(
|
||||
event: 'exit' | 'error',
|
||||
listener: ((code: number | null, signal: NodeJS.Signals | null) => void)
|
||||
| ((error: Error) => void),
|
||||
): void {
|
||||
this.events.once(event, listener)
|
||||
}
|
||||
|
||||
/** Remove a process lifecycle listener. */
|
||||
off(
|
||||
event: 'exit' | 'error',
|
||||
listener: ((code: number | null, signal: NodeJS.Signals | null) => void)
|
||||
| ((error: Error) => void),
|
||||
): void {
|
||||
this.events.off(event, listener)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* One-shot Claude Code lifecycle: invoke the official Agent SDK, place its
|
||||
* real CLI process under the shared subprocess owner, map only strict SDK
|
||||
* success to completion, and dispose to whole-tree quiescence.
|
||||
*
|
||||
* @module @deepseek-ai/dsh-subagent-claude-code/run
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import {
|
||||
query as officialQuery,
|
||||
type Options,
|
||||
type Query,
|
||||
type SDKMessage,
|
||||
type SDKResultMessage,
|
||||
type SpawnOptions,
|
||||
} from '@anthropic-ai/claude-agent-sdk'
|
||||
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import {
|
||||
settleRunResult,
|
||||
subprocessRunHandle,
|
||||
type SubagentResult,
|
||||
type SubagentRun,
|
||||
type SubagentStartRequest,
|
||||
type SubagentStopReason,
|
||||
} from '@deepseek-ai/dsh-subagent'
|
||||
import {
|
||||
scrubbedParentEnv,
|
||||
type SubprocessHandle,
|
||||
type SubprocessSpawnSpec,
|
||||
} from '@deepseek-ai/dsh-subprocess'
|
||||
import {
|
||||
claudeSpawnSpec,
|
||||
ManagedClaudeCodeProcess,
|
||||
} from './process.ts'
|
||||
|
||||
/** Default POSIX grace between subprocess termination tiers. */
|
||||
export const DEFAULT_DISPOSE_GRACE_MS = 3_000
|
||||
|
||||
/** Largest delay Node schedules without collapsing it to one millisecond. */
|
||||
const MAX_TIMER_DELAY_MS = 2_147_483_647n
|
||||
|
||||
/**
|
||||
* Bound final exit observation at twice a positive finite grace without
|
||||
* narrowing the public config to Node's single-timer integer range.
|
||||
*/
|
||||
function doubledGraceWindow(graceMs: number): {
|
||||
readonly signal: AbortSignal
|
||||
readonly cancel: () => void
|
||||
} {
|
||||
const whole = Math.floor(graceMs)
|
||||
let remaining = BigInt(whole) * 2n
|
||||
+ BigInt(Math.ceil((graceMs - whole) * 2))
|
||||
const controller = new AbortController()
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
const arm = (): void => {
|
||||
const chunk = remaining > MAX_TIMER_DELAY_MS
|
||||
? MAX_TIMER_DELAY_MS
|
||||
: remaining
|
||||
remaining -= chunk
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined
|
||||
if (remaining === 0n) {
|
||||
controller.abort()
|
||||
} else {
|
||||
arm()
|
||||
}
|
||||
}, Number(chunk))
|
||||
}
|
||||
arm()
|
||||
return {
|
||||
signal: controller.signal,
|
||||
cancel: () => {
|
||||
if (timer === undefined) return
|
||||
clearTimeout(timer)
|
||||
timer = undefined
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type QueryFactory = (params: {
|
||||
prompt: string
|
||||
options: Options
|
||||
}) => Query
|
||||
|
||||
/** Fully resolved inputs for one official Claude Agent SDK query. */
|
||||
export interface ClaudeCodeRunSpec {
|
||||
/** Parent Session workspace supplied to the SDK and real CLI. */
|
||||
readonly cwd: string
|
||||
/** Explicit deployment/test environment layered after shared scrubbing. */
|
||||
readonly env: Record<string, string>
|
||||
/** Subprocess termination grace and final tree-exit bound. */
|
||||
readonly disposeGraceMs: number
|
||||
/** Shared subprocess service spawn operation. */
|
||||
readonly spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle
|
||||
/** Official query entrypoint; replaced only by package-local unit tests. */
|
||||
readonly query?: QueryFactory
|
||||
/** Diagnostic sink for a post-publication error flattened into a result. */
|
||||
readonly onError?: (error: Error, stopReason: SubagentStopReason) => void
|
||||
}
|
||||
|
||||
function thrown(value: unknown): Error {
|
||||
/* v8 ignore next -- SDK and subprocess failures reject with Error. */
|
||||
return value instanceof Error ? value : new Error(String(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and preserve the one-shot task before crossing the SDK boundary.
|
||||
* @param prompt - task content accepted from the shared subagent service.
|
||||
* @returns the exact text sequence as one SDK prompt.
|
||||
*/
|
||||
export function textTask(prompt: readonly ContentBlock[]): string {
|
||||
if (prompt.length === 0) {
|
||||
throw new Error('subagent-claude-code: the one-shot task must contain only text blocks')
|
||||
}
|
||||
const texts: string[] = []
|
||||
for (const block of prompt) {
|
||||
if (block.type !== 'text') {
|
||||
throw new Error('subagent-claude-code: the one-shot task must contain only text blocks')
|
||||
}
|
||||
texts.push(block.text)
|
||||
}
|
||||
if (texts.every(text => text.trim().length === 0)) {
|
||||
throw new Error('subagent-claude-code: the one-shot task must not be empty')
|
||||
}
|
||||
return texts.join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* Strictly derive the only SDK result that can complete a shared run.
|
||||
* @param message - an official discriminated result union.
|
||||
* @returns exact final text for a successful, non-error result.
|
||||
*/
|
||||
export function successfulResult(message: SDKResultMessage): string {
|
||||
if (
|
||||
message.subtype !== 'success'
|
||||
|| message.is_error
|
||||
|| message.result.trim().length === 0
|
||||
) {
|
||||
const detail = message.subtype === 'success'
|
||||
? 'success result was marked as an error or contained no answer'
|
||||
: message.errors.join('; ') || message.subtype
|
||||
throw new Error(`subagent-claude-code: Claude Code failed: ${detail}`)
|
||||
}
|
||||
return message.result
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the complete SDK stream and require one strict success plus normal
|
||||
* iterator completion.
|
||||
* @param query - published official SDK query.
|
||||
* @param setOutput - captures the candidate result for error diagnostics.
|
||||
* @returns the completed shared result.
|
||||
*/
|
||||
export async function consumeClaudeQuery(
|
||||
query: AsyncIterable<SDKMessage>,
|
||||
setOutput: (output: ContentBlock[]) => void,
|
||||
): Promise<SubagentResult> {
|
||||
let answer: string | undefined
|
||||
for await (const message of query) {
|
||||
if (message.type !== 'result') continue
|
||||
answer = successfulResult(message)
|
||||
setOutput([{ type: 'text', text: answer }])
|
||||
}
|
||||
if (answer === undefined) {
|
||||
throw new Error('subagent-claude-code: Claude Code ended without a result')
|
||||
}
|
||||
return {
|
||||
output: [{ type: 'text', text: answer }],
|
||||
stopReason: 'completed',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the official query, terminate the managed process tree, and wait for
|
||||
* the subprocess owner to prove it is gone.
|
||||
* @param query - official SDK query, when creation reached that point.
|
||||
* @param child - shared-service handle that owns the CLI process tree.
|
||||
* @param graceMs - termination grace used to bound final exit observation.
|
||||
*/
|
||||
export async function disposeClaudeCodeChild(
|
||||
query: Pick<Query, 'close'> | undefined,
|
||||
child: SubprocessHandle,
|
||||
graceMs: number,
|
||||
): Promise<void> {
|
||||
const failures: Error[] = []
|
||||
let treeExited = child.pid <= 0
|
||||
try {
|
||||
query?.close()
|
||||
} catch (error: unknown) {
|
||||
failures.push(thrown(error))
|
||||
}
|
||||
|
||||
if (child.pid > 0) {
|
||||
child.terminate()
|
||||
const exitWindow = doubledGraceWindow(graceMs)
|
||||
try {
|
||||
treeExited = await child.waitForExit(exitWindow.signal)
|
||||
if (!treeExited) {
|
||||
failures.push(new Error(
|
||||
'subagent-claude-code: Claude Code process tree did not exit within its dispose window',
|
||||
))
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
failures.push(thrown(error))
|
||||
} finally {
|
||||
exitWindow.cancel()
|
||||
}
|
||||
}
|
||||
if (treeExited) {
|
||||
try {
|
||||
await child.done
|
||||
} catch (error: unknown) {
|
||||
failures.push(thrown(error))
|
||||
}
|
||||
} else {
|
||||
// The bounded tree observation owns teardown completion. Keep a later
|
||||
// direct-child spawn failure observed without turning that bound into an
|
||||
// unbounded wait.
|
||||
void child.done.catch(() => {})
|
||||
}
|
||||
|
||||
const firstFailure = failures[0]
|
||||
if (failures.length === 1 && firstFailure !== undefined) throw firstFailure
|
||||
if (failures.length > 1) {
|
||||
throw new AggregateError(
|
||||
failures,
|
||||
'subagent-claude-code: query and process cleanup failed',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the fixed official SDK options for one one-shot provider run.
|
||||
* @param spec - workspace, environment, process seam, and disposal policy.
|
||||
* @param controller - per-run cancellation owner.
|
||||
* @param capture - receives the real managed child synchronously from the SDK hook.
|
||||
* @returns options that inherit native settings while disabling persistence and user questions.
|
||||
*/
|
||||
export function claudeQueryOptions(
|
||||
spec: ClaudeCodeRunSpec,
|
||||
controller: AbortController,
|
||||
capture: (child: SubprocessHandle) => void,
|
||||
): Options {
|
||||
return {
|
||||
abortController: controller,
|
||||
cwd: spec.cwd,
|
||||
env: { ...scrubbedParentEnv(), ...spec.env },
|
||||
persistSession: false,
|
||||
disallowedTools: ['AskUserQuestion'],
|
||||
spawnClaudeCodeProcess: (options: SpawnOptions) => {
|
||||
const child = spec.spawn(claudeSpawnSpec(options, spec.disposeGraceMs))
|
||||
capture(child)
|
||||
return new ManagedClaudeCodeProcess(child)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start one official Claude Agent SDK query and publish its one-shot run.
|
||||
* @param request - resolved shared subagent request.
|
||||
* @param spec - workspace, environment, process seam, and diagnostic policy.
|
||||
* @returns the published run after both Query and real CLI handle exist.
|
||||
*/
|
||||
export async function startClaudeCodeRun(
|
||||
request: SubagentStartRequest,
|
||||
spec: ClaudeCodeRunSpec,
|
||||
): Promise<SubagentRun> {
|
||||
const prompt = textTask(request.prompt)
|
||||
if (request.signal.aborted) {
|
||||
throw new Error('subagent-claude-code: request was aborted before SDK startup')
|
||||
}
|
||||
|
||||
const controller = new AbortController()
|
||||
const requestCancel = (): void => {
|
||||
if (!controller.signal.aborted) {
|
||||
controller.abort(new Error('subagent-claude-code: run cancelled locally'))
|
||||
}
|
||||
}
|
||||
const onAbort = (): void => { requestCancel() }
|
||||
request.signal.addEventListener('abort', onAbort, { once: true })
|
||||
|
||||
let child: SubprocessHandle | undefined
|
||||
let query: Query | undefined
|
||||
try {
|
||||
query = (spec.query ?? officialQuery)({
|
||||
prompt,
|
||||
options: claudeQueryOptions(spec, controller, (captured) => {
|
||||
child = captured
|
||||
}),
|
||||
})
|
||||
if (child === undefined || child.pid <= 0) {
|
||||
throw new Error(
|
||||
'subagent-claude-code: official SDK did not publish a controllable Claude Code process',
|
||||
)
|
||||
}
|
||||
if (controller.signal.aborted) {
|
||||
throw new Error('subagent-claude-code: request was aborted before SDK startup')
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
request.signal.removeEventListener('abort', onAbort)
|
||||
const cancelledBeforeCleanup = controller.signal.aborted
|
||||
requestCancel()
|
||||
if (child !== undefined) {
|
||||
try {
|
||||
await disposeClaudeCodeChild(query, child, spec.disposeGraceMs)
|
||||
} catch (disposeError: unknown) {
|
||||
throw new AggregateError(
|
||||
[thrown(error), thrown(disposeError)],
|
||||
'subagent-claude-code: startup failed and CLI cleanup also failed',
|
||||
)
|
||||
}
|
||||
} else if (query !== undefined) {
|
||||
try {
|
||||
query.close()
|
||||
} catch (disposeError: unknown) {
|
||||
throw new AggregateError(
|
||||
[thrown(error), thrown(disposeError)],
|
||||
'subagent-claude-code: startup failed and query cleanup also failed',
|
||||
)
|
||||
}
|
||||
}
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- the request can abort while process cleanup is awaited.
|
||||
if (cancelledBeforeCleanup || request.signal.aborted) {
|
||||
throw new Error('subagent-claude-code: request was aborted before SDK startup')
|
||||
}
|
||||
throw thrown(error)
|
||||
}
|
||||
|
||||
let output: ContentBlock[] = []
|
||||
const publishedQuery = query
|
||||
const publishedChild = child
|
||||
const result = settleRunResult({
|
||||
attempt: () => consumeClaudeQuery(publishedQuery, (value) => {
|
||||
output = value
|
||||
}),
|
||||
collectOutput: () => output,
|
||||
cancelled: () => controller.signal.aborted,
|
||||
onError: spec.onError,
|
||||
signal: request.signal,
|
||||
onAbort,
|
||||
})
|
||||
|
||||
return subprocessRunHandle({
|
||||
id: SessionId(randomUUID()),
|
||||
result,
|
||||
signal: request.signal,
|
||||
onAbort,
|
||||
requestCancel,
|
||||
teardown: () => disposeClaudeCodeChild(
|
||||
publishedQuery,
|
||||
publishedChild,
|
||||
spec.disposeGraceMs,
|
||||
),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user