agentloop-profiler 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""Explicit LangGraph dispatch controls; the SDK remains an optional dependency."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable, Iterator
|
|
7
|
+
from contextlib import contextmanager, nullcontext
|
|
8
|
+
from contextvars import ContextVar
|
|
9
|
+
from dataclasses import dataclass, replace
|
|
10
|
+
from functools import lru_cache, wraps
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
12
|
+
from weakref import ReferenceType, ref
|
|
13
|
+
|
|
14
|
+
from agentloop.budget_types import DispatchOptions
|
|
15
|
+
from agentloop.harness import (
|
|
16
|
+
ACTIONS,
|
|
17
|
+
BOUNDARIES,
|
|
18
|
+
EXECUTION_KINDS,
|
|
19
|
+
AdapterCapabilities,
|
|
20
|
+
Harness,
|
|
21
|
+
HarnessConfig,
|
|
22
|
+
HarnessControlError,
|
|
23
|
+
HarnessRun,
|
|
24
|
+
Hook,
|
|
25
|
+
)
|
|
26
|
+
from agentloop.harness_evidence import HarnessEvidenceError
|
|
27
|
+
from agentloop.loop_types import StepInfo
|
|
28
|
+
from agentloop.tracer import AgentTrace, bind_trace_context, current_event_id, current_trace
|
|
29
|
+
|
|
30
|
+
SUPPORTED_VERSION = "1.2.11"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_version() -> None:
|
|
34
|
+
try:
|
|
35
|
+
installed = version("langgraph")
|
|
36
|
+
except PackageNotFoundError:
|
|
37
|
+
installed = None
|
|
38
|
+
if installed != SUPPORTED_VERSION:
|
|
39
|
+
raise RuntimeError(f"LangGraph harness requires langgraph=={SUPPORTED_VERSION}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@lru_cache(maxsize=1)
|
|
43
|
+
def _signal_type():
|
|
44
|
+
# Public control-flow base: the pinned runtime bypasses retries and handlers.
|
|
45
|
+
from langgraph.errors import GraphBubbleUp
|
|
46
|
+
|
|
47
|
+
class HarnessSignal(GraphBubbleUp):
|
|
48
|
+
def __init__(self, original):
|
|
49
|
+
self.original = original
|
|
50
|
+
super().__init__("AgentLoop harness control")
|
|
51
|
+
|
|
52
|
+
return HarnessSignal
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _runtime_identity() -> tuple[str, int]:
|
|
56
|
+
from langgraph.runtime import get_runtime
|
|
57
|
+
|
|
58
|
+
info = get_runtime().execution_info
|
|
59
|
+
task_id, attempt = info.task_id, info.node_attempt
|
|
60
|
+
if not isinstance(task_id, str) or not task_id or type(attempt) is not int or attempt < 1:
|
|
61
|
+
raise RuntimeError("LangGraph did not supply a valid task identity and attempt")
|
|
62
|
+
return task_id, attempt
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _kind(function):
|
|
66
|
+
for kind, check in (
|
|
67
|
+
("async_generator", inspect.isasyncgenfunction),
|
|
68
|
+
("generator", inspect.isgeneratorfunction),
|
|
69
|
+
("async", inspect.iscoroutinefunction),
|
|
70
|
+
):
|
|
71
|
+
if check(function) or check(getattr(function, "__call__", None)):
|
|
72
|
+
return kind
|
|
73
|
+
return "sync"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class _Session:
|
|
78
|
+
run: HarnessRun
|
|
79
|
+
trace: AgentTrace | None
|
|
80
|
+
parent: str | None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _ScopedIterator:
|
|
84
|
+
"""Forward the generator protocol with no context binding across yields."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, source, scope):
|
|
87
|
+
self.source, self.scope = source, scope
|
|
88
|
+
|
|
89
|
+
def __iter__(self):
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def __next__(self):
|
|
93
|
+
with self.scope():
|
|
94
|
+
return next(self.source)
|
|
95
|
+
|
|
96
|
+
def send(self, value):
|
|
97
|
+
with self.scope():
|
|
98
|
+
return self.source.send(value)
|
|
99
|
+
|
|
100
|
+
def throw(self, *args):
|
|
101
|
+
with self.scope():
|
|
102
|
+
return self.source.throw(*args)
|
|
103
|
+
|
|
104
|
+
def close(self):
|
|
105
|
+
with self.scope():
|
|
106
|
+
close = getattr(self.source, "close", None)
|
|
107
|
+
if close is not None:
|
|
108
|
+
return close()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _ScopedAsyncIterator:
|
|
112
|
+
def __init__(self, source, scope):
|
|
113
|
+
self.source, self.scope = source, scope
|
|
114
|
+
|
|
115
|
+
async def __anext__(self):
|
|
116
|
+
with self.scope():
|
|
117
|
+
return await self.source.__anext__()
|
|
118
|
+
|
|
119
|
+
async def asend(self, value):
|
|
120
|
+
with self.scope():
|
|
121
|
+
return await self.source.asend(value)
|
|
122
|
+
|
|
123
|
+
async def athrow(self, error):
|
|
124
|
+
with self.scope():
|
|
125
|
+
return await self.source.athrow(error)
|
|
126
|
+
|
|
127
|
+
async def aclose(self):
|
|
128
|
+
with self.scope():
|
|
129
|
+
return await self.source.aclose()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _identity(value):
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class LangGraphHarness:
|
|
137
|
+
"""Register explicit boundaries before compiling, then wrap the compiled app.
|
|
138
|
+
|
|
139
|
+
``boundaries`` declares coverage, not discovery: only decorated callables
|
|
140
|
+
are controlled. Node admission uses the ``iteration`` boundary. No builder
|
|
141
|
+
registry, provider client, or existing tracing adapter is modified.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
config: HarnessConfig = HarnessConfig(),
|
|
147
|
+
*,
|
|
148
|
+
boundaries=frozenset({"iteration", "completion"}),
|
|
149
|
+
):
|
|
150
|
+
boundaries = frozenset(boundaries)
|
|
151
|
+
if not boundaries <= BOUNDARIES:
|
|
152
|
+
raise ValueError("unsupported LangGraph harness boundary")
|
|
153
|
+
self._harness = Harness(
|
|
154
|
+
config,
|
|
155
|
+
AdapterCapabilities(
|
|
156
|
+
"langgraph",
|
|
157
|
+
frozenset(
|
|
158
|
+
Hook(boundary, phase)
|
|
159
|
+
for boundary in boundaries
|
|
160
|
+
for phase in ("before", "after")
|
|
161
|
+
),
|
|
162
|
+
ACTIONS,
|
|
163
|
+
EXECUTION_KINDS,
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
self._session: ContextVar[_Session | None] = ContextVar(
|
|
167
|
+
"langgraph_harness_session", default=None
|
|
168
|
+
)
|
|
169
|
+
self._branch: ContextVar[str] = ContextVar("langgraph_harness_branch", default="main")
|
|
170
|
+
self._last_run: ContextVar[HarnessRun | None] = ContextVar(
|
|
171
|
+
"langgraph_harness_last_run", default=None
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def capabilities(self) -> AdapterCapabilities:
|
|
176
|
+
return self._harness.capabilities
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def last_run(self) -> HarnessRun | None:
|
|
180
|
+
"""Most recently started run in this caller's context, including failures."""
|
|
181
|
+
return self._last_run.get()
|
|
182
|
+
|
|
183
|
+
@contextmanager
|
|
184
|
+
def _scope(self, session, branch=None, *, root=False) -> Iterator[None]:
|
|
185
|
+
session_token = self._session.set(session)
|
|
186
|
+
branch_token = self._branch.set(branch if branch is not None else self._branch.get())
|
|
187
|
+
trace_scope = bind_trace_context(session.trace, session.parent) if root else nullcontext()
|
|
188
|
+
try:
|
|
189
|
+
with trace_scope:
|
|
190
|
+
try:
|
|
191
|
+
yield
|
|
192
|
+
except (HarnessControlError, HarnessEvidenceError) as exc:
|
|
193
|
+
if root:
|
|
194
|
+
raise
|
|
195
|
+
raise _signal_type()(exc) from None
|
|
196
|
+
except _signal_type() as exc:
|
|
197
|
+
if root:
|
|
198
|
+
raise exc.original from None
|
|
199
|
+
raise
|
|
200
|
+
finally:
|
|
201
|
+
self._branch.reset(branch_token)
|
|
202
|
+
self._session.reset(session_token)
|
|
203
|
+
|
|
204
|
+
def _start(self, run=None):
|
|
205
|
+
if run is not None and (
|
|
206
|
+
not isinstance(run, HarnessRun) or run.harness is not self._harness
|
|
207
|
+
):
|
|
208
|
+
raise ValueError("recovered run must belong to this adapter")
|
|
209
|
+
session = _Session(
|
|
210
|
+
self._harness.start_run() if run is None else run, current_trace(), current_event_id()
|
|
211
|
+
)
|
|
212
|
+
self._last_run.set(session.run)
|
|
213
|
+
return session
|
|
214
|
+
|
|
215
|
+
def node(self, name: str, *, branch_id: str | None = None, step_reader: Callable | None = None):
|
|
216
|
+
"""Decorate a sync/async node before add_node; arguments are passed unchanged.
|
|
217
|
+
|
|
218
|
+
By default each framework task is a separate branch. Supply a stable
|
|
219
|
+
branch_id to compare sequential graph iterations. An optional trusted
|
|
220
|
+
step_reader accepts the same arguments and returns bounded StepInfo.
|
|
221
|
+
"""
|
|
222
|
+
StepInfo(name)
|
|
223
|
+
if branch_id is not None and (not isinstance(branch_id, str) or not branch_id):
|
|
224
|
+
raise ValueError("branch_id must be a nonempty string")
|
|
225
|
+
if step_reader is not None and (not callable(step_reader) or _kind(step_reader) != "sync"):
|
|
226
|
+
raise ValueError("step_reader must be a synchronous callable")
|
|
227
|
+
return lambda function: self._wrap(
|
|
228
|
+
function,
|
|
229
|
+
"iteration",
|
|
230
|
+
DispatchOptions(step=StepInfo(name)),
|
|
231
|
+
None,
|
|
232
|
+
node=True,
|
|
233
|
+
branch_id=branch_id,
|
|
234
|
+
step_reader=step_reader,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def model(self, function, *, dispatch: DispatchOptions | None = None, usage_reader=None):
|
|
238
|
+
"""Wrap one explicit model callable, including a lazy stream callable."""
|
|
239
|
+
return self._wrap(
|
|
240
|
+
function, "model", DispatchOptions() if dispatch is None else dispatch, usage_reader
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def tool(self, function, *, dispatch: DispatchOptions | None = None, usage_reader=None):
|
|
244
|
+
"""Wrap one explicit tool callable; hidden work is outside this boundary."""
|
|
245
|
+
return self._wrap(
|
|
246
|
+
function, "tool", DispatchOptions() if dispatch is None else dispatch, usage_reader
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _wrap(
|
|
250
|
+
self,
|
|
251
|
+
function,
|
|
252
|
+
boundary,
|
|
253
|
+
options,
|
|
254
|
+
usage_reader,
|
|
255
|
+
*,
|
|
256
|
+
node=False,
|
|
257
|
+
branch_id=None,
|
|
258
|
+
step_reader=None,
|
|
259
|
+
):
|
|
260
|
+
if not callable(function):
|
|
261
|
+
raise ValueError("protected work must be callable")
|
|
262
|
+
if self._harness.config.mode == "disabled":
|
|
263
|
+
return function
|
|
264
|
+
if Hook(boundary) not in self.capabilities.hooks:
|
|
265
|
+
raise ValueError("boundary must be declared in LangGraphHarness(boundaries=...)")
|
|
266
|
+
if type(options) is not DispatchOptions:
|
|
267
|
+
raise ValueError("dispatch must be DispatchOptions")
|
|
268
|
+
if usage_reader is not None and (
|
|
269
|
+
not callable(usage_reader) or _kind(usage_reader) != "sync"
|
|
270
|
+
):
|
|
271
|
+
raise ValueError("usage_reader must be a synchronous callable")
|
|
272
|
+
kind = _kind(function)
|
|
273
|
+
if node and kind in {"generator", "async_generator"}:
|
|
274
|
+
raise ValueError(
|
|
275
|
+
"generator nodes are unsupported; compiled graph streams are supported"
|
|
276
|
+
)
|
|
277
|
+
settings = (self, boundary, options, usage_reader, node, branch_id, step_reader)
|
|
278
|
+
marker = getattr(function, "__agentloop_langgraph_harness__", None)
|
|
279
|
+
if (
|
|
280
|
+
isinstance(marker, tuple)
|
|
281
|
+
and len(marker) == 2
|
|
282
|
+
and isinstance(marker[0], ReferenceType)
|
|
283
|
+
and marker[0]() is function
|
|
284
|
+
):
|
|
285
|
+
if marker[1] == settings:
|
|
286
|
+
return function
|
|
287
|
+
raise ValueError("wrap the original callable to change harness settings")
|
|
288
|
+
|
|
289
|
+
def prepare(args, kwargs):
|
|
290
|
+
session = self._session.get()
|
|
291
|
+
if session is None:
|
|
292
|
+
raise RuntimeError("active LangGraph controls require adapter.runnable(app)")
|
|
293
|
+
branch, dispatch = self._branch.get(), options
|
|
294
|
+
if node:
|
|
295
|
+
task_id, attempt = _runtime_identity()
|
|
296
|
+
branch = branch_id or task_id
|
|
297
|
+
step = step_reader(*args, **kwargs) if step_reader is not None else options.step
|
|
298
|
+
if type(step) is not StepInfo:
|
|
299
|
+
raise ValueError("step_reader must return StepInfo")
|
|
300
|
+
dispatch = replace(
|
|
301
|
+
options, step=step, retry_source="framework" if attempt > 1 else None
|
|
302
|
+
)
|
|
303
|
+
protected = session.run.wrap(
|
|
304
|
+
function,
|
|
305
|
+
boundary=boundary,
|
|
306
|
+
branch_id=branch,
|
|
307
|
+
dispatch=dispatch,
|
|
308
|
+
usage_reader=usage_reader,
|
|
309
|
+
)
|
|
310
|
+
return protected, lambda: self._scope(session, branch)
|
|
311
|
+
|
|
312
|
+
if kind == "async":
|
|
313
|
+
|
|
314
|
+
@wraps(function)
|
|
315
|
+
async def wrapped(*args, **kwargs):
|
|
316
|
+
protected, scope = prepare(args, kwargs)
|
|
317
|
+
with scope():
|
|
318
|
+
return await protected(*args, **kwargs)
|
|
319
|
+
elif kind == "generator":
|
|
320
|
+
|
|
321
|
+
@wraps(function)
|
|
322
|
+
def wrapped(*args, **kwargs):
|
|
323
|
+
protected, scope = prepare(args, kwargs)
|
|
324
|
+
with scope():
|
|
325
|
+
source = protected(*args, **kwargs)
|
|
326
|
+
return (yield from _ScopedIterator(source, scope))
|
|
327
|
+
elif kind == "async_generator":
|
|
328
|
+
|
|
329
|
+
@wraps(function)
|
|
330
|
+
async def wrapped(*args, **kwargs):
|
|
331
|
+
protected, scope = prepare(args, kwargs)
|
|
332
|
+
source = _ScopedAsyncIterator(protected(*args, **kwargs), scope)
|
|
333
|
+
# Forward sends, throws and close instead of reducing to async-for.
|
|
334
|
+
try:
|
|
335
|
+
item = await source.__anext__()
|
|
336
|
+
while True:
|
|
337
|
+
try:
|
|
338
|
+
sent = yield item
|
|
339
|
+
except GeneratorExit:
|
|
340
|
+
await source.aclose()
|
|
341
|
+
raise
|
|
342
|
+
except BaseException as exc:
|
|
343
|
+
item = await source.athrow(exc)
|
|
344
|
+
else:
|
|
345
|
+
item = await source.asend(sent)
|
|
346
|
+
except StopAsyncIteration:
|
|
347
|
+
return
|
|
348
|
+
else:
|
|
349
|
+
|
|
350
|
+
@wraps(function)
|
|
351
|
+
def wrapped(*args, **kwargs):
|
|
352
|
+
protected, scope = prepare(args, kwargs)
|
|
353
|
+
with scope():
|
|
354
|
+
return protected(*args, **kwargs)
|
|
355
|
+
|
|
356
|
+
wrapped.__agentloop_langgraph_harness__ = (ref(wrapped), settings)
|
|
357
|
+
return wrapped
|
|
358
|
+
|
|
359
|
+
def runnable(self, app):
|
|
360
|
+
"""Wrap invoke/ainvoke/stream/astream; disabled mode returns app itself."""
|
|
361
|
+
if self._harness.config.mode == "disabled":
|
|
362
|
+
return app
|
|
363
|
+
if isinstance(app, ControlledRunnable):
|
|
364
|
+
if app._adapter is self:
|
|
365
|
+
return app
|
|
366
|
+
raise ValueError("runnable is already controlled by another adapter")
|
|
367
|
+
_check_version()
|
|
368
|
+
for method in ("invoke", "ainvoke", "stream", "astream"):
|
|
369
|
+
if not callable(getattr(app, method, None)):
|
|
370
|
+
raise TypeError(f"compiled app must provide {method}")
|
|
371
|
+
return ControlledRunnable(self, app)
|
|
372
|
+
|
|
373
|
+
def _complete(self, session, value):
|
|
374
|
+
if Hook("completion") not in self.capabilities.hooks:
|
|
375
|
+
return value
|
|
376
|
+
return session.run.wrap(_identity, boundary="completion")(value)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class ControlledRunnable:
|
|
380
|
+
"""The four controlled entrypoints; access the original app for other APIs.
|
|
381
|
+
|
|
382
|
+
This intentionally does not proxy batch or configuration methods that could
|
|
383
|
+
return an unprotected runnable. Rewrap such a runnable explicitly.
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
def __init__(self, adapter, app):
|
|
387
|
+
self._adapter, self.app = adapter, app
|
|
388
|
+
self._last_invocation = ContextVar("langgraph_root_invocation", default=None)
|
|
389
|
+
self._recovery_bound = False
|
|
390
|
+
|
|
391
|
+
def _ordinary_entry(self):
|
|
392
|
+
if self._recovery_bound:
|
|
393
|
+
raise ValueError("checkpoint-owned runnable requires LangGraphRecovery.resume")
|
|
394
|
+
|
|
395
|
+
def invoke(self, *args, **kwargs):
|
|
396
|
+
self._ordinary_entry()
|
|
397
|
+
adapter = self._adapter
|
|
398
|
+
session = adapter._start()
|
|
399
|
+
config = kwargs.get("config", args[1] if len(args) > 1 else None)
|
|
400
|
+
selected = config.get("configurable", {}) if isinstance(config, dict) else {}
|
|
401
|
+
selected = selected if isinstance(selected, dict) else {}
|
|
402
|
+
identity = {
|
|
403
|
+
"run_id": session.run.run_id,
|
|
404
|
+
"thread_id": selected.get("thread_id"),
|
|
405
|
+
"checkpoint_ns": selected.get("checkpoint_ns", ""),
|
|
406
|
+
"trace_id": None if session.trace is None else session.trace.run_id,
|
|
407
|
+
"durability": kwargs.get("durability"),
|
|
408
|
+
"finished": False,
|
|
409
|
+
}
|
|
410
|
+
self._last_invocation.set(identity)
|
|
411
|
+
try:
|
|
412
|
+
with adapter._scope(session, "main", root=True):
|
|
413
|
+
return adapter._complete(session, self.app.invoke(*args, **kwargs))
|
|
414
|
+
finally:
|
|
415
|
+
self._last_invocation.set({**identity, "finished": True})
|
|
416
|
+
|
|
417
|
+
async def ainvoke(self, *args, **kwargs):
|
|
418
|
+
self._ordinary_entry()
|
|
419
|
+
self._last_invocation.set(None)
|
|
420
|
+
adapter = self._adapter
|
|
421
|
+
session = adapter._start()
|
|
422
|
+
with adapter._scope(session, "main", root=True):
|
|
423
|
+
return adapter._complete(session, await self.app.ainvoke(*args, **kwargs))
|
|
424
|
+
|
|
425
|
+
def stream(self, *args, **kwargs):
|
|
426
|
+
self._ordinary_entry()
|
|
427
|
+
self._last_invocation.set(None)
|
|
428
|
+
adapter = self._adapter
|
|
429
|
+
session = adapter._start()
|
|
430
|
+
|
|
431
|
+
def scope():
|
|
432
|
+
return adapter._scope(session, "main", root=True)
|
|
433
|
+
|
|
434
|
+
with scope():
|
|
435
|
+
source = self.app.stream(*args, **kwargs)
|
|
436
|
+
result = yield from _ScopedIterator(source, scope)
|
|
437
|
+
with scope():
|
|
438
|
+
return adapter._complete(session, result)
|
|
439
|
+
|
|
440
|
+
async def astream(self, *args, **kwargs):
|
|
441
|
+
self._ordinary_entry()
|
|
442
|
+
self._last_invocation.set(None)
|
|
443
|
+
adapter = self._adapter
|
|
444
|
+
session = adapter._start()
|
|
445
|
+
|
|
446
|
+
def scope():
|
|
447
|
+
return adapter._scope(session, "main", root=True)
|
|
448
|
+
|
|
449
|
+
with scope():
|
|
450
|
+
source = self.app.astream(*args, **kwargs)
|
|
451
|
+
source = _ScopedAsyncIterator(source, scope)
|
|
452
|
+
try:
|
|
453
|
+
item = await source.__anext__()
|
|
454
|
+
while True:
|
|
455
|
+
try:
|
|
456
|
+
sent = yield item
|
|
457
|
+
except GeneratorExit:
|
|
458
|
+
await source.aclose()
|
|
459
|
+
raise
|
|
460
|
+
except BaseException as exc:
|
|
461
|
+
item = await source.athrow(exc)
|
|
462
|
+
else:
|
|
463
|
+
item = await source.asend(sent)
|
|
464
|
+
except StopAsyncIteration:
|
|
465
|
+
with scope():
|
|
466
|
+
adapter._complete(session, None)
|