refactor(subagent-subprocess): shared provider skeleton for out-of-process backends

The duplication gate flagged three ACP/SDK clones; the shared halves move
into dsh-subagent-subprocess as provider.ts: NO_START_CAPABILITIES (frozen
all-false advertisement), assertPositiveFinite (prefix-parameterized timing
validation), settleRunResult (never-reject result settlement with contained
onError sink and listener hygiene), and subprocessRunHandle (idempotent
dispose publication). Both backends now compose these; the previously
unreachable cancelled-rejection branch is directly unit-tested at the
library level instead of v8-ignored in each backend. knip learns the
subagent-sdk workspace (e2e entry outside vitest unit includes).
This commit is contained in:
Tianyi Cui
2026-07-27 04:58:11 +08:00
parent 34aabc4183
commit 441110f6da
14 changed files with 340 additions and 96 deletions
+5 -12
View File
@@ -13,7 +13,7 @@
import type { Context } from 'cordis'
import z from 'schemastery'
import type { SubagentCapabilities, SubagentProvider, SubagentStartRequest } from '@deepseek-ai/dsh-subagent'
import { resolveChildCwd, validateConfiguredCwd } from '@deepseek-ai/dsh-subagent-subprocess'
import { assertPositiveFinite, NO_START_CAPABILITIES, resolveChildCwd, validateConfiguredCwd } from '@deepseek-ai/dsh-subagent-subprocess'
import {
DEFAULT_DISPOSE_EOF_GRACE_MS,
DEFAULT_DISPOSE_GRACE_MS,
@@ -79,13 +79,6 @@ export const Config: z<Config> = z.object({
disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS),
})
/** A timing bound must be a positive finite number (it bounds a teardown wait). */
function assertPositiveFinite(name: string, value: number): void {
if (!Number.isFinite(value) || value <= 0) {
throw new Error(`subagent-sdk: ${name} must be a positive finite number`)
}
}
/** The shape after schemastery applied the defaults (cwd has none). */
type ResolvedConfig = Required<Omit<Config, 'cwd'>> & Pick<Config, 'cwd'>
@@ -95,7 +88,7 @@ type ResolvedConfig = Required<Omit<Config, 'cwd'>> & Pick<Config, 'cwd'>
* service rejects a request needing any of them before `start` runs).
*/
class SdkProvider implements SubagentProvider {
readonly capabilities: SubagentCapabilities = { outputSchema: false, depthLimit: false, toolFilter: false, persona: false }
readonly capabilities: SubagentCapabilities = NO_START_CAPABILITIES
// Context contract: an out-of-process SDK child starts fresh — no parent conversation crosses the process boundary.
readonly inheritsParentContext = false
@@ -125,9 +118,9 @@ class SdkProvider implements SubagentProvider {
export function apply(ctx: Context, config: Config): void {
// schemastery (Config) has already filled every defaulted field.
const resolved = config as ResolvedConfig
assertPositiveFinite('shutdownTimeoutMs', resolved.shutdownTimeoutMs)
assertPositiveFinite('disposeEofGraceMs', resolved.disposeEofGraceMs)
assertPositiveFinite('disposeGraceMs', resolved.disposeGraceMs)
assertPositiveFinite('subagent-sdk', 'shutdownTimeoutMs', resolved.shutdownTimeoutMs)
assertPositiveFinite('subagent-sdk', 'disposeEofGraceMs', resolved.disposeEofGraceMs)
assertPositiveFinite('subagent-sdk', 'disposeGraceMs', resolved.disposeGraceMs)
// Interpret a relative configured cwd against the harness launch directory
// ONCE, at load, and fail a misconfigured directory here — not per start.
const configuredCwd = validateConfiguredCwd('subagent-sdk', resolved.cwd)
+22 -33
View File
@@ -14,7 +14,7 @@ import { DeepSeekHarness, type HarnessNotification } from '@deepseek-ai/dsh-sdk-
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
import type { SubagentResult, SubagentRun, SubagentStartRequest, SubagentStopReason } from '@deepseek-ai/dsh-subagent'
import { buildChildEnv } from '@deepseek-ai/dsh-subagent-subprocess'
import { buildChildEnv, settleRunResult, subprocessRunHandle } from '@deepseek-ai/dsh-subagent-subprocess'
/** Resolved spawn spec for an SDK runtime child process (no defaults — see Config). */
export interface SdkRunSpec {
@@ -175,43 +175,32 @@ export async function startSdkRun(request: SubagentStartRequest, spec: SdkRunSpe
return text.length > 0 ? [{ type: 'text', text }] : []
}
const result: Promise<SubagentResult> = (async (): Promise<SubagentResult> => {
try {
// Race the child turn against local cancellation; the shared settlement
// flattens failures under the seam's never-reject contract.
const result: Promise<SubagentResult> = settleRunResult({
attempt: async () => {
const turn = await Promise.race([
harness.session(childSessionId).run(request.prompt, { onNotification: observe }),
cancelSettled.then(() => 'cancelled' as const),
])
if (turn === 'cancelled') return { output: collectOutput(), stopReason: 'aborted' }
return { output: collectOutput(), stopReason: sdkStopReason(turn.reason) }
} catch (error: unknown) {
// Cover a transport rejection already queued when cancellation arrives.
/* v8 ignore next */
if (flags.cancelled) return { output: collectOutput(), stopReason: 'aborted' }
// Flatten post-publication transport failures while preserving diagnostics.
try {
spec.onError?.(toError(error), 'error')
} catch {
// The diagnostic sink cannot reject the run result.
}
return { output: collectOutput(), stopReason: 'error' }
} finally {
request.signal.removeEventListener('abort', onAbort)
}
})()
let disposal: Promise<void> | undefined
return {
id,
localAgent: undefined,
result,
dispose(): Promise<void> {
if (disposal !== undefined) return disposal
request.signal.removeEventListener('abort', onAbort)
// There is no wire-level prompt cancel: settle the result locally, then
// the bounded shutdown request + dispose ladder tears the child down.
requestCancel()
disposal = harness.close()
return disposal
},
}
collectOutput,
cancelled: () => flags.cancelled,
onError: spec.onError,
signal: request.signal,
onAbort,
})
// There is no wire-level prompt cancel: dispose settles the result locally,
// then the bounded shutdown request + dispose ladder tears the child down.
return subprocessRunHandle({
id,
result,
signal: request.signal,
onAbort,
requestCancel,
teardown: () => harness.close(),
})
}