Keep provider adapters private

This commit is contained in:
pku-xht
2026-08-05 00:26:39 +08:00
parent 2cf637480c
commit 34b6cb91ed
5 changed files with 28 additions and 33 deletions

View File

@@ -76,11 +76,8 @@ export class ManagedClaudeCodeProcess implements SpawnedProcess {
* @param child - shared handle that remains the process-tree authority. * @param child - shared handle that remains the process-tree authority.
*/ */
constructor(private readonly child: SubprocessHandle) { constructor(private readonly child: SubprocessHandle) {
if (child.stdin === undefined || child.stdout === undefined) { this.stdin = child.stdin as NonNullable<SubprocessHandle['stdin']>
throw new Error('subagent-claude-code: SDK child requires piped stdin and stdout') this.stdout = child.stdout as NonNullable<SubprocessHandle['stdout']>
}
this.stdin = child.stdin
this.stdout = child.stdout
// EventEmitter gives `error` special throw semantics without a listener. // EventEmitter gives `error` special throw semantics without a listener.
// The SDK attaches its listener synchronously after custom spawn returns, // The SDK attaches its listener synchronously after custom spawn returns,
// while this no-op also contains an already-rejected spawn handle. // while this no-op also contains an already-rejected spawn handle.

View File

@@ -20,7 +20,6 @@ import { SessionId } from '@deepseek-ai/dsh-session'
import { import {
settleRunResult, settleRunResult,
subprocessRunHandle, subprocessRunHandle,
thrownError,
type SubagentResult, type SubagentResult,
type SubagentRun, type SubagentRun,
type SubagentStartRequest, type SubagentStartRequest,
@@ -39,6 +38,8 @@ import {
/** Default POSIX grace between subprocess termination tiers. */ /** Default POSIX grace between subprocess termination tiers. */
export const DEFAULT_DISPOSE_GRACE_MS = 3_000 export const DEFAULT_DISPOSE_GRACE_MS = 3_000
/* jscpd:ignore-start -- sibling providers intentionally keep product-private
* run inputs and error normalization instead of adding a shared lifecycle owner. */
/** Fully resolved inputs for one official Claude Agent SDK query. */ /** Fully resolved inputs for one official Claude Agent SDK query. */
export interface ClaudeCodeRunSpec { export interface ClaudeCodeRunSpec {
/** Parent Session workspace supplied to the SDK and real CLI. */ /** Parent Session workspace supplied to the SDK and real CLI. */
@@ -53,6 +54,12 @@ export interface ClaudeCodeRunSpec {
readonly onError?: (error: Error, stopReason: SubagentStopReason) => void readonly onError?: (error: Error, stopReason: SubagentStopReason) => void
} }
function thrown(value: unknown): Error {
/* v8 ignore next -- typed SDK and subprocess failures reject with Error. */
return value instanceof Error ? value : new Error(String(value))
}
/* jscpd:ignore-end */
/** /**
* Validate and preserve the one-shot task before crossing the SDK boundary. * Validate and preserve the one-shot task before crossing the SDK boundary.
* @param prompt - task content accepted from the shared subagent service. * @param prompt - task content accepted from the shared subagent service.
@@ -131,7 +138,7 @@ export async function disposeClaudeCodeChild(
try { try {
query?.close() query?.close()
} catch (error: unknown) { } catch (error: unknown) {
failures.push(thrownError(error)) failures.push(thrown(error))
} }
if (child.pid > 0) { if (child.pid > 0) {
@@ -139,13 +146,13 @@ export async function disposeClaudeCodeChild(
try { try {
await child.waitForExit() await child.waitForExit()
} catch (error: unknown) { } catch (error: unknown) {
failures.push(thrownError(error)) failures.push(thrown(error))
} }
} }
try { try {
await child.done await child.done
} catch (error: unknown) { } catch (error: unknown) {
failures.push(thrownError(error)) failures.push(thrown(error))
} }
const firstFailure = failures[0] const firstFailure = failures[0]
@@ -234,7 +241,7 @@ export async function startClaudeCodeRun(
await disposeClaudeCodeChild(query, child) await disposeClaudeCodeChild(query, child)
} catch (disposeError: unknown) { } catch (disposeError: unknown) {
throw new AggregateError( throw new AggregateError(
[thrownError(error), thrownError(disposeError)], [thrown(error), thrown(disposeError)],
'subagent-claude-code: startup failed and CLI cleanup also failed', 'subagent-claude-code: startup failed and CLI cleanup also failed',
) )
} }
@@ -243,7 +250,7 @@ export async function startClaudeCodeRun(
query.close() query.close()
} catch (disposeError: unknown) { } catch (disposeError: unknown) {
throw new AggregateError( throw new AggregateError(
[thrownError(error), thrownError(disposeError)], [thrown(error), thrown(disposeError)],
'subagent-claude-code: startup failed and query cleanup also failed', 'subagent-claude-code: startup failed and query cleanup also failed',
) )
} }
@@ -252,7 +259,7 @@ export async function startClaudeCodeRun(
if (cancelledBeforeCleanup || request.signal.aborted) { if (cancelledBeforeCleanup || request.signal.aborted) {
throw new Error('subagent-claude-code: request was aborted before SDK startup') throw new Error('subagent-claude-code: request was aborted before SDK startup')
} }
throw thrownError(error) throw thrown(error)
} }
const publishedQuery = query const publishedQuery = query

View File

@@ -445,7 +445,7 @@ describe('official spawn projection', () => {
expect(process.kill('SIGTERM')).toBe(false) expect(process.kill('SIGTERM')).toBe(false)
}) })
it('emits spawn errors and rejects handles without the required pipes', async () => { it('emits spawn errors', async () => {
const child = fakeChild() const child = fakeChild()
const process = new ManagedClaudeCodeProcess(child.handle) const process = new ManagedClaudeCodeProcess(child.handle)
const errorListener = vi.fn() const errorListener = vi.fn()
@@ -459,15 +459,6 @@ describe('official spawn projection', () => {
message: 'spawn boom', message: 'spawn boom',
})) }))
expect(removed).not.toHaveBeenCalled() expect(removed).not.toHaveBeenCalled()
const missingStdin = fakeChild({ stdin: undefined })
Object.defineProperty(missingStdin.handle, 'stdin', { value: undefined })
expect(() => new ManagedClaudeCodeProcess(missingStdin.handle))
.toThrow('requires piped stdin and stdout')
const missingStdout = fakeChild({ stdout: undefined })
Object.defineProperty(missingStdout.handle, 'stdout', { value: undefined })
expect(() => new ManagedClaudeCodeProcess(missingStdout.handle))
.toThrow('requires piped stdin and stdout')
}) })
it('exposes a settled direct-child exit code', async () => { it('exposes a settled direct-child exit code', async () => {

View File

@@ -13,7 +13,6 @@ import { SessionId } from '@deepseek-ai/dsh-session'
import { import {
settleRunResult, settleRunResult,
subprocessRunHandle, subprocessRunHandle,
thrownError,
type SubagentResult, type SubagentResult,
type SubagentRun, type SubagentRun,
type SubagentStartRequest, type SubagentStartRequest,
@@ -39,6 +38,11 @@ export interface CodexRunSpec {
readonly onError?: (error: Error, stopReason: SubagentStopReason) => void readonly onError?: (error: Error, stopReason: SubagentStopReason) => void
} }
function thrown(value: unknown): Error {
/* v8 ignore next -- typed subprocess/wire failures reject with Error. */
return value instanceof Error ? value : new Error(String(value))
}
/** /**
* Validate and preserve the one-shot task before crossing the process seam. * Validate and preserve the one-shot task before crossing the process seam.
* @param prompt - task content accepted from the shared subagent service. * @param prompt - task content accepted from the shared subagent service.
@@ -120,7 +124,7 @@ export async function startCodexRun(
'subagent-codex: app-server exited before the run settled ' 'subagent-codex: app-server exited before the run settled '
+ `(code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`, + `(code ${String(outcome.exitCode)}, signal ${String(outcome.signal)})`,
)), )),
(error: unknown) => Promise.reject(thrownError(error)), (error: unknown) => Promise.reject(thrown(error)),
) )
// A normal post-result dispose also closes the process. Keep that expected // A normal post-result dispose also closes the process. Keep that expected
// late rejection observed after the result race has already settled. // late rejection observed after the result race has already settled.
@@ -145,14 +149,14 @@ export async function startCodexRun(
await disposeProcess() await disposeProcess()
} catch (disposeError: unknown) { } catch (disposeError: unknown) {
throw new AggregateError( throw new AggregateError(
[thrownError(error), thrownError(disposeError)], [thrown(error), thrown(disposeError)],
'subagent-codex: startup failed and app-server cleanup also failed', 'subagent-codex: startup failed and app-server cleanup also failed',
) )
} }
if (runAbort.signal.aborted) { if (runAbort.signal.aborted) {
throw new Error('subagent-codex: request was aborted before run publication') throw new Error('subagent-codex: request was aborted before run publication')
} }
throw thrownError(error) throw thrown(error)
} }
const collectOutput = (): ContentBlock[] => wire.collectOutput() const collectOutput = (): ContentBlock[] => wire.collectOutput()

View File

@@ -119,12 +119,8 @@ export function resolveChildCwd(prefix: string, configured: string | undefined,
return assertUsableCwd(prefix, 'parent session cwd', parentCwd) return assertUsableCwd(prefix, 'parent session cwd', parentCwd)
} }
/** /** Normalize an unknown thrown value to an Error (the catch binding is `unknown`). */
* Normalize an unknown thrown value to an Error. function toError(value: unknown): Error {
* @param value - the unknown catch binding.
* @returns the original Error or a defensive Error wrapper.
*/
export function thrownError(value: unknown): Error {
// The rejecting surfaces (wire clients, spawn failures) only throw // The rejecting surfaces (wire clients, spawn failures) only throw
// `Error`s; the `String(value)` arm is a defensive fallback for a non-Error // `Error`s; the `String(value)` arm is a defensive fallback for a non-Error
// throw the typed surfaces cannot produce. // throw the typed surfaces cannot produce.
@@ -168,7 +164,7 @@ export async function settleRunResult(parts: RunResultSettlement): Promise<Subag
if (parts.cancelled()) return { output: parts.collectOutput(), stopReason: 'aborted' } if (parts.cancelled()) return { output: parts.collectOutput(), stopReason: 'aborted' }
// Flatten post-publication transport failures while preserving diagnostics. // Flatten post-publication transport failures while preserving diagnostics.
try { try {
parts.onError?.(thrownError(error), 'error') parts.onError?.(toError(error), 'error')
} catch { } catch {
// The diagnostic sink cannot reject the run result. // The diagnostic sink cannot reject the run result.
} }