Skip to content

Commit a564649

Browse files
jsonbaileyclaude
andcommitted
feat!: Add ManagedResult, RunnerResult, and Runner protocol; rename invoke() to run()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 381cf75 commit a564649

9 files changed

Lines changed: 358 additions & 114 deletions

File tree

packages/sdk/server-ai/src/ldai/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@
3636
AgentGraphRunner,
3737
AgentResult,
3838
AgentRunner,
39+
ManagedResult,
40+
Runner,
41+
RunnerResult,
3942
ToolRegistry,
4043
)
4144
from ldai.providers.types import JudgeResult
42-
from ldai.tracker import AIGraphTracker
45+
from ldai.tracker import AIGraphTracker, LDAIMetricSummary
4346

4447
__all__ = [
4548
'LDAIClient',
@@ -48,6 +51,10 @@
4851
'AgentGraphRunner',
4952
'AgentResult',
5053
'AgentGraphResult',
54+
'ManagedResult',
55+
'Runner',
56+
'RunnerResult',
57+
'LDAIMetricSummary',
5158
'ToolRegistry',
5259
'AIAgentConfig',
5360
'AIAgentConfigDefault',

packages/sdk/server-ai/src/ldai/managed_agent.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,55 @@
11
"""ManagedAgent — LaunchDarkly managed wrapper for agent invocations."""
22

3+
from typing import Union
4+
35
from ldai.models import AIAgentConfig
46
from ldai.providers import AgentResult, AgentRunner
7+
from ldai.providers.runner import Runner
8+
from ldai.providers.types import ManagedResult, RunnerResult
59

610

711
class ManagedAgent:
812
"""
913
LaunchDarkly managed wrapper for AI agent invocations.
1014
11-
Holds an AgentRunner. Handles tracking automatically via ``create_tracker()``.
15+
Holds an AgentRunner or Runner. Handles tracking automatically via
16+
``create_tracker()``.
1217
Obtain an instance via ``LDAIClient.create_agent()``.
1318
"""
1419

1520
def __init__(
1621
self,
1722
ai_config: AIAgentConfig,
18-
agent_runner: AgentRunner,
23+
agent_runner: Union[Runner, AgentRunner],
1924
):
2025
self._ai_config = ai_config
2126
self._agent_runner = agent_runner
2227

23-
async def run(self, input: str) -> AgentResult:
28+
async def run(self, input: str) -> ManagedResult:
2429
"""
2530
Run the agent with the given input string.
2631
2732
:param input: The user prompt or input to the agent
28-
:return: AgentResult containing the agent's output and metrics
33+
:return: ManagedResult containing the agent's output and metric summary
2934
"""
3035
tracker = self._ai_config.create_tracker()
31-
return await tracker.track_metrics_of_async(
32-
lambda result: result.metrics,
36+
result: Union[RunnerResult, AgentResult] = await tracker.track_metrics_of_async(
37+
lambda r: r.metrics,
3338
lambda: self._agent_runner.run(input),
3439
)
40+
# Support both RunnerResult (content) and legacy AgentResult (output)
41+
content = result.content if isinstance(result, RunnerResult) else result.output # type: ignore[union-attr]
42+
return ManagedResult(
43+
content=content,
44+
metrics=tracker.get_summary(),
45+
raw=result.raw,
46+
)
3547

36-
def get_agent_runner(self) -> AgentRunner:
48+
def get_agent_runner(self) -> Union[Runner, AgentRunner]:
3749
"""
38-
Return the underlying AgentRunner for advanced use.
50+
Return the underlying runner for advanced use.
3951
40-
:return: The AgentRunner instance.
52+
:return: The Runner or AgentRunner instance.
4153
"""
4254
return self._agent_runner
4355

packages/sdk/server-ai/src/ldai/managed_model.py

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,112 @@
11
import asyncio
2-
from typing import List, Optional
2+
import warnings
3+
from typing import List, Union
34

45
from ldai.models import AICompletionConfig, LDMessage
56
from ldai.providers.model_runner import ModelRunner
6-
from ldai.providers.types import JudgeResult, ModelResponse
7+
from ldai.providers.runner import Runner
8+
from ldai.providers.types import JudgeResult, ManagedResult, ModelResponse, RunnerResult
79
from ldai.tracker import LDAIConfigTracker
810

911

1012
class ManagedModel:
1113
"""
1214
LaunchDarkly managed wrapper for AI model invocations.
1315
14-
Holds a ModelRunner. Handles conversation management, judge evaluation
15-
dispatch, and tracking automatically via ``create_tracker()``.
16+
Holds a Runner (or legacy ModelRunner). Handles conversation management,
17+
judge evaluation dispatch, and tracking automatically via ``create_tracker()``.
1618
Obtain an instance via ``LDAIClient.create_model()``.
1719
"""
1820

1921
def __init__(
2022
self,
2123
ai_config: AICompletionConfig,
22-
model_runner: ModelRunner,
24+
model_runner: Union[Runner, ModelRunner],
2325
):
2426
self._ai_config = ai_config
2527
self._model_runner = model_runner
2628
self._messages: List[LDMessage] = []
2729

28-
async def invoke(self, prompt: str) -> ModelResponse:
30+
async def run(self, prompt: str) -> ManagedResult:
2931
"""
30-
Invoke the model with a prompt string.
32+
Run the model with a prompt string.
3133
3234
Appends the prompt to the conversation history, prepends any
3335
system messages from the config, delegates to the runner, and
3436
appends the response to the history.
3537
38+
:param prompt: The user prompt to send to the model
39+
:return: ManagedResult containing the model's response, metric summary,
40+
and an optional evaluations task
41+
"""
42+
tracker = self._ai_config.create_tracker()
43+
44+
user_message = LDMessage(role='user', content=prompt)
45+
self._messages.append(user_message)
46+
47+
config_messages = self._ai_config.messages or []
48+
all_messages = config_messages + self._messages
49+
50+
result: Union[RunnerResult, ModelResponse] = await tracker.track_metrics_of_async(
51+
lambda r: r.metrics,
52+
lambda: self._invoke_runner(all_messages),
53+
)
54+
55+
# Support both new RunnerResult and legacy ModelResponse
56+
if isinstance(result, RunnerResult):
57+
content = result.content
58+
raw = result.raw
59+
parsed = result.parsed
60+
assistant_message = LDMessage(role='assistant', content=content)
61+
else:
62+
content = result.message.content
63+
raw = getattr(result, 'raw', None)
64+
parsed = getattr(result, 'parsed', None)
65+
assistant_message = result.message
66+
67+
input_text = '\r\n'.join(m.content for m in self._messages) if self._messages else ''
68+
69+
evaluations_task = self._track_judge_results(tracker, input_text, content)
70+
71+
self._messages.append(assistant_message)
72+
73+
return ManagedResult(
74+
content=content,
75+
metrics=tracker.get_summary(),
76+
raw=raw,
77+
parsed=parsed,
78+
evaluations=evaluations_task,
79+
)
80+
81+
async def _invoke_runner(
82+
self, all_messages: List[LDMessage]
83+
) -> Union[RunnerResult, ModelResponse]:
84+
"""
85+
Delegate to the runner. Supports both the new ``Runner`` protocol
86+
(``run(messages) → RunnerResult``) and the legacy ``ModelRunner``
87+
(``invoke_model(messages) → ModelResponse``).
88+
"""
89+
if isinstance(self._model_runner, Runner):
90+
return await self._model_runner.run(all_messages)
91+
# Legacy ModelRunner path
92+
return await self._model_runner.invoke_model(all_messages) # type: ignore[union-attr]
93+
94+
async def invoke(self, prompt: str) -> ModelResponse:
95+
"""
96+
Invoke the model with a prompt string.
97+
98+
.. deprecated::
99+
Use :meth:`run` instead. This method will be removed in a future
100+
release once the migration to :class:`ManagedResult` is complete.
101+
36102
:param prompt: The user prompt to send to the model
37103
:return: ModelResponse containing the model's response and metrics
38104
"""
105+
warnings.warn(
106+
"ManagedModel.invoke() is deprecated. Use run() instead.",
107+
DeprecationWarning,
108+
stacklevel=2,
109+
)
39110
tracker = self._ai_config.create_tracker()
40111

41112
user_message = LDMessage(role='user', content=prompt)
@@ -44,9 +115,9 @@ async def invoke(self, prompt: str) -> ModelResponse:
44115
config_messages = self._ai_config.messages or []
45116
all_messages = config_messages + self._messages
46117

47-
response = await tracker.track_metrics_of_async(
118+
response: ModelResponse = await tracker.track_metrics_of_async(
48119
lambda result: result.metrics,
49-
lambda: self._model_runner.invoke_model(all_messages),
120+
lambda: self._model_runner.invoke_model(all_messages), # type: ignore[union-attr]
50121
)
51122

52123
input_text = '\r\n'.join(m.content for m in self._messages) if self._messages else ''

packages/sdk/server-ai/src/ldai/providers/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
from ldai.providers.agent_runner import AgentRunner
33
from ldai.providers.ai_provider import AIProvider
44
from ldai.providers.model_runner import ModelRunner
5+
from ldai.providers.runner import Runner
56
from ldai.providers.runner_factory import RunnerFactory
67
from ldai.providers.types import (
78
AgentGraphResult,
89
AgentResult,
910
JudgeResult,
1011
LDAIMetrics,
12+
ManagedResult,
1113
ModelResponse,
14+
RunnerResult,
1215
StructuredResponse,
1316
ToolRegistry,
1417
)
@@ -21,9 +24,12 @@
2124
'AgentRunner',
2225
'JudgeResult',
2326
'LDAIMetrics',
27+
'ManagedResult',
2428
'ModelResponse',
2529
'ModelRunner',
30+
'Runner',
2631
'RunnerFactory',
32+
'RunnerResult',
2733
'StructuredResponse',
2834
'ToolRegistry',
2935
]
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Unified Runner protocol for AI providers."""
2+
3+
from typing import Any, Dict, Optional, Protocol, runtime_checkable
4+
5+
from ldai.providers.types import RunnerResult
6+
7+
8+
@runtime_checkable
9+
class Runner(Protocol):
10+
"""
11+
Unified runtime capability interface for all AI provider runners.
12+
13+
A :class:`Runner` is a focused, configured object that performs a single
14+
AI invocation. Both model runners and agent runners implement this protocol.
15+
16+
:param input: The input to the runner (string prompt, list of messages, or
17+
other provider-specific input type).
18+
:param output_type: Optional JSON schema dict that requests structured output.
19+
When provided, the runner populates :attr:`~RunnerResult.parsed` on the
20+
returned :class:`RunnerResult`.
21+
:return: :class:`RunnerResult` containing ``content``, ``metrics``, and
22+
optionally ``raw`` and ``parsed``.
23+
"""
24+
25+
async def run(
26+
self,
27+
input: Any,
28+
output_type: Optional[Dict[str, Any]] = None,
29+
) -> RunnerResult:
30+
"""
31+
Execute the runner with the given input.
32+
33+
:param input: The input to the runner.
34+
:param output_type: Optional JSON schema for structured output.
35+
:return: RunnerResult containing content, metrics, raw, and parsed fields.
36+
"""
37+
...

packages/sdk/server-ai/src/ldai/providers/types.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any, Callable, Dict, List, Optional
88

99
from ldai.models import LDMessage
10-
from ldai.tracker import TokenUsage
10+
from ldai.tracker import LDAIMetricSummary, TokenUsage
1111

1212
# Type alias for a registry of tools available to an agent.
1313
# Keys are tool names; values are the callable implementations.
@@ -38,10 +38,48 @@ def to_dict(self) -> Dict[str, Any]:
3838
return result
3939

4040

41+
@dataclass
42+
class RunnerResult:
43+
"""
44+
Result returned by a :class:`~ldai.providers.runner.Runner` from a single
45+
invocation.
46+
47+
This is the unified return type for all Runner implementations.
48+
``evaluations`` is intentionally absent — judge evaluations are dispatched
49+
by the managed layer and live on :class:`ManagedResult`.
50+
"""
51+
content: str
52+
metrics: LDAIMetrics
53+
raw: Optional[Any] = None
54+
parsed: Optional[Dict[str, Any]] = None
55+
56+
57+
@dataclass
58+
class ManagedResult:
59+
"""
60+
Result returned by the managed layer (:class:`~ldai.ManagedModel` /
61+
:class:`~ldai.ManagedAgent`) after a single invocation.
62+
63+
``metrics`` is an :class:`~ldai.tracker.LDAIMetricSummary` (from
64+
``tracker.get_summary()``) rather than a raw :class:`LDAIMetrics`.
65+
``evaluations`` is an optional asyncio Task that resolves to a list of
66+
:class:`JudgeResult` instances when awaited.
67+
"""
68+
content: str
69+
metrics: LDAIMetricSummary
70+
raw: Optional[Any] = None
71+
parsed: Optional[Dict[str, Any]] = None
72+
evaluations: Optional[asyncio.Task[List[JudgeResult]]] = None
73+
74+
4175
@dataclass
4276
class ModelResponse:
4377
"""
4478
Response from a model invocation.
79+
80+
.. deprecated::
81+
Use :class:`RunnerResult` (from a runner) and :class:`ManagedResult`
82+
(from the managed layer) instead.
4583
"""
4684
message: LDMessage
4785
metrics: LDAIMetrics
@@ -52,6 +90,9 @@ class ModelResponse:
5290
class StructuredResponse:
5391
"""
5492
Structured response from AI models.
93+
94+
.. deprecated::
95+
Structured output is now represented by :attr:`RunnerResult.parsed`.
5596
"""
5697
data: Dict[str, Any]
5798
raw_response: str
@@ -96,6 +137,10 @@ def to_dict(self) -> Dict[str, Any]:
96137
class AgentResult:
97138
"""
98139
Result from a single-agent run.
140+
141+
.. deprecated::
142+
Use :class:`ManagedResult` (managed layer) or :class:`RunnerResult`
143+
(runner layer) instead.
99144
"""
100145
output: str
101146
raw: Any

0 commit comments

Comments
 (0)