Contract: Provider-Agnostic Execution Interface¶
Module: src/holodeck/lib/backends/base.py
Feature: 021-claude-agent-sdk (FR-012b)
Overview¶
This contract defines the provider-agnostic interface that all agent execution backends must implement. It replaces the Semantic Kernel type dependencies (ChatHistory, ChatMessageContent, FunctionCallContent) throughout HoloDeck's test runner and chat layer.
Consumers: TestExecutor, ChatSessionManager, AgentExecutor (chat)
Producers: SKBackend, ClaudeBackend
ExecutionResult¶
Single-turn invocation result. Produced by both SK and Claude-native backends.
@dataclass
class ExecutionResult:
response: str
tool_calls: list[dict[str, Any]] = field(default_factory=list)
tool_results: list[dict[str, Any]] = field(default_factory=list)
token_usage: dict[str, int] = field(default_factory=dict)
structured_output: Any | None = None
num_turns: int = 1
is_error: bool = False
error_reason: str | None = None
Field Schemas¶
tool_calls — each element:
{
"name": "vectorstore_search",
"arguments": {"query": "refund policy"},
"call_id": "toolu_01Abc..."
}
tool_results — each element:
{
"call_id": "toolu_01Abc...",
"result": "Our refund policy is 30 days...",
"is_error": false
}
token_usage — keys:
{
"input_tokens": 450,
"output_tokens": 180,
"total_tokens": 630
}
Error Conditions¶
is_error |
error_reason |
Meaning |
|---|---|---|
False |
None |
Successful turn |
True |
"max_turns limit reached" |
Agent hit configured max_turns |
True |
"subprocess terminated unexpectedly" |
Claude subprocess crashed |
True |
"tool execution failed: <name>" |
Critical tool failure |
True |
"timeout exceeded" |
Agent exceeded timeout |
AgentSession Protocol¶
Stateful multi-turn session. Used by ChatSessionManager.
class AgentSession(Protocol):
async def send(self, message: str) -> ExecutionResult: ...
async def send_streaming(self, message: str) -> AsyncIterator[str]: ...
async def close(self) -> None: ...
Implementation Notes¶
SK implementation (SKSession):
- send(): invokes AgentThreadRun.execute_turn(), wraps result in ExecutionResult
- send_streaming(): currently non-streaming; yields full response as single chunk (no-op streaming)
- close(): releases SK kernel resources
Claude implementation (ClaudeSession):
- send(): calls client.query(), collects full response from receive_response() iterator
- send_streaming(): calls client.query(), yields text chunks from StreamEvent partial messages
- close(): calls client.disconnect()
AgentBackend Protocol¶
Backend factory. BackendSelector creates the appropriate implementation.
class AgentBackend(Protocol):
async def initialize(self) -> None: ...
async def invoke_once(self, message: str, context: list[dict] | None = None) -> ExecutionResult: ...
async def create_session(self) -> AgentSession: ...
async def teardown(self) -> None: ...
Lifecycle¶
BackendSelector.create(agent_config, tools)
│
▼
backend.initialize() # Tool index setup, credential validation
│
├─► backend.invoke_once() × N # For test cases (TestExecutor)
│
└─► session = backend.create_session()
session.send() × N # For chat turns (ChatSessionManager)
session.close()
│
▼
backend.teardown() # Resource cleanup
Error Types¶
class BackendError(HoloDeckError):
"""Base for all backend errors."""
class BackendInitError(BackendError):
"""Raised during initialize() — startup validation failures."""
class BackendSessionError(BackendError):
"""Raised during send() — session-level failures (subprocess crash)."""
class BackendTimeoutError(BackendError):
"""Raised when a single invocation exceeds configured timeout."""
Compatibility Guarantee¶
- SK backend
ExecutionResult.responseis identical to the currentAgentThreadRunresponse string. - SK backend
ExecutionResult.tool_callsformat is identical to the currentAgentExecutionResult.tool_callsdict structure. TestExecutorandChatSessionManagerare modified to useExecutionResultandAgentSessiononly. NoChatHistory,FunctionCallContent, or any other SK type is referenced in these consumers after this feature is implemented.- Non-Anthropic agents continue to work without modification — they use the SK backend which implements the same interface.