Tasks: Native Claude Agent SDK Integration — Foundations (Phase 0–3)¶
Feature: 021-claude-agent-sdk
Input: Design documents from /docs/product-specs/021-claude-agent-sdk/
Scope: Phases 0–3 only (SDK installation → verification → model extensions → validation layer).
Phases 4–12 will be recorded in tasks.md during task breakdown.
Approach: TDD — test tasks are written and must fail before implementation tasks begin
References: Each task includes a document reference (doc:L<line>) pointing to the relevant design decision
Format: [ID] [P?] Description (doc:Lline)¶
- [P]: Task is parallelizable (different files, no unresolved dependencies)
- No
[US?]labels in Phases 0–3: all tasks are setup/foundational prerequisites - TDD rule: Test tasks MUST precede their implementation tasks in every phase
Phase 0: SDK Installation + Verification Smoke Test¶
Purpose: Mandatory gate — install the SDK, then confirm the actual Claude Agent SDK API against all [ASSUMED] names in research.md §2 before any production code is written. No implementation begins until this phase passes.
Gate criteria: All [ASSUMED] markers in research.md §2 are replaced with confirmed names. If any name differs from the spec, plan.md, data-model.md, and quickstart.md are updated before Phase 1 begins.
TDD note: Phase 0 is exploratory verification, not production code. There are no failing unit tests to write first — the smoke test script is the test.
- T001 Add
claude-agent-sdk>=0.1.39,<0.2.0topyproject.tomldependencies and runuv syncto verify resolution — must run before T002; the smoke test cannot import the SDK until this completes (plan.md:L180-183, research.md:L11-13) - T002 Create
scripts/smoke_test_sdk.pyimporting and exercisingClaudeAgentOptions,PermissionMode,@tool,create_sdk_mcp_server,ResultMessage,ClaudeSDKClient, andqueryto confirm actual class/function names (plan.md:L144-154, quickstart.md:L57-94, research.md:L48-55) - T003 Extend
scripts/smoke_test_sdk.pywith a 2-turn conversation usingClaudeSDKClientto confirm whether multi-turn state is automatic or requirescontinue_conversation=TrueinClaudeAgentOptions(plan.md:L155-168, research.md:L56-58) - T004 Update
research.md §2replacing every[ASSUMED]marker with the confirmed API name from T002–T003 (plan.md:L170-172, research.md:L27-58) - T005 Update
plan.md,data-model.md, andquickstart.mdwhere any assumed API name proved incorrect in T002–T003 (plan.md:L172-173)
Checkpoint: All [ASSUMED] markers in research.md §2 resolved. After T004/T005, scan all line number references in T006–T023b and update any that shifted due to doc edits before starting Phase 1.
Phase 1: Backends Package + Core Interfaces¶
Purpose: Establish the lib/backends/ package skeleton and implement the provider-agnostic ExecutionResult / AgentSession / AgentBackend interfaces that all subsequent phases depend on.
⚠️ CRITICAL: base.py (T008) must be complete before ANY user story phase can proceed. All downstream consumers — test runner, chat layer — depend on ExecutionResult.
Tests for Phase 1 (write these first — they MUST fail before T008)¶
- T006 [P] Write unit tests for
ExecutionResultconstruction, field defaults, and all error condition scenarios (max_turns, subprocess crash, tool failure, timeout) intests/unit/lib/backends/test_base.py. UseTokenUsage.zero()(notTokenUsage()) for the default token_usage field. ImportTokenUsagefromholodeck.models.token_usage. (plan.md:L193, contracts/execution-result.md:L17-72)
Implementation for Phase 1¶
- T007 [P] Create
src/holodeck/lib/backends/__init__.py(empty package marker) (plan.md:L185) - T007b Create
tests/unit/lib/backends/__init__.py(empty init required for pytest package discovery — all test files T006, T017–T022 live in this package and will not be discovered without it) - T008 Create
src/holodeck/lib/backends/base.pywith: ExecutionResultdataclass (all fields from contract;token_usage: TokenUsage = field(default_factory=TokenUsage.zero)— importTokenUsagefromholodeck.models.token_usage, NOT fromholodeck.models.test_resultwhich does not exist)AgentSessionProtocol withsend,send_streaming, andclose— typesend_streamingasAsyncGenerator[str, None](async generator withyieldinside), not-> AsyncIterator[str](which implies returning an external iterator object; these are not the same in Python's type system)AgentBackendProtocol withinitialize,invoke_once,create_session,teardownBackendError,BackendInitError,BackendSessionError,BackendTimeoutErrorexception hierarchy (plan.md:L187-193, data-model.md:L201-338, contracts/execution-result.md:L21-146)
Checkpoint: test_base.py tests now pass. ExecutionResult, AgentSession, and AgentBackend are importable from holodeck.lib.backends.base.
Phase 2: Model Extensions¶
Purpose: Extend HoloDeck's Pydantic models with Claude-native fields. Backward compatibility — existing YAML configurations must continue to parse without error.
⚠️ CRITICAL: These models are prerequisites for Phase 3 validators, Phase 4 SK backend refactor, and Phase 8 Claude backend. No backend work can begin until claude_config.py, llm.py, and agent.py are updated and tested.
Tests for Phase 2 (write these first — they MUST fail before T014–T016)¶
- T009 [P] Write unit tests for
AuthProviderenum values (api_key,oauth_token,bedrock,vertex,foundry) andPermissionModeenum values (manual,acceptEdits,acceptAll) intests/unit/models/test_claude_config.py. Note: T009 and T010 target the same file —[P]applies only when two developers own each task; a single developer should execute them sequentially. (plan.md:L219-223, data-model.md:L14-35) - T010 [P] Write unit tests for
ClaudeConfigvalid construction covering: minimal (all defaults),ExtendedThinkingConfigenabled,BashConfigwith excluded commands,FileSystemConfigall flags,SubagentConfigwithmax_parallel, andextra="forbid"rejects unknown fields intests/unit/models/test_claude_config.py. Note: Same file as T009 — parallelizable only with a second developer. (plan.md:L219-223, data-model.md:L74-121) - T011 [P] Write unit tests for
LLMProvider.auth_providerfield: valid values accepted,Nonedefault, andlogging.warningemitted (verified viacaplogfixture — notpytest.warns) whenauth_provideris set for a non-Anthropic provider intests/unit/models/test_llm_extensions.py(plan.md:L219-223, data-model.md:L125-142) - T012 [P] Write unit tests for
Agent.embedding_provider(acceptsLLMProvider | None) andAgent.claude(acceptsClaudeConfig | None) field acceptance intests/unit/models/test_agent_extensions.py(plan.md:L219-223, data-model.md:L145-175) - T013 [P] Write backward compatibility tests: load each YAML file in
tests/fixtures/agents/throughAgent(**config)and assert noValidationErroris raised. For each fixture also assertagent.claude is Noneandagent.embedding_provider is None, confirming new optional fields default correctly when absent intests/unit/models/test_agent_extensions.py(plan.md:L222-223, contracts/agent-yaml-schema.md:L200-205)
Implementation for Phase 2¶
- T014 Create
src/holodeck/models/claude_config.pywithAuthProvider(str, Enum),PermissionMode(str, Enum),ExtendedThinkingConfig(BaseModel),BashConfig(BaseModel),FileSystemConfig(BaseModel),SubagentConfig(BaseModel), andClaudeConfig(BaseModel)— all withextra="forbid"and all capabilities defaulting to disabled (plan.md:L201-208, data-model.md:L14-121, contracts/agent-yaml-schema.md:L28-56) - T015 Update
src/holodeck/models/llm.pyto addauth_provider: AuthProvider | None = Field(default=None, ...)and a newmodel_validator(mode="after")method (give it a unique name distinct from the existingcheck_endpoint_requiredvalidator) that emitslogging.warning(...)whenauth_provideris set for a non-Anthropic provider (plan.md:L210-212, data-model.md:L125-142, contracts/agent-yaml-schema.md:L66-84) - T016 Update
src/holodeck/models/agent.pyto addembedding_provider: LLMProvider | None = Field(default=None, ...)andclaude: ClaudeConfig | None = Field(default=None, ...), preservingmodel_config = ConfigDict(extra="forbid")(plan.md:L214-217, data-model.md:L145-175, contracts/agent-yaml-schema.md:L10-25)
Checkpoint: All Phase 2 tests pass. claude_config.py importable. Agent, LLMProvider accept new fields. All tests/fixtures/agents/ YAML files parse without error.
Phase 3: Validation Layer¶
Purpose: Implement all startup validators that gate Claude-native agent execution. These validators are called by ClaudeBackend.initialize() (Phase 8) before any subprocess is spawned — early, clear errors instead of cryptic runtime failures.
⚠️ CRITICAL: validators.py is a direct dependency of ClaudeBackend. Phase 8 cannot begin until all validators are complete and tested.
ConfigErrorconvention: All validators raiseConfigError(field, message)with two positional arguments:fieldis the configuration field name (e.g."nodejs","ANTHROPIC_API_KEY","embedding_provider");messageis the human-readable description. This matches the actual constructor signature atlib/errors.py:L26. Do not callConfigErrorwith a single string argument — that raisesTypeError.
Tests for Phase 3 (write these first — they MUST fail before T023)¶
NOTE: Write ALL validator tests before implementing
validators.py. Runpytest tests/unit/lib/backends/test_validators.py -n autoto confirm all fail withImportErrororModuleNotFoundErrorbefore T023.
- T017 [P] Write unit tests for
validate_nodejs(): passes whenshutil.which("node")returns a path; raisesConfigError("nodejs", "Node.js is required…")(2-arg form) with install instructions when it returnsNoneintests/unit/lib/backends/test_validators.py(plan.md:L231-233, research.md:L204-219) - T018 [P] Write unit tests for
validate_credentials()covering all 5auth_providervalues:api_keyraisesConfigError("ANTHROPIC_API_KEY", "…")when absent;oauth_tokenraisesConfigError("CLAUDE_CODE_OAUTH_TOKEN", "… run 'claude setup-token'")when absent;bedrockreturns env dict with{"CLAUDE_CODE_USE_BEDROCK": "1"};vertexreturns{"CLAUDE_CODE_USE_VERTEX": "1"};foundryreturns{"CLAUDE_CODE_USE_FOUNDRY": "1"}intests/unit/lib/backends/test_validators.py(plan.md:L231-234, research.md:L83-94, quickstart.md:L279-317) - T019 [P] Write unit tests for
validate_embedding_provider()covering 4 cases: (1) passes when no vectorstore tools present; (2) passes when vectorstore tool present andembedding_providerset to a valid provider (openai); (3) raisesConfigError("embedding_provider", "…")whenprovider=anthropic+ vectorstore tool + noembedding_providerconfigured; (4) raisesConfigError("embedding_provider", "… anthropic cannot generate embeddings")whenembedding_provider.provider == anthropic— onlyopenaiandazure_openaiare valid embedding providers intests/unit/lib/backends/test_validators.py(plan.md:L231-235, research.md:L244-250, data-model.md:L170-175) - T020 [P] Write unit tests for
validate_tool_filtering(): emitslogging.warning(...)(verified viacaplogfixture) whenprovider=anthropic+tool_filteringis notNone— validator warns only and does NOT mutateagent.tool_filtering;ClaudeBackendignores the field internally; passes silently whentool_filteringisNoneintests/unit/lib/backends/test_validators.py(plan.md:L231-236, research.md:L279-284, data-model.md:L172-174) - T021 [P] Write unit tests for
validate_working_directory(): passes whenworking_directory=None; passes when path has noCLAUDE.md; emitslogging.warning(...)(verified viacaplogfixture — this is a warning, not aConfigError) when<working_directory>/CLAUDE.mdcontains# CLAUDE.mdheader intests/unit/lib/backends/test_validators.py(plan.md:L231-237, research.md:L270-274, data-model.md:L175) - T022 [P] Write unit tests for
validate_response_format()covering 4 cases: (1) passes whenNone; (2) passes when a valid serialisable JSON Schemadict; (3) raisesConfigError("response_format", "…")when dict contains non-serialisable types; (4) whenresponse_formatis astr(file path), loads and parses the file as JSON Schema — raisesConfigError("response_format", "…")if the file is missing or its parsed content is non-serialisable, passes if the file contains a valid JSON Schema dict intests/unit/lib/backends/test_validators.py(plan.md:L231-238, research.md:L226-239)
Implementation for Phase 3¶
- T023 Create
src/holodeck/lib/backends/validators.pyimplementing: validate_nodejs() -> None— usesshutil.which("node"); raisesConfigError("nodejs", "…")if absentvalidate_credentials(model: LLMProvider) -> dict[str, str]— maps eachauth_providervalue to its required env vars; raisesConfigError(var_name, "…")if a required var is missing; usesholodeck.config.env_loader.get_env_varfor resolutionvalidate_embedding_provider(agent: Agent) -> None— raisesConfigError("embedding_provider", "…")when vectorstore/hierarchical-doc tools are present andembedding_provideris absent, or whenembedding_provider.provider == anthropicvalidate_tool_filtering(agent: Agent) -> None— emitslogging.warning(...)whentool_filteringis set for Anthropic provider; does not mutateagent.tool_filteringvalidate_working_directory(path: str | None) -> None— emitslogging.warning(...)on CLAUDE.md collision; does not raisevalidate_response_format(response_format: dict | str | None) -> None— ifstr, load file and parse as JSON Schema dict; validate JSON serializability for dict cases; raisesConfigError("response_format", "…")on failure- All
ConfigErrorcalls use the 2-arg formConfigError(field_name, message)fromholodeck.lib.errors(plan.md:L231-253, research.md:L204-219, quickstart.md:L279-317) - T023b Remove the local
class AgentFactoryError(Exception)redefinition fromsrc/holodeck/lib/test_runner/agent_factory.py(approximately line 100) and replace all references within that file withfrom holodeck.lib.errors import AgentFactoryError. Runmake test-unitto confirm zero regressions. This resolves the duplicate-class conflict before Phase 4 begins its SK backend refactor. (lib/errors.py:L153)
Checkpoint: All Phase 3 tests pass. validators.py importable from holodeck.lib.backends.validators. Gate confirmed: startup errors surface before any subprocess spawns. AgentFactoryError consolidated to lib/errors.py only — Phase 4 can now safely from holodeck.lib.errors import AgentFactoryError without ambiguity.
Dependencies & Execution Order¶
Phase Dependencies¶
Phase 0 (SDK install + smoke test — mandatory gate)
│
▼
Phase 1 (backends/ package + core interfaces)
│
▼
Phase 2 (Model Extensions)
│
▼
Phase 3 (Validation Layer)
│
▼ (unblocks Phase 4–12 in main tasks.md)
Within Each Phase: TDD Order¶
1. Write test tasks (marked with "MUST fail before")
2. Verify tests fail: pytest <test_file> -n auto
3. Implement: make test tasks pass
4. Run: make format && make lint-fix && make type-check
5. Checkpoint: confirm tests pass, proceed to next phase
Parallel Opportunities Per Phase¶
Phase 0: Sequential — T001 must complete before T002; T002 before T003; T004/T005 can overlap.
Phase 1: - T006 (tests), T007, and T007b (init files) can all run in parallel - T008 (implementation) requires T006 + T007 + T007b complete
Phase 2:
- T009–T013 (all five test tasks) can run in parallel — different test functions (note: T009+T010 share a file; parallel only with two developers)
- T014 (claude_config.py) requires T009, T010
- T015 (llm.py) requires T011, T014 (needs AuthProvider from T014)
- T016 (agent.py) requires T012, T013, T014 (needs ClaudeConfig from T014)
Phase 3: - T017–T022 (all six test tasks) can run in parallel — different test functions in same file - T023 (validators.py) requires T017–T022 all written and confirmed failing - T023b (AgentFactoryError consolidation) can run in parallel with T023 — different file, no shared dependency
Parallel Example: Phase 2¶
# All Phase 2 test tasks can start together (different files):
# Developer A: tests/unit/models/test_claude_config.py (T009, T010 — same file, do sequentially)
# Developer B: tests/unit/models/test_llm_extensions.py (T011)
# Developer C: tests/unit/models/test_agent_extensions.py (T012, T013)
# After all test tasks confirmed failing:
# T014 → T015 → T016 (sequential; each depends on the prior model)
Parallel Example: Phase 3¶
# All six validator tests can be written simultaneously (same file, different functions):
# T017: test_validate_nodejs
# T018: test_validate_credentials (5 parametrized cases)
# T019: test_validate_embedding_provider (4 cases including anthropic-as-embedding rejection)
# T020: test_validate_tool_filtering (caplog-based, warn-only)
# T021: test_validate_working_directory (caplog-based)
# T022: test_validate_response_format (4 cases including str file-path resolution)
# After all confirmed failing → T023 and T023b can run in parallel (different files)
Task Summary¶
| Phase | Tasks | Tests | Implementations | Parallel |
|---|---|---|---|---|
| Phase 0 (SDK install + Smoke Test) | T001–T005 | 0 | 0 | T004∥T005 |
| Phase 1 (Backends Package) | T006–T008 + T007b | 1 | 3 | T006, T007, T007b |
| Phase 2 (Models) | T009–T016 | 5 | 3 | T009–T013 |
| Phase 3 (Validators) | T017–T023b | 6 | 2 | T017–T022, T023b∥T023 |
| Total | 25 tasks | 12 | 8 |
Code Quality Gates¶
Run after each phase:
make format # Black + Ruff formatting
make lint-fix # Auto-fix linting issues
make type-check # MyPy strict mode
make test-unit # pytest tests/unit/ -n auto
make security # Bandit + Safety + detect-secrets
Notes¶
- Phase 0 is a hard gate — do not modify any production source file until T004 is complete
- T001 (SDK install) is the first task in Phase 0; the smoke test (T002) cannot run without it
- All
[ASSUMED]names inresearch.md §2carry risk; Phase 0 eliminates that risk before code is written ExecutionResult(T008) is the most critical deliverable in Phases 0–3; every downstream phase depends on itTokenUsagedefault: useTokenUsage.zero()—TokenUsage()with no args raisesValidationError(all three fields are required with sum constraint enforcement)TokenUsageimport path:from holodeck.models.token_usage import TokenUsage— there is noholodeck.models.test_resultmoduleConfigErrorsignature: always 2-argConfigError(field, message)— single-arg form raisesTypeErrorsend_streamingtyping: useAsyncGenerator[str, None](async generator withyield) — not-> AsyncIterator[str]validate_tool_filteringwarns only — it does not mutateAgent.tool_filtering;ClaudeBackendignores the field internally- Backward compatibility (T013) scope:
tests/fixtures/agents/*.yamlonly — not deploy, ollama, or wizard fixtures - T023b resolves the
AgentFactoryErrorduplication before Phase 4's SK refactor needs a clean import