feat(subagent): add Claude Code provider
This commit is contained in:
54 files changed
+3014
-220
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user