agent-postmortem 0.3.4__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 (56) hide show
  1. agent_postmortem/__init__.py +9 -0
  2. agent_postmortem/argerrors.py +160 -0
  3. agent_postmortem/claims.py +43 -0
  4. agent_postmortem/cli.py +2278 -0
  5. agent_postmortem/commands.py +1543 -0
  6. agent_postmortem/config.py +435 -0
  7. agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000001.jsonl +5 -0
  8. agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000001.meta.json +8 -0
  9. agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000002.jsonl +2 -0
  10. agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000002.meta.json +8 -0
  11. agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000003.jsonl +5 -0
  12. agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000003.meta.json +8 -0
  13. agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000004.jsonl +2 -0
  14. agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000004.meta.json +8 -0
  15. agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/journal.jsonl +7 -0
  16. agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000.jsonl +4 -0
  17. agent_postmortem/detectors/__init__.py +98 -0
  18. agent_postmortem/detectors/attempt_failed.py +261 -0
  19. agent_postmortem/detectors/base.py +319 -0
  20. agent_postmortem/detectors/bash_backgrounded.py +72 -0
  21. agent_postmortem/detectors/bash_timeout.py +94 -0
  22. agent_postmortem/detectors/claim_contradiction.py +251 -0
  23. agent_postmortem/detectors/classifier_unavailable.py +73 -0
  24. agent_postmortem/detectors/codex_unavailable.py +102 -0
  25. agent_postmortem/detectors/codex_write_denied.py +100 -0
  26. agent_postmortem/detectors/git_cwd_mismatch.py +74 -0
  27. agent_postmortem/detectors/mislabeled_kill.py +164 -0
  28. agent_postmortem/detectors/missing_artifact.py +159 -0
  29. agent_postmortem/detectors/permission_denied.py +99 -0
  30. agent_postmortem/detectors/silent_retry.py +163 -0
  31. agent_postmortem/detectors/structured_output_failure.py +91 -0
  32. agent_postmortem/detectors/tool_json_error.py +77 -0
  33. agent_postmortem/detectors/wasted_time.py +125 -0
  34. agent_postmortem/detectors/workflow_failed.py +62 -0
  35. agent_postmortem/disclosure.py +931 -0
  36. agent_postmortem/hooks/model_allocation_guard.mjs +248 -0
  37. agent_postmortem/hooks_cli.py +1269 -0
  38. agent_postmortem/ledger.py +624 -0
  39. agent_postmortem/markers.py +505 -0
  40. agent_postmortem/models.py +815 -0
  41. agent_postmortem/observe.py +792 -0
  42. agent_postmortem/reader.py +4611 -0
  43. agent_postmortem/redact.py +1981 -0
  44. agent_postmortem/report.py +3010 -0
  45. agent_postmortem/rescue.py +2096 -0
  46. agent_postmortem/returns.py +499 -0
  47. agent_postmortem/rules.py +627 -0
  48. agent_postmortem/severity.py +275 -0
  49. agent_postmortem/state.py +1261 -0
  50. agent_postmortem/trend.py +391 -0
  51. agent_postmortem/watch.py +973 -0
  52. agent_postmortem-0.3.4.dist-info/METADATA +264 -0
  53. agent_postmortem-0.3.4.dist-info/RECORD +56 -0
  54. agent_postmortem-0.3.4.dist-info/WHEEL +4 -0
  55. agent_postmortem-0.3.4.dist-info/entry_points.txt +3 -0
  56. agent_postmortem-0.3.4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,91 @@
1
+ """Detects a StructuredOutput call whose payload failed schema validation.
2
+
3
+ DESIGN.md 4.3, 4.13.4: severity comes from the `recovered` input (major unless
4
+ the attempt or a later attempt completed with a result); fires on every
5
+ anchored, not-in-payload 'structured_output_mismatch' MarkerHit (docs/DESIGN.md
6
+ 4.13.2: the
7
+ registry's own shape restricts this id to a tool_result paired to
8
+ StructuredOutput). metrics: count. A 'bytes' metric measuring the recovered
9
+ payload is not included: reader.py truncates every event's text to a
10
+ 400-char excerpt before any detector sees it, so there is no way to recover
11
+ the original payload size here, only the length of the (already bounded)
12
+ marker text, which is not the same thing. rule_id: R-SCALAR-SCHEMA. Also
13
+ skips a read_only_output event (docs/DESIGN.md 4.3's self-reference rule),
14
+ though a StructuredOutput call is never itself read-only in practice.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from .. import severity as severity_rules
20
+ from ..models import Event, Finding, Run
21
+ from .base import (
22
+ KINDS_BOTH,
23
+ Detector,
24
+ anchored_marker_hits,
25
+ event_for_marker_hit,
26
+ evidence_line,
27
+ make_finding_id,
28
+ single_agent_id,
29
+ tooling_attempts,
30
+ )
31
+
32
+ ID = "structured_output_failure"
33
+ SEVERITY = "major"
34
+ RULE_ID = "R-SCALAR-SCHEMA"
35
+ _REGISTRY_MARKER = "structured_output_mismatch"
36
+
37
+
38
+ class StructuredOutputFailure:
39
+ id = ID
40
+ severity_default = SEVERITY
41
+ kinds = KINDS_BOTH
42
+
43
+ def run(self, run: Run) -> list[Finding]:
44
+ hits: list[Event] = []
45
+ hit_agents: list[str | None] = []
46
+ levels: list[str] = []
47
+ for attempt in tooling_attempts(run):
48
+ attempt_hits = 0
49
+ for hit in anchored_marker_hits(attempt, _REGISTRY_MARKER):
50
+ event = event_for_marker_hit(attempt, hit)
51
+ if event is None or event.read_only_output:
52
+ continue
53
+ hits.append(event)
54
+ attempt_hits += 1
55
+ hit_agents.append(attempt.agent_id)
56
+ if attempt_hits:
57
+ # Per attempt: did a later attempt, or the attempt itself, complete
58
+ # with a result (4.13.4)? The finding takes the highest of them.
59
+ levels.append(severity_rules.resolve(
60
+ run, ID,
61
+ recovered=severity_rules.recovered(run, attempt, include_self=True),
62
+ ))
63
+ if not hits:
64
+ return []
65
+
66
+ # docs/DESIGN.md 4.3, 4.7: attributed to an agent only when every hit is in it.
67
+ agent_id = single_agent_id(hit_agents)
68
+ count = len(hits)
69
+ plural = "s" if count != 1 else ""
70
+ title = f"{count} StructuredOutput call{plural} failed schema validation"
71
+ evidence = [evidence_line(run, event) for event in hits]
72
+ metrics = {"count": count}
73
+
74
+ return [
75
+ Finding(
76
+ detector=ID,
77
+ severity=severity_rules.worst(levels),
78
+ title=title,
79
+ run_id=run.run_id,
80
+ agent_id=agent_id,
81
+ evidence=evidence,
82
+ metrics=metrics,
83
+ rule_id=RULE_ID,
84
+ finding_id=make_finding_id(
85
+ run.run_id, ID, agent_id, evidence[0] if evidence else None
86
+ ),
87
+ )
88
+ ]
89
+
90
+
91
+ DETECTOR: Detector = StructuredOutputFailure()
@@ -0,0 +1,77 @@
1
+ """Detects a tool call whose input failed the runtime's own JSON validation.
2
+
3
+ DESIGN.md 4.3: minor severity, fires on every anchored, not-in-payload
4
+ 'tool_input_invalid' MarkerHit (docs/DESIGN.md 4.13.2: the registry's own
5
+ shape already excludes a StructuredOutput call, which gets its own
6
+ 'structured_output_mismatch' id instead). This is a runtime defect an agent
7
+ cannot route around, so there is no rule_id; it goes into the bug report
8
+ instead. metrics: count, tools (the distinct tool names that hit it). Skips
9
+ a read_only_output event (docs/DESIGN.md 4.3's self-reference rule): a
10
+ Read/Grep/cat/sed/etc. call can only ever be quoting a marker string that
11
+ already exists on disk, never reporting a tool-input failure that actually
12
+ happened to this agent.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from .. import severity as severity_rules
18
+ from ..models import Event, Finding, Run
19
+ from .base import (
20
+ KINDS_BOTH,
21
+ Detector,
22
+ anchored_marker_hits,
23
+ event_for_marker_hit,
24
+ evidence_line,
25
+ make_finding_id,
26
+ single_agent_id,
27
+ tooling_attempts,
28
+ )
29
+
30
+ ID = "tool_json_error"
31
+ SEVERITY = "minor"
32
+ _REGISTRY_MARKER = "tool_input_invalid"
33
+
34
+
35
+ class ToolJsonError:
36
+ id = ID
37
+ severity_default = SEVERITY
38
+ kinds = KINDS_BOTH
39
+
40
+ def run(self, run: Run) -> list[Finding]:
41
+ hits: list[Event] = []
42
+ hit_agents: list[str] = []
43
+ for attempt in tooling_attempts(run):
44
+ for hit in anchored_marker_hits(attempt, _REGISTRY_MARKER):
45
+ event = event_for_marker_hit(attempt, hit)
46
+ if event is None or event.read_only_output:
47
+ continue
48
+ hits.append(event)
49
+ hit_agents.append(attempt.agent_id)
50
+ if not hits:
51
+ return []
52
+ agent_id = single_agent_id(hit_agents)
53
+
54
+ count = len(hits)
55
+ tools = sorted({event.tool_name for event in hits if event.tool_name})
56
+ plural = "s" if count != 1 else ""
57
+ title = f"{count} tool call{plural} failed input validation"
58
+ evidence = [evidence_line(run, event) for event in hits]
59
+
60
+ return [
61
+ Finding(
62
+ detector=ID,
63
+ severity=severity_rules.resolve(run, ID),
64
+ title=title,
65
+ run_id=run.run_id,
66
+ agent_id=agent_id,
67
+ evidence=evidence,
68
+ metrics={"count": count, "tools": tools},
69
+ rule_id=None,
70
+ finding_id=make_finding_id(
71
+ run.run_id, ID, agent_id, evidence[0] if evidence else None
72
+ ),
73
+ )
74
+ ]
75
+
76
+
77
+ DETECTOR: Detector = ToolJsonError()
@@ -0,0 +1,125 @@
1
+ """wasted_time: one info-level summary Finding per run of wall-clock burned.
2
+
3
+ Always fires, even on a clean run with nothing to report (wasted_seconds and
4
+ wasted_tokens are then zero), so the headline number this tool exists to
5
+ produce is always present. wasted_seconds and wasted_tokens sum the attempts that
6
+ returned no result (docs/DESIGN.md 4.3, FA5): every non-final attempt of each
7
+ logical agent (the ones a retry threw away) plus every final attempt whose
8
+ outcome is killed, failed, or superseded; run_seconds and attempt_seconds
9
+ cover the whole run.
10
+
11
+ A session run has no retries to observe (docs/DESIGN.md 4.7), so its
12
+ metrics are a different, smaller set: run_seconds (the same whole-run span),
13
+ child_seconds (the sum of every subagent's own wall-clock span, not just
14
+ non-final ones -- there is no "non-final" for a session), child_count
15
+ (how many subagents ran), and wasted_seconds (the same rule: a killed or
16
+ failed subagent's span). See docs/DESIGN.md section 4.3.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from .. import severity as severity_rules
22
+ from ..models import Attempt, Finding, Run
23
+ from .base import (
24
+ KINDS_BOTH,
25
+ Detector,
26
+ attempt_seconds,
27
+ format_duration,
28
+ make_finding_id,
29
+ run_wasted_attempts,
30
+ )
31
+
32
+
33
+ def _sum_usage(attempts: list[Attempt]) -> int:
34
+ total = 0
35
+ for attempt in attempts:
36
+ for value in attempt.usage.values():
37
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
38
+ total += value
39
+ return int(total)
40
+
41
+
42
+ def _non_final_attempts(run: Run) -> list[Attempt]:
43
+ non_final: list[Attempt] = []
44
+ for agent in run.agents:
45
+ attempts = sorted(agent.attempts, key=lambda a: a.ordinal)
46
+ non_final.extend(attempts[:-1])
47
+ return non_final
48
+
49
+
50
+ def _run_seconds(run: Run) -> float:
51
+ if run.started_at is None or run.ended_at is None:
52
+ return 0.0
53
+ return max((run.ended_at - run.started_at).total_seconds(), 0.0)
54
+
55
+
56
+ def _finding(run: Run, title: str, metrics: dict) -> Finding:
57
+ return Finding(
58
+ detector="wasted_time",
59
+ severity=severity_rules.resolve(run, "wasted_time"),
60
+ title=title,
61
+ run_id=run.run_id,
62
+ agent_id=None,
63
+ evidence=[],
64
+ metrics=metrics,
65
+ rule_id=None,
66
+ finding_id=make_finding_id(run.run_id, "wasted_time", None, None),
67
+ )
68
+
69
+
70
+ def _workflow_finding(run: Run) -> Finding:
71
+ non_final = _non_final_attempts(run)
72
+ wasted = run_wasted_attempts(run)
73
+ run_seconds = _run_seconds(run)
74
+ total_attempt_seconds = sum(attempt_seconds(attempt) for attempt in run.attempts)
75
+ wasted_seconds = sum(attempt_seconds(attempt) for attempt in wasted)
76
+ wasted_tokens = _sum_usage(wasted)
77
+ final_extra = len(wasted) - len(non_final)
78
+
79
+ title = (
80
+ f"run spent {format_duration(run_seconds)} wall-clock; "
81
+ f"{format_duration(wasted_seconds)} wasted across {len(non_final)} retried attempts"
82
+ )
83
+ if final_extra:
84
+ noun = "attempt" if final_extra == 1 else "attempts"
85
+ title += f" and {final_extra} final {noun} that ended without a result"
86
+ metrics = {
87
+ "run_seconds": run_seconds,
88
+ "attempt_seconds": total_attempt_seconds,
89
+ "wasted_seconds": wasted_seconds,
90
+ "wasted_tokens": wasted_tokens,
91
+ }
92
+ return _finding(run, title, metrics)
93
+
94
+
95
+ def _session_finding(run: Run) -> Finding:
96
+ run_seconds = _run_seconds(run)
97
+ child_seconds = sum(attempt_seconds(attempt) for attempt in run.attempts)
98
+ child_count = len(run.attempts)
99
+ wasted_seconds = sum(attempt_seconds(attempt) for attempt in run_wasted_attempts(run))
100
+
101
+ title = (
102
+ f"session ran {format_duration(run_seconds)}; {child_count} subagents "
103
+ f"used {format_duration(child_seconds)} of agent time"
104
+ )
105
+ metrics = {
106
+ "run_seconds": run_seconds,
107
+ "child_seconds": child_seconds,
108
+ "child_count": child_count,
109
+ "wasted_seconds": wasted_seconds,
110
+ }
111
+ return _finding(run, title, metrics)
112
+
113
+
114
+ class WastedTime:
115
+ id = "wasted_time"
116
+ severity_default = "info"
117
+ kinds = KINDS_BOTH
118
+
119
+ def run(self, run: Run) -> list[Finding]:
120
+ if run.kind == "session":
121
+ return [_session_finding(run)]
122
+ return [_workflow_finding(run)]
123
+
124
+
125
+ DETECTOR: Detector = WastedTime()
@@ -0,0 +1,62 @@
1
+ """The harness itself reported this workflow run failed.
2
+
3
+ docs/DESIGN.md 4.13.3/4.13.4 (v0.3 N2): fires once per run when Run.lifecycle
4
+ .terminal == 'failed' -- an accepted terminal notification, TaskStop, or
5
+ synchronous launch result whose value is 'failed' (4.13.3), independent of
6
+ whatever the run's own journal says (a journal can report every attempt
7
+ 'completed' while the harness that launched the workflow still saw it fail,
8
+ docs/DESIGN.md's field_failed_generation_completed_attempt case). major
9
+ severity (4.13.4), workflow only. Evidence is the parent transcript's own
10
+ accepted notification/TaskStop/launch-result line, `path:line`, never an
11
+ excerpt of it (the line itself is a structured handshake, not prose worth
12
+ quoting). No rule_id: section 9's rules library names none for this
13
+ detector.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from .. import reader
19
+ from .. import severity as severity_rules
20
+ from ..models import Finding, Run
21
+ from .base import KINDS_WORKFLOW_ONLY, Detector, make_finding_id, relative_path
22
+
23
+ ID = "workflow_failed"
24
+ SEVERITY = "major"
25
+
26
+
27
+ class WorkflowFailed:
28
+ id = ID
29
+ severity_default = SEVERITY
30
+ kinds = KINDS_WORKFLOW_ONLY
31
+
32
+ def run(self, run: Run) -> list[Finding]:
33
+ lifecycle = run.lifecycle
34
+ if lifecycle is None or lifecycle.terminal != "failed":
35
+ return []
36
+
37
+ parent_path = reader.parent_transcript_path(run)
38
+ line = reader.lifecycle_terminal_line(run)
39
+ evidence = (
40
+ [f"{relative_path(run, parent_path)}:{line}"]
41
+ if parent_path is not None and line is not None
42
+ else []
43
+ )
44
+
45
+ return [
46
+ Finding(
47
+ detector=ID,
48
+ severity=severity_rules.resolve(run, ID, terminal=lifecycle.terminal),
49
+ title="the harness reported this workflow failed",
50
+ run_id=run.run_id,
51
+ agent_id=None,
52
+ evidence=evidence,
53
+ metrics={},
54
+ rule_id=None,
55
+ finding_id=make_finding_id(
56
+ run.run_id, ID, None, evidence[0] if evidence else None
57
+ ),
58
+ )
59
+ ]
60
+
61
+
62
+ DETECTOR: Detector = WorkflowFailed()