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.
- agent_postmortem/__init__.py +9 -0
- agent_postmortem/argerrors.py +160 -0
- agent_postmortem/claims.py +43 -0
- agent_postmortem/cli.py +2278 -0
- agent_postmortem/commands.py +1543 -0
- agent_postmortem/config.py +435 -0
- agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000001.jsonl +5 -0
- agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000001.meta.json +8 -0
- agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000002.jsonl +2 -0
- agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000002.meta.json +8 -0
- agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000003.jsonl +5 -0
- agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000003.meta.json +8 -0
- agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000004.jsonl +2 -0
- agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/agent-ac200000000000004.meta.json +8 -0
- agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000/subagents/workflows/wf_00000000-d30/journal.jsonl +7 -0
- agent_postmortem/demo/-home-user-project/00000000-0000-4000-8000-000000000000.jsonl +4 -0
- agent_postmortem/detectors/__init__.py +98 -0
- agent_postmortem/detectors/attempt_failed.py +261 -0
- agent_postmortem/detectors/base.py +319 -0
- agent_postmortem/detectors/bash_backgrounded.py +72 -0
- agent_postmortem/detectors/bash_timeout.py +94 -0
- agent_postmortem/detectors/claim_contradiction.py +251 -0
- agent_postmortem/detectors/classifier_unavailable.py +73 -0
- agent_postmortem/detectors/codex_unavailable.py +102 -0
- agent_postmortem/detectors/codex_write_denied.py +100 -0
- agent_postmortem/detectors/git_cwd_mismatch.py +74 -0
- agent_postmortem/detectors/mislabeled_kill.py +164 -0
- agent_postmortem/detectors/missing_artifact.py +159 -0
- agent_postmortem/detectors/permission_denied.py +99 -0
- agent_postmortem/detectors/silent_retry.py +163 -0
- agent_postmortem/detectors/structured_output_failure.py +91 -0
- agent_postmortem/detectors/tool_json_error.py +77 -0
- agent_postmortem/detectors/wasted_time.py +125 -0
- agent_postmortem/detectors/workflow_failed.py +62 -0
- agent_postmortem/disclosure.py +931 -0
- agent_postmortem/hooks/model_allocation_guard.mjs +248 -0
- agent_postmortem/hooks_cli.py +1269 -0
- agent_postmortem/ledger.py +624 -0
- agent_postmortem/markers.py +505 -0
- agent_postmortem/models.py +815 -0
- agent_postmortem/observe.py +792 -0
- agent_postmortem/reader.py +4611 -0
- agent_postmortem/redact.py +1981 -0
- agent_postmortem/report.py +3010 -0
- agent_postmortem/rescue.py +2096 -0
- agent_postmortem/returns.py +499 -0
- agent_postmortem/rules.py +627 -0
- agent_postmortem/severity.py +275 -0
- agent_postmortem/state.py +1261 -0
- agent_postmortem/trend.py +391 -0
- agent_postmortem/watch.py +973 -0
- agent_postmortem-0.3.4.dist-info/METADATA +264 -0
- agent_postmortem-0.3.4.dist-info/RECORD +56 -0
- agent_postmortem-0.3.4.dist-info/WHEEL +4 -0
- agent_postmortem-0.3.4.dist-info/entry_points.txt +3 -0
- 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()
|