Browse Source

Keep provider adapters private

pku-xht 1 tháng trước cách đây
mục cha
commit
34b6cb91ed

+ 2 - 5
packages/subagent/subagent-claude-code/src/process.ts

@@ -76,11 +76,8 @@ export class ManagedClaudeCodeProcess implements SpawnedProcess {
    * @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
+    this.stdin = child.stdin as NonNullable<SubprocessHandle['stdin']>
+    this.stdout = child.stdout as NonNullable<SubprocessHandle['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.

+ 14 - 7
packages/subagent/subagent-claude-code/src/run.ts

@@ -20,7 +20,6 @@ import { SessionId } from '@deepseek-ai/dsh-session'
 import {
   settleRunResult,
   subprocessRunHandle,
-  thrownError,
   type SubagentResult,
   type SubagentRun,
   type SubagentStartRequest,
@@ -39,6 +38,8 @@ import {
 /** Default POSIX grace between subprocess termination tiers. */
 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. */
 export interface ClaudeCodeRunSpec {
   /** Parent Session workspace supplied to the SDK and real CLI. */
@@ -53,6 +54,12 @@ export interface ClaudeCodeRunSpec {
   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.
  * @param prompt - task content accepted from the shared subagent service.
@@ -131,7 +138,7 @@ export async function disposeClaudeCodeChild(
   try {
     query?.close()
   } catch (error: unknown) {
-    failures.push(thrownError(error))
+    failures.push(thrown(error))
   }
 
   if (child.pid > 0) {
@@ -139,13 +146,13 @@ export async function disposeClaudeCodeChild(
     try {
       await child.waitForExit()
     } catch (error: unknown) {
-      failures.push(thrownError(error))
+      failures.push(thrown(error))
     }
   }
   try {
     await child.done
   } catch (error: unknown) {
-    failures.push(thrownError(error))
+    failures.push(thrown(error))
   }
 
   const firstFailure = failures[0]
@@ -234,7 +241,7 @@ export async function startClaudeCodeRun(
         await disposeClaudeCodeChild(query, child)
       } catch (disposeError: unknown) {
         throw new AggregateError(
-          [thrownError(error), thrownError(disposeError)],
+          [thrown(error), thrown(disposeError)],
           'subagent-claude-code: startup failed and CLI cleanup also failed',
         )
       }
@@ -243,7 +250,7 @@ export async function startClaudeCodeRun(
         query.close()
       } catch (disposeError: unknown) {
         throw new AggregateError(
-          [thrownError(error), thrownError(disposeError)],
+          [thrown(error), thrown(disposeError)],
           'subagent-claude-code: startup failed and query cleanup also failed',
         )
       }
@@ -252,7 +259,7 @@ export async function startClaudeCodeRun(
     if (cancelledBeforeCleanup || request.signal.aborted) {
       throw new Error('subagent-claude-code: request was aborted before SDK startup')
     }
-    throw thrownError(error)
+    throw thrown(error)
   }
 
   const publishedQuery = query

+ 1 - 10
packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts

@@ -445,7 +445,7 @@ describe('official spawn projection', () => {
     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 process = new ManagedClaudeCodeProcess(child.handle)
     const errorListener = vi.fn()
@@ -459,15 +459,6 @@ describe('official spawn projection', () => {
       message: 'spawn boom',
     }))
     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 () => {

+ 8 - 4
packages/subagent/subagent-codex/src/run.ts

@@ -13,7 +13,6 @@ import { SessionId } from '@deepseek-ai/dsh-session'
 import {
   settleRunResult,
   subprocessRunHandle,
-  thrownError,
   type SubagentResult,
   type SubagentRun,
   type SubagentStartRequest,
@@ -39,6 +38,11 @@ export interface CodexRunSpec {
   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.
  * @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 '
       + `(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
   // late rejection observed after the result race has already settled.
@@ -145,14 +149,14 @@ export async function startCodexRun(
       await disposeProcess()
     } catch (disposeError: unknown) {
       throw new AggregateError(
-        [thrownError(error), thrownError(disposeError)],
+        [thrown(error), thrown(disposeError)],
         'subagent-codex: startup failed and app-server cleanup also failed',
       )
     }
     if (runAbort.signal.aborted) {
       throw new Error('subagent-codex: request was aborted before run publication')
     }
-    throw thrownError(error)
+    throw thrown(error)
   }
 
   const collectOutput = (): ContentBlock[] => wire.collectOutput()

+ 3 - 7
packages/subagent/subagent/src/out-of-process.ts

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