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.
Files changed (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. dashboard/value_view.py +105 -0
@@ -0,0 +1,554 @@
1
+ """Source-bound coordination observations; unknown boundaries stay unknown."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter, defaultdict
6
+ from hashlib import sha256
7
+ from pathlib import Path
8
+ from typing import Iterable
9
+
10
+ from agentloop.graph import ExecutionGraph
11
+ from agentloop.integrations.omnigent.evidence import read_observations
12
+ from agentloop.interoperability.artifacts import native_bytes, write_artifact
13
+ from agentloop.interoperability.contracts import ImportReceipt
14
+ from agentloop.interoperability.evidence import read_external
15
+ from agentloop.interoperability.validation import ImportValidationError
16
+ from agentloop.interventions import canonical_json
17
+ from agentloop.timing import event_interval_ms, timestamp_ms
18
+ from agentloop.tracer import AgentTrace
19
+
20
+
21
+ def _fail(reason: str) -> None:
22
+ raise ImportValidationError("invalid_coordination", "coordination", reason)
23
+
24
+
25
+ def _scope(receipt: dict) -> tuple:
26
+ identity = receipt["external_identity"]
27
+ return (
28
+ receipt["source"]["system"],
29
+ identity["job_id"],
30
+ identity["trial_id"],
31
+ identity["task_digest"] or receipt["source"]["artifact_sha256"],
32
+ )
33
+
34
+
35
+ def _trajectory_scope(receipt: dict) -> tuple:
36
+ identity = receipt["external_identity"]
37
+ return (
38
+ receipt["source"]["system"],
39
+ identity["job_id"],
40
+ identity["trial_id"],
41
+ identity["task_digest"],
42
+ )
43
+
44
+
45
+ def _overlap(intervals: list[tuple[float, float]]) -> float | None:
46
+ if not intervals:
47
+ return None
48
+ changes = Counter()
49
+ for start, end in intervals:
50
+ changes[start] += 1
51
+ changes[end] -= 1
52
+ active, overlap, previous = 0, 0.0, None
53
+ for moment, change in sorted(changes.items()):
54
+ if previous is not None and active >= 2:
55
+ overlap += moment - previous
56
+ active += change
57
+ previous = moment
58
+ return overlap
59
+
60
+
61
+ def _captured_arguments(value) -> bool:
62
+ if not isinstance(value, dict):
63
+ return False
64
+ pending = [value]
65
+ while pending:
66
+ node = pending.pop()
67
+ if isinstance(node, dict):
68
+ if node.get("capture") == "omitted":
69
+ return False
70
+ pending.extend(node.values())
71
+ elif isinstance(node, list):
72
+ pending.extend(node)
73
+ return True
74
+
75
+
76
+ def _aggregate(value) -> dict | None:
77
+ if not isinstance(value, dict):
78
+ return None
79
+ keys = {
80
+ "total_prompt_tokens",
81
+ "total_completion_tokens",
82
+ "total_cached_tokens",
83
+ "total_cost_usd",
84
+ "input_tokens",
85
+ "output_tokens",
86
+ "cached_input_tokens",
87
+ "cost_usd",
88
+ }
89
+ result = {
90
+ key: item
91
+ for key, item in value.items()
92
+ if key in keys and (item is None or type(item) in {int, float})
93
+ }
94
+ extra = value.get("extra")
95
+ usage_scope = extra.get("usage_scope") if isinstance(extra, dict) else None
96
+ if isinstance(usage_scope, str) and usage_scope in {
97
+ "includes_subagents",
98
+ "agent_and_subagents",
99
+ "agent_only",
100
+ "trajectory_only",
101
+ "inclusive",
102
+ "exclusive",
103
+ }:
104
+ result["usage_scope"] = usage_scope
105
+ return result
106
+
107
+
108
+ def _critical_path(trace: AgentTrace, receipt: dict) -> dict:
109
+ graph = ExecutionGraph.from_trace(trace)
110
+ nodes = {node.node_id: node for node in graph.nodes}
111
+ roots = [node for node in graph.nodes if node.parent_id is None]
112
+ complete = (
113
+ bool(nodes)
114
+ and len(nodes) == len(graph.nodes)
115
+ and len(roots) == 1
116
+ and receipt["source_metadata"].get("conflicting_segments", 0) == 0
117
+ and all(
118
+ event.metadata.get("timing_available") is True
119
+ and event.metadata.get("timing_provenance") == "external_reported"
120
+ for event in trace.events
121
+ )
122
+ and all(event_interval_ms(node) is not None for node in graph.nodes)
123
+ and all(node.parent_id is None or node.parent_id in nodes for node in graph.nodes)
124
+ and all(not event.metadata.get("unresolved_parent_span_id") for event in trace.events)
125
+ )
126
+ # Native graph traversal detects cycles without inventing sequence edges for
127
+ # qualified imports. Every path must reach the sole recorded root.
128
+ if complete:
129
+ visited, pending = set(), [roots[0].node_id]
130
+ children = defaultdict(list)
131
+ for node in graph.nodes:
132
+ if node.parent_id:
133
+ children[node.parent_id].append(node.node_id)
134
+ while pending:
135
+ identity = pending.pop()
136
+ if identity in visited:
137
+ complete = False
138
+ break
139
+ visited.add(identity)
140
+ pending.extend(children[identity])
141
+ complete = complete and len(visited) == len(nodes)
142
+ value = graph.critical_path().to_dict() if complete else None
143
+ return {
144
+ "critical_path_ms": value["duration_ms"] if value else None,
145
+ "node_ids": value["node_ids"] if value else [],
146
+ "coverage": "recorded_connected_span_tree" if value else "unavailable",
147
+ "scope": "this source trace's explicit span tree; not complete cross-process task causality",
148
+ }
149
+
150
+
151
+ def summarize_coordination(traces: Iterable[AgentTrace], receipts: Iterable[ImportReceipt]) -> dict:
152
+ """Join captured projections/receipts without merging trace or session IDs."""
153
+ traces, receipts = tuple(traces), tuple(receipts)
154
+ documents = [receipt.to_dict() for receipt in receipts]
155
+ if len({row["receipt_id"] for row in documents}) != len(documents):
156
+ _fail("duplicate source receipt identity")
157
+ trace_index = {trace.run_id: trace for trace in traces}
158
+ if len(trace_index) != len(traces):
159
+ _fail("duplicate native trace identity")
160
+ bindings, receipt_by_run = {}, {}
161
+ for receipt in documents:
162
+ for captured in receipt["traces"]:
163
+ identity = captured["run_id"]
164
+ if identity in bindings:
165
+ _fail("multiple receipts claim one native trace")
166
+ bindings[identity] = captured["trace_sha256"]
167
+ receipt_by_run[identity] = receipt
168
+ for trace in traces:
169
+ if sha256(native_bytes(trace)).hexdigest() != bindings.get(trace.run_id):
170
+ _fail("native projection differs from captured receipt")
171
+
172
+ actors, actor_index, trajectory_index, event_actors = [], {}, {}, {}
173
+ relationships, per_trace, raw_aggregates, models, tools = [], [], [], [], []
174
+ outcomes = {}
175
+ for row in documents:
176
+ if row["source"]["format"] == "harbor_trial":
177
+ key = (row["external_identity"]["job_id"], row["external_identity"]["trial_id"])
178
+ if key in outcomes:
179
+ _fail("ambiguous parent trial identity")
180
+ outcomes[key] = row
181
+ for trace in traces:
182
+ receipt = receipt_by_run[trace.run_id]
183
+ identity, source = receipt["external_identity"], receipt["source"]
184
+ if source["format"] == "harbor_trial":
185
+ # Agent-phase usage already includes trajectory/subagent usage.
186
+ raw_aggregates.append(
187
+ {
188
+ "receipt_id": receipt["receipt_id"],
189
+ "scope": "trial_agent_context",
190
+ "metrics": _aggregate(receipt["source_metadata"].get("usage")),
191
+ }
192
+ )
193
+ continue
194
+ external = read_external(trace)
195
+ if external is None:
196
+ _fail("coordination requires qualified source projections")
197
+ observed = read_observations(trace)
198
+ if observed:
199
+ relationships.extend(
200
+ {"run_id": trace.run_id, **row} for row in observed["relationships"]
201
+ )
202
+ per_trace.append({"run_id": trace.run_id, **_critical_path(trace, receipt)})
203
+ aggregate = receipt["source_metadata"].get("source_final_metrics")
204
+ if aggregate is not None:
205
+ raw_aggregates.append(
206
+ {
207
+ "receipt_id": receipt["receipt_id"],
208
+ "scope": "source_final_metrics_may_include_children",
209
+ "metrics": _aggregate(aggregate),
210
+ }
211
+ )
212
+ raw_aggregates.extend(
213
+ {
214
+ "receipt_id": receipt["receipt_id"],
215
+ "scope": "source_aggregate",
216
+ "metrics": _aggregate(item),
217
+ }
218
+ for item in receipt["source_metadata"].get("source_aggregates", [])
219
+ )
220
+
221
+ atif = source["format"] == "atif"
222
+ selected = (
223
+ [None] if atif else [event for event in trace.events if event.operation_kind == "agent"]
224
+ )
225
+ for event in selected:
226
+ actor_id = trace.run_id if event is None else f"{trace.run_id}/{event.event_id}"
227
+ aliases = event.metadata.get("source.omnigent", {}) if event else {}
228
+ interval = event_interval_ms(event) if event else None
229
+ if event and (
230
+ event.metadata.get("timing_provenance") != "external_reported"
231
+ or receipt["source_metadata"].get("conflicting_segments", 0)
232
+ ):
233
+ interval = None
234
+ if atif and external["runtime_ms"] is not None:
235
+ start, end = timestamp_ms(trace.started_at), timestamp_ms(trace.ended_at)
236
+ if start is not None and end is not None and end >= start:
237
+ interval = (start, end)
238
+ agent = receipt["source_metadata"].get("agent") or {}
239
+ trial = outcomes.get((identity["job_id"], identity["trial_id"]))
240
+ status = (
241
+ receipt["outcome"]["execution_status"]
242
+ if event is None
243
+ else event.metadata.get("source_execution_status", "unknown")
244
+ )
245
+ actor = {
246
+ "actor_id": actor_id,
247
+ "run_id": trace.run_id,
248
+ "event_id": event.event_id if event else None,
249
+ "source_receipt_id": receipt["receipt_id"],
250
+ "source_identity": identity,
251
+ "agent_name": aliases.get("agent_name") or agent.get("name"),
252
+ "declared_role": aliases.get("agent_role")
253
+ or (agent.get("extra") or {}).get("role"),
254
+ "agent_id": aliases.get("agent_id"),
255
+ "parent_agent_id": aliases.get("parent_agent_id"),
256
+ "session_id": aliases.get("session_id") or identity["session_id"],
257
+ "parent_session_id": aliases.get("parent_session_id"),
258
+ "execution_status": status,
259
+ "runtime_ms": interval[1] - interval[0] if interval else None,
260
+ "timing_basis": "external_reported_interval" if interval else "unknown",
261
+ "task_outcome": trial["outcome"] if trial else receipt["outcome"],
262
+ "outcome_scope": "parent trial; child contribution not separately verified"
263
+ if trial
264
+ else "source receipt; role labels do not establish correctness",
265
+ }
266
+ if not isinstance(actor["declared_role"], str):
267
+ actor["declared_role"] = None
268
+ actors.append(actor)
269
+ actor_index[actor_id] = (actor, interval)
270
+ if atif:
271
+ key = (*_trajectory_scope(receipt), identity["trajectory_id"])
272
+ if key in trajectory_index:
273
+ _fail("ambiguous trajectory document identity")
274
+ trajectory_index[key] = actor_id
275
+ else:
276
+ event_actors[(trace.run_id, event.event_id)] = actor_id
277
+ models.extend(
278
+ (trace, event, receipt) for event in trace.events if event.operation_kind == "model"
279
+ )
280
+ tools.extend(
281
+ (trace, event, receipt) for event in trace.events if event.operation_kind == "tool"
282
+ )
283
+
284
+ alias_index = defaultdict(list)
285
+ for actor in actors:
286
+ identity = actor["source_identity"]
287
+ receipt = receipt_by_run[actor["run_id"]]
288
+ namespace = (receipt["source"]["system"], identity["job_id"], identity["trial_id"])
289
+ for agent_id, session_id in {
290
+ (actor["agent_id"], actor["session_id"]),
291
+ (actor["agent_id"], None),
292
+ (None, actor["session_id"]),
293
+ }:
294
+ if agent_id is not None or session_id is not None:
295
+ alias_index[(*namespace, agent_id, session_id)].append(actor["actor_id"])
296
+ for actor in actors:
297
+ if actor["parent_agent_id"] is None and actor["parent_session_id"] is None:
298
+ continue
299
+ identity = actor["source_identity"]
300
+ receipt = receipt_by_run[actor["run_id"]]
301
+ matches = alias_index.get(
302
+ (
303
+ receipt["source"]["system"],
304
+ identity["job_id"],
305
+ identity["trial_id"],
306
+ actor["parent_agent_id"],
307
+ actor["parent_session_id"],
308
+ ),
309
+ [],
310
+ )
311
+ resolved = len(matches) == 1 and matches[0] != actor["actor_id"]
312
+ relationships.append(
313
+ {
314
+ "kind": "declared_parent_identity",
315
+ "source_actor_id": actor["actor_id"],
316
+ "target_actor_id": matches[0] if resolved else None,
317
+ "target_count": len(matches),
318
+ "resolved": resolved,
319
+ "causal_edge": False,
320
+ "basis": "external_reported_identity; no dispatch or dependency boundary",
321
+ }
322
+ )
323
+
324
+ handoffs = []
325
+ for trace in traces:
326
+ receipt = receipt_by_run[trace.run_id]
327
+ if receipt["source"]["format"] == "atif":
328
+ for relation in receipt["relationships"]:
329
+ if relation["kind"] != "delegation":
330
+ continue
331
+ target = (
332
+ trajectory_index.get((*_trajectory_scope(receipt), relation["target_id"]))
333
+ if relation["resolved"]
334
+ else None
335
+ )
336
+ handoffs.append(
337
+ {
338
+ "parent_actor_id": trace.run_id,
339
+ "child_actor_id": target,
340
+ "target_id": relation["target_id"],
341
+ "basis": "external_reported_trajectory_delegation",
342
+ "causal_edge": False,
343
+ }
344
+ )
345
+ else:
346
+ events = {event.event_id: event for event in trace.events}
347
+ ancestor_cache = {}
348
+
349
+ def parent_actor(event_id):
350
+ path, seen, current = [], set(), event_id
351
+ result = None
352
+ while current is not None and current not in seen:
353
+ if current in ancestor_cache:
354
+ result = ancestor_cache[current]
355
+ break
356
+ found = event_actors.get((trace.run_id, current))
357
+ if found:
358
+ result = found
359
+ break
360
+ seen.add(current)
361
+ path.append(current)
362
+ ancestor = events.get(current)
363
+ current = ancestor.parent_id if ancestor else None
364
+ for key in path:
365
+ ancestor_cache[key] = result
366
+ return result
367
+
368
+ for event in trace.events:
369
+ child = event_actors.get((trace.run_id, event.event_id))
370
+ if child and event.parent_id:
371
+ parent = events.get(event.parent_id)
372
+ if parent:
373
+ handoffs.append(
374
+ {
375
+ "parent_actor_id": parent_actor(parent.event_id),
376
+ "parent_event_id": parent.event_id,
377
+ "child_actor_id": child,
378
+ "target_id": event.event_id,
379
+ "basis": "explicit_source_span_parent",
380
+ "causal_edge": True,
381
+ }
382
+ )
383
+ else:
384
+ relationships.append(
385
+ {
386
+ "run_id": trace.run_id,
387
+ "kind": "missing_span_parent",
388
+ "source_event_id": event.event_id,
389
+ "resolved": False,
390
+ "causal_edge": False,
391
+ }
392
+ )
393
+
394
+ child_ids = {row["child_actor_id"] for row in handoffs if row["child_actor_id"]}
395
+ parents = {row["parent_actor_id"] for row in handoffs if row["parent_actor_id"]}
396
+ for row in handoffs:
397
+ actor, interval = actor_index.get(row["child_actor_id"], (None, None))
398
+ row.update(
399
+ child_runtime_ms=actor["runtime_ms"] if actor else None,
400
+ handoff_wait_ms=None,
401
+ wait_missing_reason="no recorded dispatch/blocking-join interval; child runtime is not caller wait",
402
+ )
403
+ leaves = [pair for key, pair in actor_index.items() if key not in parents]
404
+ timed = [interval for _, interval in leaves if interval is not None]
405
+ overlap = _overlap(timed)
406
+
407
+ model_parents = {
408
+ (trace.run_id, event.parent_id) for trace, event, _ in models if event.parent_id
409
+ }
410
+ leaf_models = [
411
+ (trace, event, receipt)
412
+ for trace, event, receipt in models
413
+ if (trace.run_id, event.event_id) not in model_parents
414
+ ]
415
+ known_usage = {}
416
+ for direction in ("input", "output"):
417
+ selected = [
418
+ getattr(event, direction + "_tokens")
419
+ for _, event, _ in leaf_models
420
+ if event.metadata.get(direction + "_tokens_available") is True
421
+ ]
422
+ known_usage["known_" + direction + "_tokens"] = sum(selected) if selected else None
423
+ known_usage[direction + "_tokens"] = (
424
+ sum(selected) if leaf_models and len(selected) == len(models) else None
425
+ )
426
+
427
+ cache_values = []
428
+ for _, event, receipt in leaf_models:
429
+ cache = (
430
+ event.metadata.get("source_metrics", {}).get("cached_tokens")
431
+ if receipt["source"]["format"] == "atif"
432
+ else event.metadata.get("cached_input_tokens")
433
+ )
434
+ if type(cache) is int:
435
+ cache_values.append(cache)
436
+ known_usage["known_cached_input_tokens"] = sum(cache_values) if cache_values else None
437
+ known_usage["cached_input_tokens"] = (
438
+ sum(cache_values) if leaf_models and len(cache_values) == len(models) else None
439
+ )
440
+
441
+ duplicate_groups = defaultdict(list)
442
+ supported_tools = 0
443
+ for trace, event, receipt in tools:
444
+ arguments = event.metadata.get("source_arguments")
445
+ if receipt["source"]["format"] == "atif" and _captured_arguments(arguments):
446
+ supported_tools += 1
447
+ digest = sha256(canonical_json(arguments).encode()).hexdigest()
448
+ duplicate_groups[(*_scope(receipt), event.name, digest)].append(
449
+ {"run_id": trace.run_id, "event_id": event.event_id}
450
+ )
451
+ duplicates = [
452
+ {
453
+ "tool_name": key[-2],
454
+ "arguments_sha256": key[-1],
455
+ "spans": spans,
456
+ "basis": "identical_captured_arguments_same_named_tool_in_source_scope",
457
+ "confidence": "medium",
458
+ "safe_reuse": "unestablished",
459
+ "estimated_savings_ms": None,
460
+ "estimated_savings_usd": None,
461
+ }
462
+ for key, spans in sorted(duplicate_groups.items(), key=lambda item: str(item[0]))
463
+ if len(spans) > 1
464
+ ]
465
+ known_duplicates = sum(len(row["spans"]) - 1 for row in duplicates)
466
+ reported_counts = [
467
+ read_external(trace)["reported_model_call_count"]
468
+ for trace in traces
469
+ if receipt_by_run[trace.run_id]["source"]["format"] != "harbor_trial"
470
+ ]
471
+ unknown_children = sum(row["child_actor_id"] is None for row in handoffs)
472
+ return {
473
+ "schema_version": "1.0",
474
+ "source_receipt_ids": sorted(row["receipt_id"] for row in documents),
475
+ "actors": actors,
476
+ "handoffs": handoffs,
477
+ "relationships": relationships,
478
+ "handoff_count": len(handoffs),
479
+ "observed_child_count": len(child_ids),
480
+ "unknown_child_count": unknown_children,
481
+ "unknown_parent_reference_count": sum(
482
+ row.get("resolved") is False
483
+ and row["kind"]
484
+ in {"missing_span_parent", "declared_parent_identity", "ended_parent_reference"}
485
+ for row in relationships
486
+ ),
487
+ "child_failure_count": sum(
488
+ actor_index[key][0]["execution_status"] in {"failed", "timed_out", "cancelled"}
489
+ for key in child_ids
490
+ ),
491
+ "child_status_unknown_count": sum(
492
+ actor_index[key][0]["execution_status"] == "unknown" for key in child_ids
493
+ ),
494
+ "handoff_wait_ms": None,
495
+ "handoff_wait_coverage": {"measured": 0, "declared": len(handoffs)},
496
+ "parallel_overlap_ms": overlap
497
+ if len(timed) == len(leaves) and not unknown_children
498
+ else None,
499
+ "known_parallel_overlap_ms": overlap,
500
+ "overlap_coverage": {
501
+ "timed_leaf_actors": len(timed),
502
+ "observed_leaf_actors": len(leaves),
503
+ "unknown_children": unknown_children,
504
+ },
505
+ "overlap_basis": "union of times with at least two recorded leaf-actor intervals; enclosing parent actors excluded; overlap does not prove independent work or synchronized source clocks",
506
+ "critical_path_ms": per_trace[0]["critical_path_ms"]
507
+ if len(per_trace) == 1 and not unknown_children and not relationships
508
+ else None,
509
+ "critical_path_by_trace": per_trace,
510
+ "model_call_count": sum(reported_counts)
511
+ if reported_counts and all(value is not None for value in reported_counts)
512
+ else None,
513
+ "model_call_count_basis": "reported multiplicity for recorded source blocks; complete executor coverage not established",
514
+ "observed_model_blocks": len(models),
515
+ "retry_count": None,
516
+ "usage": {
517
+ **known_usage,
518
+ "raw_inclusive_aggregates": raw_aggregates,
519
+ "aggregation_basis": "unique native source documents/spans, model leaves only; inclusive/root/trial aggregates retained separately and never added",
520
+ "coverage": "recorded_model_leaves_only",
521
+ "provider_billing_verified": False,
522
+ },
523
+ "possible_duplicate_work_count": known_duplicates
524
+ if tools and supported_tools == len(tools)
525
+ else None,
526
+ "known_possible_duplicate_work_count": known_duplicates,
527
+ "possible_duplicate_work": duplicates,
528
+ "duplicate_work_coverage": {
529
+ "captured_tool_inputs": supported_tools,
530
+ "observed_tools": len(tools),
531
+ },
532
+ "review_overhead_ms": None,
533
+ "review_quality_delta": None,
534
+ "review_missing_reason": "requires controlled reviewed-vs-unreviewed task pairs; reviewer labels do not establish a quality effect",
535
+ "limitations": [
536
+ "source hashes bind supplied bytes, not authenticity",
537
+ "missing child logs, timing, retries and contributions remain unknown",
538
+ "session IDs and span links correlate observations without creating causal dependencies",
539
+ "identical tool inputs can require independent verification or have changing state/side effects; investigate with the existing semantic redundancy and paired-intervention contracts",
540
+ ],
541
+ "synthetic": any(trace.metadata.get("synthetic") is True for trace in traces),
542
+ }
543
+
544
+
545
+ def write_coordination_report(traces, receipts, out: str | Path) -> Path:
546
+ from agentloop.html_report import coordination_to_html
547
+
548
+ report = summarize_coordination(traces, receipts)
549
+ root = Path(out)
550
+ root.mkdir(parents=True, exist_ok=True)
551
+ root = root.resolve()
552
+ write_artifact(root, "coordination.json", (canonical_json(report) + "\n").encode())
553
+ write_artifact(root, "coordination.html", coordination_to_html(report).encode())
554
+ return root / "coordination.json"