agentprof 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 (54) hide show
  1. agentprof/__init__.py +7 -0
  2. agentprof/adapters/__init__.py +15 -0
  3. agentprof/adapters/base.py +79 -0
  4. agentprof/adapters/claude_code/__init__.py +3 -0
  5. agentprof/adapters/claude_code/adapter.py +133 -0
  6. agentprof/adapters/claude_code/discovery.py +79 -0
  7. agentprof/adapters/claude_code/prompts.py +44 -0
  8. agentprof/adapters/claude_code/tools.py +102 -0
  9. agentprof/adapters/claude_code/transcript.py +264 -0
  10. agentprof/adapters/claude_code/tree.py +492 -0
  11. agentprof/adapters/codex/__init__.py +3 -0
  12. agentprof/adapters/codex/adapter.py +164 -0
  13. agentprof/adapters/codex/discovery.py +117 -0
  14. agentprof/adapters/codex/prompts.py +16 -0
  15. agentprof/adapters/codex/rollout.py +328 -0
  16. agentprof/adapters/codex/tools.py +97 -0
  17. agentprof/adapters/codex/tree.py +311 -0
  18. agentprof/adapters/copilot_vscode/__init__.py +3 -0
  19. agentprof/adapters/copilot_vscode/adapter.py +166 -0
  20. agentprof/adapters/copilot_vscode/debuglog.py +136 -0
  21. agentprof/adapters/copilot_vscode/discovery.py +153 -0
  22. agentprof/adapters/copilot_vscode/session.py +281 -0
  23. agentprof/adapters/copilot_vscode/tools.py +127 -0
  24. agentprof/adapters/copilot_vscode/transcript.py +176 -0
  25. agentprof/adapters/copilot_vscode/tree.py +325 -0
  26. agentprof/adapters/execution.py +64 -0
  27. agentprof/adapters/timestamps.py +13 -0
  28. agentprof/adapters/turns.py +24 -0
  29. agentprof/analysis/__init__.py +3 -0
  30. agentprof/analysis/agent_summary.py +161 -0
  31. agentprof/analysis/call_context.py +101 -0
  32. agentprof/analysis/evidence_findings.py +151 -0
  33. agentprof/analysis/execution.py +39 -0
  34. agentprof/analysis/heuristics.py +306 -0
  35. agentprof/analysis/pipeline.py +31 -0
  36. agentprof/analysis/rollup.py +196 -0
  37. agentprof/cli.py +141 -0
  38. agentprof/model.py +341 -0
  39. agentprof/pricing.json +23 -0
  40. agentprof/pricing.py +120 -0
  41. agentprof/registry.py +304 -0
  42. agentprof/server/__init__.py +3 -0
  43. agentprof/server/app.py +399 -0
  44. agentprof/server/openapi.py +31 -0
  45. agentprof/server/pricing_schema.py +40 -0
  46. agentprof/server/schemas.py +579 -0
  47. agentprof/server/static/assets/index-C4HEqUVf.css +1 -0
  48. agentprof/server/static/assets/index-ZsPn1H3d.js +58 -0
  49. agentprof/server/static/index.html +14 -0
  50. agentprof-0.1.0.dist-info/METADATA +177 -0
  51. agentprof-0.1.0.dist-info/RECORD +54 -0
  52. agentprof-0.1.0.dist-info/WHEEL +4 -0
  53. agentprof-0.1.0.dist-info/entry_points.txt +8 -0
  54. agentprof-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,579 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 epicodic
3
+ """API response models and their mapping from the neutral model and registry rows."""
4
+
5
+ from typing import Any, Literal
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from agentprof.analysis.agent_summary import AgentGroup, agent_groups, agent_ids
10
+ from agentprof.model import (
11
+ CallEvent,
12
+ CostMetric,
13
+ Diagnostics,
14
+ EventCallLink,
15
+ ExecutionEvent,
16
+ Finding,
17
+ LlmCall,
18
+ Metric,
19
+ Node,
20
+ NodeKind,
21
+ Provenance,
22
+ Session,
23
+ Tokens,
24
+ ToolInfo,
25
+ )
26
+ from agentprof.registry import RowState, SessionRow
27
+
28
+ _ACTIVE_KINDS = (NodeKind.TURN, NodeKind.AGENT)
29
+
30
+
31
+ class MetricOut(BaseModel):
32
+ value: float | None
33
+ provenance: str
34
+
35
+
36
+ class CostOut(BaseModel):
37
+ value: float | None
38
+ unit: str | None
39
+ usd: float | None
40
+ provenance: str
41
+
42
+
43
+ class TokensOut(BaseModel):
44
+ input: MetricOut
45
+ output: MetricOut
46
+ cache_read: MetricOut
47
+ cache_write: MetricOut
48
+ cache_write_5m: MetricOut
49
+ cache_write_1h: MetricOut
50
+
51
+
52
+ class ToolOut(BaseModel):
53
+ native_id: str
54
+ category: str
55
+ path: str | None
56
+ paths: list[str]
57
+ line_range: tuple[int, int] | None
58
+ command: str | None
59
+ writes_file: bool
60
+ target_agent_id: str | None
61
+ is_resume: bool
62
+ linked_agent_node_id: str | None
63
+
64
+
65
+ class LlmCallOut(BaseModel):
66
+ call_id: str | None
67
+ start: MetricOut
68
+ duration: MetricOut
69
+ model: str | None
70
+ tokens: TokensOut
71
+ cost: CostOut
72
+ in_context: bool
73
+ cost_parts: dict[str, CostOut]
74
+ price_prefix: str | None
75
+ gap: MetricOut
76
+ gap_basis: str | None
77
+ preceding_events: list["CallEventOut"]
78
+ cold_reason: str
79
+ cold_rewrite: bool
80
+ source_request_id: str | None = None
81
+ source_order: int | None = None
82
+ source_stream_id: str | None = None
83
+ timing_basis: Literal["request_start", "assistant_message", "usage_report", "turn_start", "unknown"] = "unknown"
84
+
85
+
86
+ class CallEventOut(BaseModel):
87
+ kind: str
88
+ event_id: str
89
+ start: MetricOut
90
+ end: MetricOut
91
+ duration: MetricOut
92
+
93
+
94
+ class EventCallLinkOut(BaseModel):
95
+ source_request_id: str
96
+ relation: Literal["requested_by", "consumed_by", "next_observed_call"]
97
+ evidence: Literal["recorded", "observed_order"]
98
+ owner_id: str | None = None
99
+
100
+
101
+ class ExecutionEventOut(BaseModel):
102
+ kind: Literal[
103
+ "user_input",
104
+ "tool_start",
105
+ "tool_result",
106
+ "delegation",
107
+ "child_completion",
108
+ "completion",
109
+ "message",
110
+ "resume",
111
+ "compaction",
112
+ ]
113
+ event_id: str | None = None
114
+ subject_node_id: str | None = None
115
+ start: MetricOut
116
+ source_order: int | None = None
117
+ source_stream_id: str | None = None
118
+ success: bool | None = None
119
+ links: list[EventCallLinkOut]
120
+ execution_start: MetricOut
121
+ execution_end: MetricOut
122
+
123
+
124
+ class FindingOut(BaseModel):
125
+ heuristic_id: str
126
+ node_id: str
127
+ severity: str
128
+ message: str
129
+ evidence: dict[str, Any]
130
+ estimated_avoidable_cost: CostOut
131
+
132
+
133
+ class NodeOut(BaseModel):
134
+ node_id: str
135
+ agent_id: int | None
136
+ harness_agent_id: str | None
137
+ active: bool
138
+ active_descendant: bool
139
+ activity: str | None
140
+ kind: str
141
+ topic: str
142
+ model: str | None
143
+ success: bool | None
144
+ tool: ToolOut | None
145
+ start: MetricOut
146
+ end: MetricOut
147
+ duration: MetricOut
148
+ user_wait: MetricOut
149
+ llm_call_count: MetricOut
150
+ tool_call_count: MetricOut
151
+ tokens: TokensOut
152
+ tokens_total: TokensOut
153
+ cost_total: CostOut
154
+ cost_own: CostOut
155
+ context_peak: MetricOut
156
+ compactions: list[MetricOut]
157
+ resume_times: list[MetricOut]
158
+ llm_calls: list[LlmCallOut]
159
+ execution_events: list[ExecutionEventOut] = Field(default_factory=list)
160
+ findings: list[FindingOut]
161
+ children: list["NodeOut"]
162
+
163
+
164
+ class DiagnosticsOut(BaseModel):
165
+ malformed_lines: int
166
+ unknown_tool_ids: dict[str, int]
167
+ warnings: list[str]
168
+
169
+
170
+ class MalformedLineDetailOut(BaseModel):
171
+ source_path: str
172
+ line_number: int
173
+ excerpt: str
174
+ error_category: str
175
+
176
+
177
+ class DiagnosticsDetailOut(DiagnosticsOut):
178
+ malformed_line_details: list[MalformedLineDetailOut]
179
+
180
+
181
+ class SessionOut(BaseModel):
182
+ id: str
183
+ agent: str
184
+ title: str
185
+ workspace: str | None
186
+ mtime: float
187
+ sources: list[str]
188
+ diagnostics: DiagnosticsOut
189
+ root: NodeOut
190
+
191
+
192
+ class ProjectedNodeOut(NodeOut):
193
+ """A tree node in a bounded response; children may be omitted by depth."""
194
+
195
+ children: list["ProjectedNodeOut"]
196
+ children_omitted: bool | None = None
197
+
198
+
199
+ class ProjectedSessionOut(BaseModel):
200
+ """Optional top-level groups selected with the `fields` query parameter."""
201
+
202
+ id: str | None = None
203
+ agent: str | None = None
204
+ title: str | None = None
205
+ workspace: str | None = None
206
+ mtime: float | None = None
207
+ sources: list[str] | None = None
208
+ diagnostics: DiagnosticsOut | None = None
209
+ root: ProjectedNodeOut | None = None
210
+
211
+
212
+ class NodeDetailOut(BaseModel):
213
+ node_id: str
214
+ prompt: str
215
+ result: str
216
+ arguments: dict[str, Any]
217
+
218
+
219
+ class SummaryOut(BaseModel):
220
+ id: str
221
+ agent: str
222
+ title: str
223
+ workspace: str | None
224
+ state: str
225
+ mtime: float
226
+ start_ms: float | None
227
+ last_activity_ms: float | None
228
+ end_ms: float | None
229
+ file_size: int | None
230
+ cost_total: CostOut | None
231
+ error: str | None
232
+
233
+
234
+ class ApiEndpointOut(BaseModel):
235
+ method: str
236
+ path: str
237
+ parameters: list[str]
238
+ description: str
239
+
240
+
241
+ class ApiIndexOut(BaseModel):
242
+ endpoints: list[ApiEndpointOut]
243
+
244
+
245
+ class AgentSummaryRowOut(BaseModel):
246
+ agent_id: int
247
+ harness_agent_id: str | None
248
+ parent_agent_id: int | None
249
+ topic: str
250
+ models: list[str]
251
+ llm_calls: MetricOut
252
+ context_peak: MetricOut
253
+ tokens: TokensOut
254
+ own_cost: CostOut
255
+ subtree_cost: CostOut
256
+ start: MetricOut
257
+ end: MetricOut
258
+ cost_parts: dict[str, CostOut]
259
+ cache_ttl: str
260
+ cold_rewrites: MetricOut
261
+ cold_rewrite_cost: CostOut
262
+ longest_gap: MetricOut
263
+ findings_count: int
264
+ resume_count: int
265
+
266
+
267
+ class AgentSummaryOut(BaseModel):
268
+ schema_version: int
269
+ id: str
270
+ mtime: float
271
+ agents: list[AgentSummaryRowOut]
272
+
273
+
274
+ def metric_out(metric: Metric) -> MetricOut:
275
+ return MetricOut(value=metric.value, provenance=metric.provenance.value)
276
+
277
+
278
+ def cost_out(cost: CostMetric) -> CostOut:
279
+ return CostOut(value=cost.value, unit=cost.unit, usd=cost.usd, provenance=cost.provenance.value)
280
+
281
+
282
+ def tokens_out(tokens: Tokens) -> TokensOut:
283
+ return TokensOut(
284
+ input=metric_out(tokens.input),
285
+ output=metric_out(tokens.output),
286
+ cache_read=metric_out(tokens.cache_read),
287
+ cache_write=metric_out(tokens.cache_write),
288
+ cache_write_5m=metric_out(tokens.cache_write_5m),
289
+ cache_write_1h=metric_out(tokens.cache_write_1h),
290
+ )
291
+
292
+
293
+ def _tool_out(tool: ToolInfo) -> ToolOut:
294
+ return ToolOut(
295
+ native_id=tool.native_id,
296
+ category=tool.category.value,
297
+ path=tool.path,
298
+ paths=list(tool.paths),
299
+ line_range=tool.line_range,
300
+ command=tool.command,
301
+ writes_file=tool.writes_file,
302
+ target_agent_id=tool.target_agent_id,
303
+ is_resume=tool.is_resume,
304
+ linked_agent_node_id=tool.linked_agent_node_id,
305
+ )
306
+
307
+
308
+ def _llm_call_out(call: LlmCall) -> LlmCallOut:
309
+ return LlmCallOut(
310
+ call_id=call.call_id,
311
+ start=metric_out(call.start),
312
+ duration=metric_out(call.duration),
313
+ model=call.model,
314
+ tokens=tokens_out(call.tokens),
315
+ cost=cost_out(call.cost),
316
+ in_context=call.in_context,
317
+ cost_parts={kind: cost_out(part) for kind, part in call.cost_parts.items()},
318
+ price_prefix=call.price_prefix,
319
+ gap=metric_out(call.gap),
320
+ gap_basis=call.gap_basis,
321
+ preceding_events=[_call_event_out(event) for event in call.preceding_events],
322
+ cold_reason=call.cold_reason,
323
+ cold_rewrite=call.cold_rewrite,
324
+ source_request_id=call.source_request_id,
325
+ source_order=call.source_order,
326
+ source_stream_id=call.source_stream_id,
327
+ timing_basis=call.timing_basis,
328
+ )
329
+
330
+
331
+ def _event_call_link_out(link: EventCallLink) -> EventCallLinkOut:
332
+ return EventCallLinkOut(
333
+ source_request_id=link.source_request_id,
334
+ relation=link.relation,
335
+ evidence=link.evidence,
336
+ owner_id=link.owner_id,
337
+ )
338
+
339
+
340
+ def _execution_event_out(event: ExecutionEvent) -> ExecutionEventOut:
341
+ return ExecutionEventOut(
342
+ kind=event.kind,
343
+ event_id=event.event_id,
344
+ subject_node_id=event.subject_node_id,
345
+ start=metric_out(event.start),
346
+ source_order=event.source_order,
347
+ source_stream_id=event.source_stream_id,
348
+ success=event.success,
349
+ links=[_event_call_link_out(link) for link in event.links],
350
+ execution_start=metric_out(event.execution_start),
351
+ execution_end=metric_out(event.execution_end),
352
+ )
353
+
354
+
355
+ def _call_event_out(event: CallEvent) -> CallEventOut:
356
+ return CallEventOut(
357
+ kind=event.kind,
358
+ event_id=event.event_id,
359
+ start=metric_out(event.start),
360
+ end=metric_out(event.end),
361
+ duration=metric_out(event.duration),
362
+ )
363
+
364
+
365
+ def finding_out(finding: Finding) -> FindingOut:
366
+ return FindingOut(
367
+ heuristic_id=finding.heuristic_id,
368
+ node_id=finding.node_id,
369
+ severity=finding.severity,
370
+ message=finding.message,
371
+ evidence=finding.evidence,
372
+ estimated_avoidable_cost=cost_out(finding.estimated_avoidable_cost),
373
+ )
374
+
375
+
376
+ def activity_flags(root: Node, frontier: float | None) -> dict[int, tuple[bool, bool]]:
377
+ """Compute activity over the complete neutral tree for bounded projections."""
378
+ flags: dict[int, tuple[bool, bool]] = {}
379
+
380
+ def visit(node: Node) -> bool:
381
+ descendant = any([visit(child) for child in node.children])
382
+ active = (
383
+ node.kind is not NodeKind.SESSION
384
+ and node.start.value is not None
385
+ and node.end.provenance is not Provenance.EXACT
386
+ and (frontier is None or node.end.value is None or node.end.number() >= frontier)
387
+ )
388
+ flags[id(node)] = (active, descendant)
389
+ return (node.kind in _ACTIVE_KINDS and active) or descendant
390
+
391
+ visit(root)
392
+ return flags
393
+
394
+
395
+ def node_out(
396
+ node: Node,
397
+ agent_ids: dict[str, int] | None = None,
398
+ owning_agent_id: int = 1,
399
+ frontier: float | None = None,
400
+ max_depth: int | None = None,
401
+ activity_flags: dict[int, tuple[bool, bool]] | None = None,
402
+ ) -> NodeOut:
403
+ """The node and its subtree, without prompt, result and tool-argument texts.
404
+
405
+ `frontier` is the session's latest known timestamp (the root's rolled-up end); a node whose own end falls
406
+ short of it has already been overtaken by later activity elsewhere and is therefore not active, even when
407
+ that end is not `EXACT` (e.g. an adapter that never recorded one for a completed node).
408
+ """
409
+ agent_ids = agent_ids or {}
410
+ agent_id = (
411
+ agent_ids.get(node.agent_uuid or node.node_id, owning_agent_id)
412
+ if node.kind is NodeKind.AGENT
413
+ else owning_agent_id
414
+ )
415
+ children = (
416
+ [
417
+ node_out(
418
+ child,
419
+ agent_ids,
420
+ agent_id,
421
+ frontier,
422
+ None if max_depth is None else max_depth - 1,
423
+ activity_flags,
424
+ )
425
+ for child in node.children
426
+ ]
427
+ if max_depth != 0
428
+ else []
429
+ )
430
+ active = (
431
+ node.kind is not NodeKind.SESSION
432
+ and node.start.value is not None
433
+ and node.end.provenance is not Provenance.EXACT
434
+ and (frontier is None or node.end.value is None or node.end.number() >= frontier)
435
+ )
436
+ # A tool node is never itself an activity indicator, but a sub-agent nested under one still counts.
437
+ active_descendant = any(
438
+ (child.kind in _ACTIVE_KINDS and child.active) or child.active_descendant for child in children
439
+ )
440
+ if activity_flags is not None:
441
+ active, active_descendant = activity_flags[id(node)]
442
+ activity = (
443
+ "waiting"
444
+ if node.kind in _ACTIVE_KINDS and active_descendant
445
+ else "running"
446
+ if node.kind in _ACTIVE_KINDS and active
447
+ else None
448
+ )
449
+ return NodeOut(
450
+ node_id=node.node_id,
451
+ agent_id=agent_id,
452
+ harness_agent_id=node.agent_uuid if node.kind is NodeKind.AGENT else None,
453
+ active=active,
454
+ active_descendant=active_descendant,
455
+ activity=activity,
456
+ kind=node.kind.value,
457
+ topic=node.topic,
458
+ model=node.model,
459
+ success=node.success,
460
+ tool=_tool_out(node.tool) if node.tool is not None else None,
461
+ start=metric_out(node.start),
462
+ end=metric_out(node.end),
463
+ duration=metric_out(node.duration),
464
+ user_wait=metric_out(node.user_wait),
465
+ llm_call_count=metric_out(node.llm_call_count),
466
+ tool_call_count=metric_out(node.tool_call_count),
467
+ tokens=tokens_out(node.tokens),
468
+ tokens_total=tokens_out(node.tokens_total),
469
+ cost_total=cost_out(node.cost_total),
470
+ cost_own=cost_out(node.cost_own),
471
+ context_peak=metric_out(node.context_peak),
472
+ compactions=[metric_out(compaction) for compaction in node.compactions],
473
+ resume_times=[metric_out(point) for point in node.resume_times],
474
+ llm_calls=[_llm_call_out(call) for call in node.llm_calls],
475
+ execution_events=[_execution_event_out(event) for event in node.execution_events],
476
+ findings=[finding_out(finding) for finding in node.findings],
477
+ children=children,
478
+ )
479
+
480
+
481
+ def diagnostics_out(diagnostics: Diagnostics) -> DiagnosticsOut:
482
+ return DiagnosticsOut(
483
+ malformed_lines=diagnostics.malformed_lines,
484
+ unknown_tool_ids=diagnostics.unknown_tool_ids,
485
+ warnings=diagnostics.warnings,
486
+ )
487
+
488
+
489
+ def diagnostics_detail_out(diagnostics: Diagnostics) -> DiagnosticsDetailOut:
490
+ """Map diagnostic counts and bounded source-line details for the dedicated endpoint."""
491
+ return DiagnosticsDetailOut(
492
+ malformed_lines=diagnostics.malformed_lines,
493
+ unknown_tool_ids=diagnostics.unknown_tool_ids,
494
+ warnings=diagnostics.warnings,
495
+ malformed_line_details=[
496
+ MalformedLineDetailOut(
497
+ source_path=detail.source_path,
498
+ line_number=detail.line_number,
499
+ excerpt=detail.excerpt,
500
+ error_category=detail.error_category,
501
+ )
502
+ for detail in diagnostics.malformed_line_details
503
+ ],
504
+ )
505
+
506
+
507
+ def session_out(session: Session, mtime: float) -> SessionOut:
508
+ return SessionOut(
509
+ id=session.id,
510
+ agent=session.agent,
511
+ title=session.title,
512
+ workspace=session.workspace,
513
+ mtime=mtime,
514
+ sources=session.sources,
515
+ diagnostics=diagnostics_out(session.diagnostics),
516
+ root=node_out(session.root, agent_ids(session.root), frontier=session.root.end.value),
517
+ )
518
+
519
+
520
+ def agent_summary_row_out(group: AgentGroup) -> AgentSummaryRowOut:
521
+ return AgentSummaryRowOut(
522
+ agent_id=group.agent_id,
523
+ harness_agent_id=group.harness_agent_id,
524
+ parent_agent_id=group.parent_agent_id,
525
+ topic=group.topic,
526
+ models=group.models,
527
+ llm_calls=metric_out(group.llm_calls),
528
+ context_peak=metric_out(group.context_peak),
529
+ tokens=tokens_out(group.tokens),
530
+ own_cost=cost_out(group.own_cost),
531
+ subtree_cost=cost_out(group.subtree_cost),
532
+ start=metric_out(group.start),
533
+ end=metric_out(group.end),
534
+ cost_parts={kind: cost_out(part) for kind, part in group.cost_parts.items()},
535
+ cache_ttl=group.cache_ttl,
536
+ cold_rewrites=metric_out(group.cold_rewrites),
537
+ cold_rewrite_cost=cost_out(group.cold_rewrite_cost),
538
+ longest_gap=metric_out(group.longest_gap),
539
+ findings_count=group.findings_count,
540
+ resume_count=group.resume_count,
541
+ )
542
+
543
+
544
+ def agent_summary_out(session: Session, mtime: float) -> AgentSummaryOut:
545
+ return AgentSummaryOut(
546
+ schema_version=1,
547
+ id=session.id,
548
+ mtime=mtime,
549
+ agents=[agent_summary_row_out(group) for group in agent_groups(session.root)],
550
+ )
551
+
552
+
553
+ def node_detail_out(node: Node) -> NodeDetailOut:
554
+ return NodeDetailOut(
555
+ node_id=node.node_id,
556
+ prompt=node.prompt,
557
+ result=node.result,
558
+ arguments=node.tool.arguments if node.tool is not None else {},
559
+ )
560
+
561
+
562
+ def summary_out(row: SessionRow) -> SummaryOut:
563
+ """A list row: the native id as title until summarised, the summary (possibly outdated) afterwards."""
564
+ summary = row.summary
565
+ state = row.state
566
+ return SummaryOut(
567
+ id=row.ref.id,
568
+ agent=row.ref.agent,
569
+ title=summary.title if summary is not None else row.ref.native_id,
570
+ workspace=summary.workspace if summary is not None else None,
571
+ state=state.value,
572
+ mtime=row.ref.mtime,
573
+ start_ms=summary.start_ms if summary is not None else None,
574
+ last_activity_ms=summary.last_activity_ms if summary is not None else None,
575
+ end_ms=summary.end_ms if summary is not None else None,
576
+ file_size=summary.file_size if summary is not None else None,
577
+ cost_total=cost_out(summary.cost_total) if summary is not None else None,
578
+ error=row.error if state is RowState.ERROR else None,
579
+ )