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,410 @@
|
|
|
1
|
+
"""Bounded host-declared tool scheduling with cooperative stopping and no retries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
|
6
|
+
from contextvars import ContextVar, copy_context
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from threading import Event, RLock
|
|
10
|
+
from time import monotonic, perf_counter
|
|
11
|
+
from types import MappingProxyType
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
|
|
14
|
+
from agentloop.events import utc_now_iso
|
|
15
|
+
from agentloop.harness import Decision, HarnessControlError, HarnessRun, Hook, Policy
|
|
16
|
+
from agentloop.scheduling_plan import plan_tools
|
|
17
|
+
from agentloop.scheduling_types import (
|
|
18
|
+
SCHEDULE_KEY,
|
|
19
|
+
SCHEDULE_VERSION,
|
|
20
|
+
ScheduleConfig,
|
|
21
|
+
ScheduledResult,
|
|
22
|
+
SchedulingError,
|
|
23
|
+
ToolContext,
|
|
24
|
+
ToolOutcome,
|
|
25
|
+
)
|
|
26
|
+
from agentloop.tracer import current_trace
|
|
27
|
+
|
|
28
|
+
_ACTIVE = ContextVar("agentloop_scheduled_tool", default=None)
|
|
29
|
+
_EVIDENCE_LOCK = RLock()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class _ScheduleAdmission:
|
|
34
|
+
config: ScheduleConfig
|
|
35
|
+
|
|
36
|
+
def __call__(self, context):
|
|
37
|
+
if not context.branch_id.startswith(self.config.branch_id + ":"):
|
|
38
|
+
return Decision("continue", "schedule_not_selected")
|
|
39
|
+
active = _ACTIVE.get()
|
|
40
|
+
if (
|
|
41
|
+
active is None
|
|
42
|
+
or active["used"]
|
|
43
|
+
or active["run_id"] != context.run_id
|
|
44
|
+
or active["branch"] != context.branch_id
|
|
45
|
+
or active["config"] != self.config
|
|
46
|
+
):
|
|
47
|
+
return Decision("deny", "schedule_adapter_required")
|
|
48
|
+
active["used"] = True
|
|
49
|
+
active["record"]["harness_call_id"] = context.call_id
|
|
50
|
+
if active["unknown"] and self.config.on_unknown == "error":
|
|
51
|
+
return Decision("deny", "unknown_scheduling_safety")
|
|
52
|
+
return Decision("continue", "declared_schedule_admitted")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def scheduling_policy(config):
|
|
56
|
+
if type(config) is not ScheduleConfig:
|
|
57
|
+
raise ValueError("scheduling policy requires ScheduleConfig")
|
|
58
|
+
return Policy(
|
|
59
|
+
config.policy_id,
|
|
60
|
+
config.version,
|
|
61
|
+
_ScheduleAdmission(config),
|
|
62
|
+
hooks=frozenset({Hook("tool")}),
|
|
63
|
+
actions=frozenset({"continue", "deny"}),
|
|
64
|
+
priority=50,
|
|
65
|
+
configuration=config.to_dict(),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ToolScheduler:
|
|
70
|
+
"""Execute once, drain in-flight work, and return results in submission order.
|
|
71
|
+
|
|
72
|
+
Deadlines/cancellation stop new admission; Python cannot forcibly terminate a
|
|
73
|
+
running thread. Already-started callbacks must return or cooperate. The method
|
|
74
|
+
drains them before returning or re-raising an interruption. Never retries.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, run, *, config, max_records=64, max_claimed_calls=4096):
|
|
78
|
+
if not isinstance(run, HarnessRun) or type(config) is not ScheduleConfig:
|
|
79
|
+
raise ValueError("typed run and scheduling configuration required")
|
|
80
|
+
expected = scheduling_policy(config)
|
|
81
|
+
if run.harness.config.mode != "disabled" and not any(
|
|
82
|
+
policy.config_hash == expected.config_hash
|
|
83
|
+
and type(policy.evaluate) is _ScheduleAdmission
|
|
84
|
+
for policy in run.harness.config.policies
|
|
85
|
+
):
|
|
86
|
+
raise ValueError("register matching scheduling_policy before binding")
|
|
87
|
+
if (
|
|
88
|
+
"sync" not in run.harness.capabilities.execution_kinds
|
|
89
|
+
or not {Hook("tool"), Hook("tool", "after")} <= run.harness.capabilities.hooks
|
|
90
|
+
):
|
|
91
|
+
raise SchedulingError("unsupported_lifecycle")
|
|
92
|
+
if (
|
|
93
|
+
type(max_records) is not int
|
|
94
|
+
or not 1 <= max_records <= 1000
|
|
95
|
+
or type(max_claimed_calls) is not int
|
|
96
|
+
or not 1 <= max_claimed_calls <= 100000
|
|
97
|
+
):
|
|
98
|
+
raise ValueError("scheduler evidence and call registries must be bounded")
|
|
99
|
+
self._run, self._config = run, config
|
|
100
|
+
self._max_records, self._max_claimed = max_records, max_claimed_calls
|
|
101
|
+
self._lock, self._records, self._claimed = RLock(), {}, set()
|
|
102
|
+
self._running = False
|
|
103
|
+
self._capture_failed = False
|
|
104
|
+
self._last_result = None
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def run(self):
|
|
108
|
+
return self._run
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def config(self):
|
|
112
|
+
return self._config
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def last_result(self):
|
|
116
|
+
"""Retain partial/completed outputs even when caller interruption propagates."""
|
|
117
|
+
with self._lock:
|
|
118
|
+
return self._last_result
|
|
119
|
+
|
|
120
|
+
def export_evidence(self):
|
|
121
|
+
with self._lock:
|
|
122
|
+
return {"schema_version": SCHEDULE_VERSION, "records": deepcopy(self._records)}
|
|
123
|
+
|
|
124
|
+
def execute(self, calls, *, timeout_s=None, cancel_event=None):
|
|
125
|
+
if timeout_s is not None and (
|
|
126
|
+
type(timeout_s) not in {int, float} or not 0 < timeout_s < float("inf")
|
|
127
|
+
):
|
|
128
|
+
raise ValueError("timeout_s must be finite and positive")
|
|
129
|
+
if cancel_event is not None and not isinstance(cancel_event, Event):
|
|
130
|
+
raise ValueError("cancel_event must be threading.Event")
|
|
131
|
+
mode = self.run.harness.config.mode
|
|
132
|
+
plan = plan_tools(calls, self.config, enforce=mode == "enforce")
|
|
133
|
+
by_id = {call.call_id: call for call in calls}
|
|
134
|
+
identity = "schedule_" + uuid4().hex
|
|
135
|
+
trace = current_trace() if mode != "disabled" else None
|
|
136
|
+
records = {
|
|
137
|
+
call.call_id: {
|
|
138
|
+
"status": "pending",
|
|
139
|
+
"harness_call_id": None,
|
|
140
|
+
"callback_invoked": False,
|
|
141
|
+
"callback_completed": False,
|
|
142
|
+
"started_at": None,
|
|
143
|
+
"ended_at": None,
|
|
144
|
+
"duration_ms": None,
|
|
145
|
+
"reason": None,
|
|
146
|
+
}
|
|
147
|
+
for call in calls
|
|
148
|
+
}
|
|
149
|
+
concurrency = plan["proposed_concurrency"] if mode == "enforce" else 1
|
|
150
|
+
receipt = {
|
|
151
|
+
"schedule_id": identity,
|
|
152
|
+
"schema_version": SCHEDULE_VERSION,
|
|
153
|
+
"policy_config_hash": scheduling_policy(self.config).config_hash,
|
|
154
|
+
"mode": mode,
|
|
155
|
+
"plan": plan,
|
|
156
|
+
"actual_concurrency_limit": concurrency,
|
|
157
|
+
"peak_running": 0,
|
|
158
|
+
"dispatch_order": [],
|
|
159
|
+
"completion_order": [],
|
|
160
|
+
"calls": records,
|
|
161
|
+
"status": "running",
|
|
162
|
+
"stop_reason": None,
|
|
163
|
+
"timeout_semantics": "stop new admission and drain already-started callbacks; no thread termination",
|
|
164
|
+
}
|
|
165
|
+
with self._lock, _EVIDENCE_LOCK:
|
|
166
|
+
if self._running or self._claimed.intersection(by_id):
|
|
167
|
+
raise SchedulingError("already_started")
|
|
168
|
+
if self._capture_failed:
|
|
169
|
+
raise SchedulingError("invalid_trace_evidence")
|
|
170
|
+
if (
|
|
171
|
+
len(self._records) >= self._max_records
|
|
172
|
+
or len(self._claimed) + len(calls) > self._max_claimed
|
|
173
|
+
):
|
|
174
|
+
raise SchedulingError("evidence_limit")
|
|
175
|
+
if trace is not None:
|
|
176
|
+
envelope = trace.metadata.setdefault(
|
|
177
|
+
SCHEDULE_KEY, {"schema_version": SCHEDULE_VERSION, "records": {}}
|
|
178
|
+
)
|
|
179
|
+
if (
|
|
180
|
+
not isinstance(envelope, dict)
|
|
181
|
+
or envelope.get("schema_version") != SCHEDULE_VERSION
|
|
182
|
+
or not isinstance(envelope.get("records"), dict)
|
|
183
|
+
or len(envelope["records"]) >= 10000
|
|
184
|
+
):
|
|
185
|
+
raise SchedulingError("invalid_trace_evidence")
|
|
186
|
+
envelope["records"][identity] = deepcopy(receipt)
|
|
187
|
+
self._running = True
|
|
188
|
+
self._claimed.update(by_id)
|
|
189
|
+
if mode != "disabled":
|
|
190
|
+
self._records[identity] = deepcopy(receipt)
|
|
191
|
+
stop = Event()
|
|
192
|
+
deadline = None if timeout_s is None else monotonic() + timeout_s
|
|
193
|
+
outcomes, finished, active, pending = {}, {}, {}, set(by_id)
|
|
194
|
+
running = 0
|
|
195
|
+
failure = None
|
|
196
|
+
executor = None
|
|
197
|
+
|
|
198
|
+
def call_one(call, prerequisites):
|
|
199
|
+
nonlocal running
|
|
200
|
+
record = records[call.call_id]
|
|
201
|
+
returned = []
|
|
202
|
+
context = ToolContext(
|
|
203
|
+
call.call_id, MappingProxyType(prerequisites), stop, cancel_event, deadline
|
|
204
|
+
)
|
|
205
|
+
if context.cancellation_requested:
|
|
206
|
+
record.update(status="not_started", reason="admission_stopped")
|
|
207
|
+
return ToolOutcome(call.call_id, "not_started")
|
|
208
|
+
branch = self.config.branch_id + ":" + call.call_id
|
|
209
|
+
|
|
210
|
+
def invoke():
|
|
211
|
+
nonlocal running
|
|
212
|
+
if context.cancellation_requested:
|
|
213
|
+
# No side effect begins after the cooperative stop is observed.
|
|
214
|
+
return _NOT_STARTED
|
|
215
|
+
with self._lock:
|
|
216
|
+
running += 1
|
|
217
|
+
receipt["peak_running"] = max(receipt["peak_running"], running)
|
|
218
|
+
receipt["dispatch_order"].append(call.call_id)
|
|
219
|
+
record.update(callback_invoked=True, status="running", started_at=utc_now_iso())
|
|
220
|
+
started = perf_counter()
|
|
221
|
+
try:
|
|
222
|
+
value = call.invoke(context)
|
|
223
|
+
returned.append(value)
|
|
224
|
+
record["callback_completed"] = True
|
|
225
|
+
return value
|
|
226
|
+
finally:
|
|
227
|
+
with self._lock:
|
|
228
|
+
running -= 1
|
|
229
|
+
record["ended_at"] = utc_now_iso()
|
|
230
|
+
record["duration_ms"] = (perf_counter() - started) * 1000
|
|
231
|
+
|
|
232
|
+
protected = self.run.wrap(
|
|
233
|
+
invoke,
|
|
234
|
+
boundary="tool",
|
|
235
|
+
branch_id=branch,
|
|
236
|
+
dispatch=call.dispatch,
|
|
237
|
+
usage_reader=(
|
|
238
|
+
lambda value: None if value is _NOT_STARTED else call.usage_reader(value)
|
|
239
|
+
)
|
|
240
|
+
if call.usage_reader is not None
|
|
241
|
+
else None,
|
|
242
|
+
error_usage_reader=call.error_usage_reader,
|
|
243
|
+
)
|
|
244
|
+
binding = {
|
|
245
|
+
"used": False,
|
|
246
|
+
"config": self.config,
|
|
247
|
+
"branch": branch,
|
|
248
|
+
"run_id": self.run.run_id,
|
|
249
|
+
"record": record,
|
|
250
|
+
"unknown": call.call_id in plan["unknown_safety"],
|
|
251
|
+
}
|
|
252
|
+
token = _ACTIVE.set(binding)
|
|
253
|
+
try:
|
|
254
|
+
value = protected()
|
|
255
|
+
status = "not_started" if value is _NOT_STARTED else "completed"
|
|
256
|
+
record["status"] = status
|
|
257
|
+
return ToolOutcome(call.call_id, status, None if value is _NOT_STARTED else value)
|
|
258
|
+
except BaseException as exc:
|
|
259
|
+
status = (
|
|
260
|
+
"cancelled"
|
|
261
|
+
if not isinstance(exc, Exception)
|
|
262
|
+
else "blocked"
|
|
263
|
+
if isinstance(exc, HarnessControlError)
|
|
264
|
+
else "failed"
|
|
265
|
+
)
|
|
266
|
+
record.update(
|
|
267
|
+
status=status,
|
|
268
|
+
reason="harness_control"
|
|
269
|
+
if isinstance(exc, HarnessControlError)
|
|
270
|
+
else "cancelled"
|
|
271
|
+
if status == "cancelled"
|
|
272
|
+
else "tool_error",
|
|
273
|
+
)
|
|
274
|
+
if returned:
|
|
275
|
+
record["status"] = "completed"
|
|
276
|
+
return ToolOutcome(call.call_id, "completed", returned[0], exc)
|
|
277
|
+
return ToolOutcome(call.call_id, status, error=exc)
|
|
278
|
+
finally:
|
|
279
|
+
_ACTIVE.reset(token)
|
|
280
|
+
|
|
281
|
+
def worker(call, prerequisites):
|
|
282
|
+
try:
|
|
283
|
+
result = call_one(call, prerequisites)
|
|
284
|
+
except BaseException as exc:
|
|
285
|
+
records[call.call_id].update(status="failed", reason="dispatch_setup_failed")
|
|
286
|
+
result = ToolOutcome(call.call_id, "failed", error=exc)
|
|
287
|
+
with self._lock:
|
|
288
|
+
finished[call.call_id] = result
|
|
289
|
+
return result
|
|
290
|
+
|
|
291
|
+
def accept(result):
|
|
292
|
+
nonlocal failure
|
|
293
|
+
if result.call_id in outcomes:
|
|
294
|
+
return
|
|
295
|
+
outcomes[result.call_id] = result
|
|
296
|
+
receipt["completion_order"].append(result.call_id)
|
|
297
|
+
if result.status != "completed" and self.config.on_error == "stop":
|
|
298
|
+
stop.set()
|
|
299
|
+
receipt["stop_reason"] = receipt["stop_reason"] or "tool_failure"
|
|
300
|
+
if isinstance(result.error, HarnessControlError):
|
|
301
|
+
stop.set()
|
|
302
|
+
receipt["stop_reason"] = receipt["stop_reason"] or "harness_control"
|
|
303
|
+
if result.error is not None and not isinstance(result.error, Exception):
|
|
304
|
+
failure = failure or result.error
|
|
305
|
+
stop.set()
|
|
306
|
+
receipt["stop_reason"] = "cancelled"
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
if concurrency > 1:
|
|
310
|
+
executor = ThreadPoolExecutor(max_workers=concurrency)
|
|
311
|
+
while pending or active:
|
|
312
|
+
if cancel_event is not None and cancel_event.is_set():
|
|
313
|
+
stop.set()
|
|
314
|
+
receipt["stop_reason"] = "cancelled"
|
|
315
|
+
if deadline is not None and monotonic() >= deadline:
|
|
316
|
+
stop.set()
|
|
317
|
+
receipt["stop_reason"] = receipt["stop_reason"] or "deadline"
|
|
318
|
+
if self.run.stopped:
|
|
319
|
+
stop.set()
|
|
320
|
+
receipt["stop_reason"] = receipt["stop_reason"] or "harness_stopped"
|
|
321
|
+
for call_id in plan["serial_order"]:
|
|
322
|
+
if call_id not in pending or stop.is_set() or len(active) >= concurrency:
|
|
323
|
+
continue
|
|
324
|
+
dependencies = plan["effective_dependencies"][call_id]
|
|
325
|
+
if not all(dependency in outcomes for dependency in dependencies):
|
|
326
|
+
continue
|
|
327
|
+
pending.remove(call_id)
|
|
328
|
+
if any(
|
|
329
|
+
outcomes[dependency].status != "completed" for dependency in dependencies
|
|
330
|
+
):
|
|
331
|
+
records[call_id].update(
|
|
332
|
+
status="dependency_failed", reason="prerequisite_failed"
|
|
333
|
+
)
|
|
334
|
+
accept(ToolOutcome(call_id, "dependency_failed"))
|
|
335
|
+
continue
|
|
336
|
+
call = by_id[call_id]
|
|
337
|
+
selected = call.depends_on if call.depends_on is not None else tuple(outcomes)
|
|
338
|
+
prerequisites = {key: outcomes[key].value for key in selected}
|
|
339
|
+
if executor is None:
|
|
340
|
+
accept(worker(call, prerequisites))
|
|
341
|
+
break
|
|
342
|
+
future = executor.submit(copy_context().run, worker, call, prerequisites)
|
|
343
|
+
active[future] = call_id
|
|
344
|
+
if stop.is_set():
|
|
345
|
+
for call_id in pending:
|
|
346
|
+
records[call_id].update(status="not_started", reason=receipt["stop_reason"])
|
|
347
|
+
outcomes[call_id] = ToolOutcome(call_id, "not_started")
|
|
348
|
+
pending.clear()
|
|
349
|
+
if active:
|
|
350
|
+
completed, _ = wait(active, timeout=0.02, return_when=FIRST_COMPLETED)
|
|
351
|
+
for future in completed:
|
|
352
|
+
active.pop(future)
|
|
353
|
+
accept(future.result())
|
|
354
|
+
receipt["status"] = (
|
|
355
|
+
"completed"
|
|
356
|
+
if all(result.status == "completed" for result in outcomes.values())
|
|
357
|
+
and receipt["stop_reason"] is None
|
|
358
|
+
else "partial"
|
|
359
|
+
)
|
|
360
|
+
except BaseException as exc:
|
|
361
|
+
failure = exc
|
|
362
|
+
stop.set()
|
|
363
|
+
receipt.update(
|
|
364
|
+
status="interrupted",
|
|
365
|
+
stop_reason="cancelled" if not isinstance(exc, Exception) else "scheduler_error",
|
|
366
|
+
)
|
|
367
|
+
finally:
|
|
368
|
+
if executor is not None:
|
|
369
|
+
executor.shutdown(wait=True)
|
|
370
|
+
for future, call_id in active.items():
|
|
371
|
+
if future.done():
|
|
372
|
+
accept(future.result())
|
|
373
|
+
for result in finished.values():
|
|
374
|
+
accept(result)
|
|
375
|
+
for call_id in by_id.keys() - outcomes.keys():
|
|
376
|
+
records[call_id].update(
|
|
377
|
+
status="not_started", reason=receipt["stop_reason"] or "interrupted"
|
|
378
|
+
)
|
|
379
|
+
outcomes[call_id] = ToolOutcome(call_id, "not_started")
|
|
380
|
+
with self._lock, _EVIDENCE_LOCK:
|
|
381
|
+
self._running = False
|
|
382
|
+
self._last_result = ScheduledResult(
|
|
383
|
+
identity,
|
|
384
|
+
tuple(outcomes[call.call_id] for call in calls),
|
|
385
|
+
receipt["stop_reason"],
|
|
386
|
+
)
|
|
387
|
+
capture_error = False
|
|
388
|
+
if trace is not None:
|
|
389
|
+
try:
|
|
390
|
+
current = trace.metadata[SCHEDULE_KEY]
|
|
391
|
+
if (
|
|
392
|
+
current is not envelope
|
|
393
|
+
or current.get("schema_version") != SCHEDULE_VERSION
|
|
394
|
+
or not isinstance(current["records"], dict)
|
|
395
|
+
):
|
|
396
|
+
raise ValueError()
|
|
397
|
+
current["records"][identity] = deepcopy(receipt)
|
|
398
|
+
except (KeyError, TypeError, ValueError):
|
|
399
|
+
capture_error = self._capture_failed = True
|
|
400
|
+
receipt["capture_error"] = "invalid_trace_evidence"
|
|
401
|
+
if mode != "disabled":
|
|
402
|
+
self._records[identity] = deepcopy(receipt)
|
|
403
|
+
if capture_error and failure is None:
|
|
404
|
+
raise SchedulingError("invalid_trace_evidence")
|
|
405
|
+
if failure is not None:
|
|
406
|
+
raise failure
|
|
407
|
+
return self._last_result
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
_NOT_STARTED = object()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Native source-trace verification across compatible numeric normalization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
|
|
8
|
+
from agentloop.interventions import canonical_json, trace_fingerprint
|
|
9
|
+
from agentloop.tracer import AgentTrace
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class TraceSource:
|
|
14
|
+
trace: AgentTrace
|
|
15
|
+
fingerprints: frozenset[str]
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_dict(cls, payload):
|
|
19
|
+
"""Validate before accepting either canonical source or normalized identity.
|
|
20
|
+
|
|
21
|
+
Ledger creation can precede export (integer duration fields) or follow a
|
|
22
|
+
store read (normalized floats/defaults). Preserve both existing identities;
|
|
23
|
+
do not alter historical ledger hashes or recompute reports/pricing.
|
|
24
|
+
"""
|
|
25
|
+
trace = AgentTrace.from_dict(payload)
|
|
26
|
+
return cls(
|
|
27
|
+
trace,
|
|
28
|
+
frozenset(
|
|
29
|
+
{sha256(canonical_json(payload).encode()).hexdigest(), trace_fingerprint(trace)}
|
|
30
|
+
),
|
|
31
|
+
)
|