AI Agents SDE Task Viewer
      • Context
      • Plan
      • Prd
  1. Home
  2. AgentSDE
  3. agent-core
  4. gh-346
  5. plan
  6. plan.md
plan.md(3.6 KB)· Apr 8, 2026· 2 min read
  • Summary
  • Files
  • Steps
  • Verification
  • Risks

Plan: Create LLMProvider Interface + ClaudeCLIProvider#

Summary#

Define the LLMProvider abstraction interface with a normalized PhaseResult type and wrap the existing ClaudeInvocationService + SignalParser as ClaudeCLIProvider. This decouples the <promise> signal protocol from the core pipeline, making it a Claude-specific detail — Phase 1 foundation for multi-tenant.

Files#

FileActionDescription
src/llm/llm-provider.interface.tscreateLLMProvider interface, PhaseResult type, LLM_PROVIDER injection token
src/llm/claude/claude-cli.provider.tscreateClaudeCLIProvider implementing LLMProvider — delegates to ClaudeInvocationService, maps SignalResult → PhaseResult
src/llm/llm.module.tscreateLLMModule — provides LLM_PROVIDER token bound to ClaudeCLIProvider, exports it
src/llm/claude/claude-cli.provider.spec.tscreateUnit tests for all 5 signal mapping scenarios + error handling + no-signal edge case

Steps#

  1. Define PhaseResult type and LLMProvider interface in src/llm/llm-provider.interface.ts. PhaseResult must capture: status ('complete' | 'blocked' | 'skip'), blockedType ('transient' | 'persistent' | 'conflict' | null), reason, completeType, retryAfter, prNumber, prBranch, conflictMetadata (reuse ConflictMetadata from src/conflict/conflict.types.ts), and metadata. Export LLM_PROVIDER as a Symbol('LLM_PROVIDER') injection token. LLMProvider.invoke() signature: invoke(skill: string, env: Record<string, string>, taskId: number): Promise<PhaseResult>.
  2. Implement ClaudeCLIProvider in src/llm/claude/claude-cli.provider.ts. Inject ClaudeInvocationService via constructor. invoke() calls this.claude.invoke(), then maps SignalResult → PhaseResult: map type ('complete'→'complete', 'skip'→'skip', 'blocked'/'partial'/'none'→'blocked'), map subtype to blockedType ('' defaults to 'transient' for blocked/partial/none), extract prNumber/prBranch from result.metadata, pass through reason, completeType, retryAfter, conflictMetadata. Wrap in try/catch — on error return PhaseResult { status: 'blocked', blockedType: 'transient', reason: error.message }.
  3. Create LLMModule in src/llm/llm.module.ts. Import InvokeModule. Provide LLM_PROVIDER token using useClass: ClaudeCLIProvider. Export LLM_PROVIDER.
  4. Write unit tests in src/llm/claude/claude-cli.provider.spec.ts. Mock ClaudeInvocationService. Cover: (a) complete signal → PhaseResult { status: 'complete' }, (b) blocked:transient → { status: 'blocked', blockedType: 'transient' }, (c) blocked:persistent → { status: 'blocked', blockedType: 'persistent' }, (d) blocked:conflict with conflictMetadata → { status: 'blocked', blockedType: 'conflict' }, (e) skip → { status: 'skip' }, (f) no signal (type: 'none') → { status: 'blocked', blockedType: 'transient' }, (g) invoke throws → { status: 'blocked', blockedType: 'transient', reason: error.message }.

Verification#

  • npx tsc --noEmit passes with zero errors
  • npm run lint passes with zero warnings
  • npm run test -- --testPathPattern llm passes all 7 test cases

Risks#

  • SignalResult.metadata key names (prNumber, prBranch) are string-keyed — if upstream changes these keys, the mapping breaks silently. Mitigation: test cases assert metadata extraction explicitly.
  • partial signal type has no direct PhaseResult equivalent — mapped to blocked:transient (matches current toSignalKind() behavior in PhaseRouterService line 743).
ContextPrd