Skip to content

Commit b0ca696

Browse files
jsonbaileyclaude
andcommitted
feat(ldai)!: Add ManagedResult and Runner protocol
Introduces the new managed-layer return type ``ManagedResult`` and the unified ``Runner`` protocol, plus extends ``LDAIMetricSummary`` with ``tool_calls``, ``duration_ms`` (renamed from ``duration``), and ``resumption_token``. * ``ManagedModel.run()`` is the new primary API; returns ``ManagedResult``. ``ManagedModel.invoke()`` is preserved as a deprecated alias for the duration of the migration to provider runners. * ``ManagedAgent.run()`` now returns ``ManagedResult``. * ``RunnerResult`` added (no ``evaluations`` field — judge dispatch lives on the managed layer). * ``LDAIConfigTracker.__init__`` now seeds ``LDAIMetricSummary._resumption_token`` at instantiation so the token is available on ``get_summary()``. * ``ModelResponse``, ``StructuredResponse``, ``AgentResult``, ``ModelRunner``, and ``AgentRunner`` are kept as deprecated symbols so the OpenAI and LangChain provider packages keep working until they migrate to the new ``Runner`` protocol in the follow-up PRs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a2402ea commit b0ca696

9 files changed

Lines changed: 317 additions & 104 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: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from ldai.models import AIAgentConfig
44
from ldai.providers import AgentResult, AgentRunner
5+
from ldai.providers.types import ManagedResult
56

67

78
class ManagedAgent:
@@ -20,18 +21,23 @@ def __init__(
2021
self._ai_config = ai_config
2122
self._agent_runner = agent_runner
2223

23-
async def run(self, input: str) -> AgentResult:
24+
async def run(self, input: str) -> ManagedResult:
2425
"""
2526
Run the agent with the given input string.
2627
2728
:param input: The user prompt or input to the agent
28-
:return: AgentResult containing the agent's output and metrics
29+
:return: ManagedResult containing the agent's output and metric summary
2930
"""
3031
tracker = self._ai_config.create_tracker()
31-
return await tracker.track_metrics_of_async(
32-
lambda result: result.metrics,
32+
result: AgentResult = await tracker.track_metrics_of_async(
33+
lambda r: r.metrics,
3334
lambda: self._agent_runner.run(input),
3435
)
36+
return ManagedResult(
37+
content=result.output,
38+
metrics=tracker.get_summary(),
39+
raw=result.raw,
40+
)
3541

3642
def get_agent_runner(self) -> AgentRunner:
3743
"""

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

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import asyncio
2-
from typing import List, Optional
2+
import warnings
3+
from typing import List
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.types import JudgeResult, ManagedResult, ModelResponse
78
from ldai.tracker import LDAIConfigTracker
89

910

@@ -25,17 +26,62 @@ def __init__(
2526
self._model_runner = model_runner
2627
self._messages: List[LDMessage] = []
2728

28-
async def invoke(self, prompt: str) -> ModelResponse:
29+
async def run(self, prompt: str) -> ManagedResult:
2930
"""
30-
Invoke the model with a prompt string.
31+
Run the model with a prompt string.
3132
3233
Appends the prompt to the conversation history, prepends any
3334
system messages from the config, delegates to the runner, and
3435
appends the response to the history.
3536
37+
:param prompt: The user prompt to send to the model
38+
:return: ManagedResult containing the model's response, metric summary,
39+
and an optional evaluations task
40+
"""
41+
tracker = self._ai_config.create_tracker()
42+
43+
user_message = LDMessage(role='user', content=prompt)
44+
self._messages.append(user_message)
45+
46+
config_messages = self._ai_config.messages or []
47+
all_messages = config_messages + self._messages
48+
49+
response = await tracker.track_metrics_of_async(
50+
lambda result: result.metrics,
51+
lambda: self._model_runner.invoke_model(all_messages),
52+
)
53+
54+
content = response.message.content
55+
input_text = '\r\n'.join(m.content for m in self._messages) if self._messages else ''
56+
57+
evaluations_task = self._track_judge_results(tracker, input_text, content)
58+
59+
self._messages.append(response.message)
60+
61+
return ManagedResult(
62+
content=content,
63+
metrics=tracker.get_summary(),
64+
raw=getattr(response, 'raw', None),
65+
parsed=getattr(response, 'parsed', None),
66+
evaluations=evaluations_task,
67+
)
68+
69+
async def invoke(self, prompt: str) -> ModelResponse:
70+
"""
71+
Invoke the model with a prompt string.
72+
73+
.. deprecated::
74+
Use :meth:`run` instead. This method will be removed in a future
75+
release once the migration to :class:`ManagedResult` is complete.
76+
3677
:param prompt: The user prompt to send to the model
3778
:return: ModelResponse containing the model's response and metrics
3879
"""
80+
warnings.warn(
81+
"ManagedModel.invoke() is deprecated. Use run() instead.",
82+
DeprecationWarning,
83+
stacklevel=2,
84+
)
3985
tracker = self._ai_config.create_tracker()
4086

4187
user_message = LDMessage(role='user', content=prompt)

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

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

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,31 @@ class LDAIMetricSummary:
4141
"""
4242

4343
def __init__(self):
44-
self._duration = None
45-
self._success = None
46-
self._feedback = None
47-
self._usage = None
48-
self._time_to_first_token = None
44+
self._duration_ms: Optional[int] = None
45+
self._success: Optional[bool] = None
46+
self._feedback: Optional[Dict[str, FeedbackKind]] = None
47+
self._usage: Optional[TokenUsage] = None
48+
self._time_to_first_token: Optional[int] = None
49+
self._tool_calls: Optional[List[str]] = None
50+
self._resumption_token: Optional[str] = None
51+
52+
@property
53+
def duration_ms(self) -> Optional[int]:
54+
"""Duration of the AI operation in milliseconds."""
55+
return self._duration_ms
4956

5057
@property
5158
def duration(self) -> Optional[int]:
52-
return self._duration
59+
"""
60+
.. deprecated::
61+
Use :attr:`duration_ms` instead.
62+
"""
63+
warnings.warn(
64+
"LDAIMetricSummary.duration is deprecated. Use duration_ms instead.",
65+
DeprecationWarning,
66+
stacklevel=2,
67+
)
68+
return self._duration_ms
5369

5470
@property
5571
def success(self) -> Optional[bool]:
@@ -67,6 +83,20 @@ def usage(self) -> Optional[TokenUsage]:
6783
def time_to_first_token(self) -> Optional[int]:
6884
return self._time_to_first_token
6985

86+
@property
87+
def tool_calls(self) -> Optional[List[str]]:
88+
"""List of tool keys that were invoked during this operation."""
89+
return self._tool_calls
90+
91+
@property
92+
def resumption_token(self) -> Optional[str]:
93+
"""
94+
URL-safe Base64-encoded resumption token captured at tracker
95+
instantiation. Useful for deferred feedback flows where a downstream
96+
process needs to associate events with the original execution.
97+
"""
98+
return self._resumption_token
99+
70100

71101
class LDAIConfigTracker:
72102
"""
@@ -107,8 +137,10 @@ def __init__(
107137
self._provider_name = provider_name
108138
self._context = context
109139
self._graph_key = graph_key
110-
self._summary = LDAIMetricSummary()
111140
self._run_id = run_id
141+
self._summary = LDAIMetricSummary()
142+
# Capture resumption_token immediately so it's available on the summary at instantiation.
143+
self._summary._resumption_token = self.resumption_token
112144

113145
@property
114146
def resumption_token(self) -> str:
@@ -200,10 +232,10 @@ def track_duration(self, duration: int) -> None:
200232
201233
:param duration: Duration in milliseconds.
202234
"""
203-
if self._summary.duration is not None:
235+
if self._summary.duration_ms is not None:
204236
log.warning("Duration has already been tracked for this execution. %s", self.__get_track_data())
205237
return
206-
self._summary._duration = duration
238+
self._summary._duration_ms = duration
207239
self._ld_client.track(
208240
"$ld:ai:duration:total", self._context, self.__get_track_data(), duration
209241
)

0 commit comments

Comments
 (0)