From ff3022ea4ec540d231d994b4ade3bfdd70e4a77f Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Wed, 11 Mar 2026 13:12:36 -0500 Subject: [PATCH 1/2] feat: add AgentRunner, AgentGraphRunner ABCs and result types Adds the runner ABCs and result types needed to support agent and agent graph execution in later PRs: - ldai/runners/agent_runner.py: AgentRunner ABC with run(input) -> AgentResult - ldai/runners/agent_graph_runner.py: AgentGraphRunner ABC with run(input) -> AgentGraphResult - ldai/runners/types.py: AgentResult, AgentGraphResult (output, raw, metrics), ToolRegistry alias - Exports all new types from ldai top-level __init__.py Co-Authored-By: Claude Sonnet 4.6 --- packages/sdk/server-ai/src/ldai/__init__.py | 7 ++ .../server-ai/src/ldai/runners/__init__.py | 13 +++ .../src/ldai/runners/agent_graph_runner.py | 26 +++++ .../src/ldai/runners/agent_runner.py | 25 +++++ .../sdk/server-ai/src/ldai/runners/types.py | 32 ++++++ .../sdk/server-ai/tests/test_runner_abcs.py | 100 ++++++++++++++++++ 6 files changed, 203 insertions(+) create mode 100644 packages/sdk/server-ai/src/ldai/runners/__init__.py create mode 100644 packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py create mode 100644 packages/sdk/server-ai/src/ldai/runners/agent_runner.py create mode 100644 packages/sdk/server-ai/src/ldai/runners/types.py create mode 100644 packages/sdk/server-ai/tests/test_runner_abcs.py diff --git a/packages/sdk/server-ai/src/ldai/__init__.py b/packages/sdk/server-ai/src/ldai/__init__.py index e8ef83b4..7810db63 100644 --- a/packages/sdk/server-ai/src/ldai/__init__.py +++ b/packages/sdk/server-ai/src/ldai/__init__.py @@ -28,10 +28,17 @@ ProviderConfig, ) from ldai.providers.types import EvalScore, JudgeResponse +from ldai.runners import AgentGraphRunner, AgentRunner +from ldai.runners.types import AgentGraphResult, AgentResult, ToolRegistry from ldai.tracker import AIGraphTracker __all__ = [ 'LDAIClient', + 'AgentRunner', + 'AgentGraphRunner', + 'AgentResult', + 'AgentGraphResult', + 'ToolRegistry', 'AIAgentConfig', 'AIAgentConfigDefault', 'AIAgentConfigRequest', diff --git a/packages/sdk/server-ai/src/ldai/runners/__init__.py b/packages/sdk/server-ai/src/ldai/runners/__init__.py new file mode 100644 index 00000000..a9a834cd --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/runners/__init__.py @@ -0,0 +1,13 @@ +"""Runner ABCs and result types for LaunchDarkly AI SDK.""" + +from ldai.runners.agent_graph_runner import AgentGraphRunner +from ldai.runners.agent_runner import AgentRunner +from ldai.runners.types import AgentGraphResult, AgentResult, ToolRegistry + +__all__ = [ + 'AgentRunner', + 'AgentGraphRunner', + 'AgentResult', + 'AgentGraphResult', + 'ToolRegistry', +] diff --git a/packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py b/packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py new file mode 100644 index 00000000..3cfdd809 --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py @@ -0,0 +1,26 @@ +"""Abstract base class for agent graph runners.""" + +from abc import ABC, abstractmethod +from typing import Any + +from ldai.runners.types import AgentGraphResult + + +class AgentGraphRunner(ABC): + """ + Abstract base class for agent graph runners. + + An AgentGraphRunner encapsulates multi-agent graph execution. + Provider-specific implementations (e.g. OpenAIAgentGraphRunner) are + returned by RunnerFactory.create_agent_graph() and hold all provider + wiring internally. + """ + + @abstractmethod + async def run(self, input: Any) -> AgentGraphResult: + """ + Run the agent graph with the given input. + + :param input: The input to the agent graph (string prompt or structured input) + :return: AgentGraphResult containing the output, raw response, and metrics + """ diff --git a/packages/sdk/server-ai/src/ldai/runners/agent_runner.py b/packages/sdk/server-ai/src/ldai/runners/agent_runner.py new file mode 100644 index 00000000..063198de --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/runners/agent_runner.py @@ -0,0 +1,25 @@ +"""Abstract base class for agent runners.""" + +from abc import ABC, abstractmethod +from typing import Any + +from ldai.runners.types import AgentResult + + +class AgentRunner(ABC): + """ + Abstract base class for single-agent runners. + + An AgentRunner encapsulates the execution of a single AI agent. + Provider-specific implementations (e.g. OpenAIAgentRunner) are returned + by RunnerFactory.create_agent() and hold all provider wiring internally. + """ + + @abstractmethod + async def run(self, input: Any) -> AgentResult: + """ + Run the agent with the given input. + + :param input: The input to the agent (string prompt or structured input) + :return: AgentResult containing the output, raw response, and metrics + """ diff --git a/packages/sdk/server-ai/src/ldai/runners/types.py b/packages/sdk/server-ai/src/ldai/runners/types.py new file mode 100644 index 00000000..f4db9a6a --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/runners/types.py @@ -0,0 +1,32 @@ +"""Result types and type aliases for agent and agent graph runners.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable, Dict + +from ldai.providers.types import LDAIMetrics + +# Type alias for a registry of tools available to an agent. +# Keys are tool names; values are the callable implementations. +ToolRegistry = Dict[str, Callable] + + +@dataclass +class AgentResult: + """ + Result from a single-agent run. + """ + output: str + raw: Any + metrics: LDAIMetrics + + +@dataclass +class AgentGraphResult: + """ + Result from an agent graph run. + """ + output: str + raw: Any + metrics: LDAIMetrics diff --git a/packages/sdk/server-ai/tests/test_runner_abcs.py b/packages/sdk/server-ai/tests/test_runner_abcs.py new file mode 100644 index 00000000..1fc03aef --- /dev/null +++ b/packages/sdk/server-ai/tests/test_runner_abcs.py @@ -0,0 +1,100 @@ +import pytest + +from ldai.providers.types import LDAIMetrics +from ldai.runners.agent_graph_runner import AgentGraphRunner +from ldai.runners.agent_runner import AgentRunner +from ldai.runners.types import AgentGraphResult, AgentResult, ToolRegistry + + +# --- Concrete test doubles --- + +class ConcreteAgentRunner(AgentRunner): + async def run(self, input): + return AgentResult( + output=f"agent response to: {input}", + raw={"raw": input}, + metrics=LDAIMetrics(success=True), + ) + + +class ConcreteAgentGraphRunner(AgentGraphRunner): + async def run(self, input): + return AgentGraphResult( + output=f"graph response to: {input}", + raw={"raw": input}, + metrics=LDAIMetrics(success=True), + ) + + +# --- AgentRunner --- + +def test_agent_runner_is_abstract(): + with pytest.raises(TypeError): + AgentRunner() # type: ignore[abstract] + + +@pytest.mark.asyncio +async def test_agent_runner_run_returns_agent_result(): + runner = ConcreteAgentRunner() + result = await runner.run("hello") + assert isinstance(result, AgentResult) + assert result.output == "agent response to: hello" + assert result.raw == {"raw": "hello"} + assert result.metrics.success is True + + +@pytest.mark.asyncio +async def test_agent_result_fields(): + metrics = LDAIMetrics(success=True) + result = AgentResult(output="done", raw={"key": "val"}, metrics=metrics) + assert result.output == "done" + assert result.raw == {"key": "val"} + assert result.metrics is metrics + + +# --- AgentGraphRunner --- + +def test_agent_graph_runner_is_abstract(): + with pytest.raises(TypeError): + AgentGraphRunner() # type: ignore[abstract] + + +@pytest.mark.asyncio +async def test_agent_graph_runner_run_returns_agent_graph_result(): + runner = ConcreteAgentGraphRunner() + result = await runner.run("hello graph") + assert isinstance(result, AgentGraphResult) + assert result.output == "graph response to: hello graph" + assert result.raw == {"raw": "hello graph"} + assert result.metrics.success is True + + +@pytest.mark.asyncio +async def test_agent_graph_result_fields(): + metrics = LDAIMetrics(success=False) + result = AgentGraphResult(output="", raw=None, metrics=metrics) + assert result.output == "" + assert result.raw is None + assert result.metrics.success is False + + +# --- ToolRegistry --- + +def test_tool_registry_is_dict_of_callables(): + tools: ToolRegistry = { + "search": lambda q: f"results for {q}", + "calculator": lambda x: x * 2, + } + assert tools["search"]("python") == "results for python" + assert tools["calculator"](21) == 42 + + +# --- Top-level exports --- + +def test_top_level_exports(): + import ldai + assert hasattr(ldai, 'AgentRunner') + assert hasattr(ldai, 'AgentGraphRunner') + assert hasattr(ldai, 'AgentResult') + assert hasattr(ldai, 'AgentGraphResult') + assert hasattr(ldai, 'ToolRegistry') From af505900fde08609b0575d13246cd37b2ff8468d Mon Sep 17 00:00:00 2001 From: jsonbailey Date: Thu, 26 Mar 2026 11:04:07 -0500 Subject: [PATCH 2/2] refactor!: convert runner ABCs to Protocols and consolidate into providers namespace Co-Authored-By: Claude Sonnet 4.6 --- packages/sdk/server-ai/src/ldai/__init__.py | 9 ++++-- .../server-ai/src/ldai/providers/__init__.py | 22 +++++++++++++ .../src/ldai/providers/agent_graph_runner.py | 23 +++++++++++++ .../src/ldai/providers/agent_runner.py | 23 +++++++++++++ .../src/ldai/providers/model_runner.py | 12 +++---- .../sdk/server-ai/src/ldai/providers/types.py | 26 ++++++++++++++- .../server-ai/src/ldai/runners/__init__.py | 13 -------- .../src/ldai/runners/agent_graph_runner.py | 26 --------------- .../src/ldai/runners/agent_runner.py | 25 --------------- .../sdk/server-ai/src/ldai/runners/types.py | 32 ------------------- .../sdk/server-ai/tests/test_runner_abcs.py | 30 ++++++++++------- 11 files changed, 125 insertions(+), 116 deletions(-) create mode 100644 packages/sdk/server-ai/src/ldai/providers/agent_graph_runner.py create mode 100644 packages/sdk/server-ai/src/ldai/providers/agent_runner.py delete mode 100644 packages/sdk/server-ai/src/ldai/runners/__init__.py delete mode 100644 packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py delete mode 100644 packages/sdk/server-ai/src/ldai/runners/agent_runner.py delete mode 100644 packages/sdk/server-ai/src/ldai/runners/types.py diff --git a/packages/sdk/server-ai/src/ldai/__init__.py b/packages/sdk/server-ai/src/ldai/__init__.py index 7810db63..944a0cb8 100644 --- a/packages/sdk/server-ai/src/ldai/__init__.py +++ b/packages/sdk/server-ai/src/ldai/__init__.py @@ -27,9 +27,14 @@ ModelConfig, ProviderConfig, ) +from ldai.providers import ( + AgentGraphResult, + AgentGraphRunner, + AgentResult, + AgentRunner, + ToolRegistry, +) from ldai.providers.types import EvalScore, JudgeResponse -from ldai.runners import AgentGraphRunner, AgentRunner -from ldai.runners.types import AgentGraphResult, AgentResult, ToolRegistry from ldai.tracker import AIGraphTracker __all__ = [ diff --git a/packages/sdk/server-ai/src/ldai/providers/__init__.py b/packages/sdk/server-ai/src/ldai/providers/__init__.py index db1c8836..0148698b 100644 --- a/packages/sdk/server-ai/src/ldai/providers/__init__.py +++ b/packages/sdk/server-ai/src/ldai/providers/__init__.py @@ -1,9 +1,31 @@ +from ldai.providers.agent_graph_runner import AgentGraphRunner +from ldai.providers.agent_runner import AgentRunner from ldai.providers.ai_provider import AIProvider from ldai.providers.model_runner import ModelRunner from ldai.providers.runner_factory import RunnerFactory +from ldai.providers.types import ( + AgentGraphResult, + AgentResult, + EvalScore, + JudgeResponse, + LDAIMetrics, + ModelResponse, + StructuredResponse, + ToolRegistry, +) __all__ = [ 'AIProvider', + 'AgentGraphResult', + 'AgentGraphRunner', + 'AgentResult', + 'AgentRunner', + 'EvalScore', + 'JudgeResponse', + 'LDAIMetrics', + 'ModelResponse', 'ModelRunner', 'RunnerFactory', + 'StructuredResponse', + 'ToolRegistry', ] diff --git a/packages/sdk/server-ai/src/ldai/providers/agent_graph_runner.py b/packages/sdk/server-ai/src/ldai/providers/agent_graph_runner.py new file mode 100644 index 00000000..a7bdefee --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/providers/agent_graph_runner.py @@ -0,0 +1,23 @@ +from typing import Any, Protocol, runtime_checkable + +from ldai.providers.types import AgentGraphResult + + +@runtime_checkable +class AgentGraphRunner(Protocol): + """ + Runtime capability interface for multi-agent graph execution. + + An AgentGraphRunner is a focused, configured object returned by + AIProvider.create_agent_graph(). It holds all provider wiring internally — + the caller just passes input. + """ + + async def run(self, input: Any) -> AgentGraphResult: + """ + Run the agent graph with the given input. + + :param input: The input to the agent graph (string prompt or structured input) + :return: AgentGraphResult containing the output, raw response, and metrics + """ + ... diff --git a/packages/sdk/server-ai/src/ldai/providers/agent_runner.py b/packages/sdk/server-ai/src/ldai/providers/agent_runner.py new file mode 100644 index 00000000..d0bcc883 --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/providers/agent_runner.py @@ -0,0 +1,23 @@ +from typing import Any, Protocol, runtime_checkable + +from ldai.providers.types import AgentResult + + +@runtime_checkable +class AgentRunner(Protocol): + """ + Runtime capability interface for single-agent execution. + + An AgentRunner is a focused, configured object returned by + AIProvider.create_agent(). It holds all provider wiring internally — + the caller just passes input. + """ + + async def run(self, input: Any) -> AgentResult: + """ + Run the agent with the given input. + + :param input: The input to the agent (string prompt or structured input) + :return: AgentResult containing the output, raw response, and metrics + """ + ... diff --git a/packages/sdk/server-ai/src/ldai/providers/model_runner.py b/packages/sdk/server-ai/src/ldai/providers/model_runner.py index 79309f33..5f00887c 100644 --- a/packages/sdk/server-ai/src/ldai/providers/model_runner.py +++ b/packages/sdk/server-ai/src/ldai/providers/model_runner.py @@ -1,20 +1,19 @@ -from abc import ABC, abstractmethod -from typing import Any, Dict, List +from typing import Any, Dict, List, Protocol, runtime_checkable from ldai.models import LDMessage from ldai.providers.types import ModelResponse, StructuredResponse -class ModelRunner(ABC): +@runtime_checkable +class ModelRunner(Protocol): """ Runtime capability interface for model invocation. A ModelRunner is a focused, configured object returned by - AIConnector.create_model(). It knows exactly which model to call + AIProvider.create_model(). It knows exactly which model to call and with what parameters — the caller just passes messages. """ - @abstractmethod async def invoke_model(self, messages: List[LDMessage]) -> ModelResponse: """ Invoke the model with an array of messages. @@ -22,8 +21,8 @@ async def invoke_model(self, messages: List[LDMessage]) -> ModelResponse: :param messages: Array of LDMessage objects representing the conversation :return: ModelResponse containing the model's response and metrics """ + ... - @abstractmethod async def invoke_structured_model( self, messages: List[LDMessage], @@ -36,3 +35,4 @@ async def invoke_structured_model( :param response_structure: Dictionary defining the JSON schema for output structure :return: StructuredResponse containing the structured data """ + ... diff --git a/packages/sdk/server-ai/src/ldai/providers/types.py b/packages/sdk/server-ai/src/ldai/providers/types.py index 0a07151f..bb87350e 100644 --- a/packages/sdk/server-ai/src/ldai/providers/types.py +++ b/packages/sdk/server-ai/src/ldai/providers/types.py @@ -3,11 +3,15 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional from ldai.models import LDMessage from ldai.tracker import TokenUsage +# Type alias for a registry of tools available to an agent. +# Keys are tool names; values are the callable implementations. +ToolRegistry = Dict[str, Callable] + @dataclass class LDAIMetrics: @@ -94,3 +98,23 @@ def to_dict(self) -> Dict[str, Any]: if self.error is not None: result['error'] = self.error return result + + +@dataclass +class AgentResult: + """ + Result from a single-agent run. + """ + output: str + raw: Any + metrics: LDAIMetrics + + +@dataclass +class AgentGraphResult: + """ + Result from an agent graph run. + """ + output: str + raw: Any + metrics: LDAIMetrics diff --git a/packages/sdk/server-ai/src/ldai/runners/__init__.py b/packages/sdk/server-ai/src/ldai/runners/__init__.py deleted file mode 100644 index a9a834cd..00000000 --- a/packages/sdk/server-ai/src/ldai/runners/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Runner ABCs and result types for LaunchDarkly AI SDK.""" - -from ldai.runners.agent_graph_runner import AgentGraphRunner -from ldai.runners.agent_runner import AgentRunner -from ldai.runners.types import AgentGraphResult, AgentResult, ToolRegistry - -__all__ = [ - 'AgentRunner', - 'AgentGraphRunner', - 'AgentResult', - 'AgentGraphResult', - 'ToolRegistry', -] diff --git a/packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py b/packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py deleted file mode 100644 index 3cfdd809..00000000 --- a/packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Abstract base class for agent graph runners.""" - -from abc import ABC, abstractmethod -from typing import Any - -from ldai.runners.types import AgentGraphResult - - -class AgentGraphRunner(ABC): - """ - Abstract base class for agent graph runners. - - An AgentGraphRunner encapsulates multi-agent graph execution. - Provider-specific implementations (e.g. OpenAIAgentGraphRunner) are - returned by RunnerFactory.create_agent_graph() and hold all provider - wiring internally. - """ - - @abstractmethod - async def run(self, input: Any) -> AgentGraphResult: - """ - Run the agent graph with the given input. - - :param input: The input to the agent graph (string prompt or structured input) - :return: AgentGraphResult containing the output, raw response, and metrics - """ diff --git a/packages/sdk/server-ai/src/ldai/runners/agent_runner.py b/packages/sdk/server-ai/src/ldai/runners/agent_runner.py deleted file mode 100644 index 063198de..00000000 --- a/packages/sdk/server-ai/src/ldai/runners/agent_runner.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Abstract base class for agent runners.""" - -from abc import ABC, abstractmethod -from typing import Any - -from ldai.runners.types import AgentResult - - -class AgentRunner(ABC): - """ - Abstract base class for single-agent runners. - - An AgentRunner encapsulates the execution of a single AI agent. - Provider-specific implementations (e.g. OpenAIAgentRunner) are returned - by RunnerFactory.create_agent() and hold all provider wiring internally. - """ - - @abstractmethod - async def run(self, input: Any) -> AgentResult: - """ - Run the agent with the given input. - - :param input: The input to the agent (string prompt or structured input) - :return: AgentResult containing the output, raw response, and metrics - """ diff --git a/packages/sdk/server-ai/src/ldai/runners/types.py b/packages/sdk/server-ai/src/ldai/runners/types.py deleted file mode 100644 index f4db9a6a..00000000 --- a/packages/sdk/server-ai/src/ldai/runners/types.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Result types and type aliases for agent and agent graph runners.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Callable, Dict - -from ldai.providers.types import LDAIMetrics - -# Type alias for a registry of tools available to an agent. -# Keys are tool names; values are the callable implementations. -ToolRegistry = Dict[str, Callable] - - -@dataclass -class AgentResult: - """ - Result from a single-agent run. - """ - output: str - raw: Any - metrics: LDAIMetrics - - -@dataclass -class AgentGraphResult: - """ - Result from an agent graph run. - """ - output: str - raw: Any - metrics: LDAIMetrics diff --git a/packages/sdk/server-ai/tests/test_runner_abcs.py b/packages/sdk/server-ai/tests/test_runner_abcs.py index 1fc03aef..d5136fd0 100644 --- a/packages/sdk/server-ai/tests/test_runner_abcs.py +++ b/packages/sdk/server-ai/tests/test_runner_abcs.py @@ -1,14 +1,12 @@ import pytest +from ldai.providers import AgentGraphResult, AgentGraphRunner, AgentResult, AgentRunner, ToolRegistry from ldai.providers.types import LDAIMetrics -from ldai.runners.agent_graph_runner import AgentGraphRunner -from ldai.runners.agent_runner import AgentRunner -from ldai.runners.types import AgentGraphResult, AgentResult, ToolRegistry # --- Concrete test doubles --- -class ConcreteAgentRunner(AgentRunner): +class ConcreteAgentRunner: async def run(self, input): return AgentResult( output=f"agent response to: {input}", @@ -17,7 +15,7 @@ async def run(self, input): ) -class ConcreteAgentGraphRunner(AgentGraphRunner): +class ConcreteAgentGraphRunner: async def run(self, input): return AgentGraphResult( output=f"graph response to: {input}", @@ -26,11 +24,18 @@ async def run(self, input): ) +class MissingRunMethod: + pass + + # --- AgentRunner --- -def test_agent_runner_is_abstract(): - with pytest.raises(TypeError): - AgentRunner() # type: ignore[abstract] +def test_agent_runner_structural_check_passes(): + assert isinstance(ConcreteAgentRunner(), AgentRunner) + + +def test_agent_runner_structural_check_fails_when_run_missing(): + assert not isinstance(MissingRunMethod(), AgentRunner) @pytest.mark.asyncio @@ -54,9 +59,12 @@ async def test_agent_result_fields(): # --- AgentGraphRunner --- -def test_agent_graph_runner_is_abstract(): - with pytest.raises(TypeError): - AgentGraphRunner() # type: ignore[abstract] +def test_agent_graph_runner_structural_check_passes(): + assert isinstance(ConcreteAgentGraphRunner(), AgentGraphRunner) + + +def test_agent_graph_runner_structural_check_fails_when_run_missing(): + assert not isinstance(MissingRunMethod(), AgentGraphRunner) @pytest.mark.asyncio