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)