Keep provider adapters private
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user