chsum 3.0.2__tar.gz → 3.0.3__tar.gz
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.
- chsum-3.0.2/README.md → chsum-3.0.3/PKG-INFO +34 -3
- chsum-3.0.2/PKG-INFO → chsum-3.0.3/README.md +23 -14
- {chsum-3.0.2 → chsum-3.0.3}/chsum/core.py +229 -7
- {chsum-3.0.2 → chsum-3.0.3}/pyproject.toml +1 -1
- chsum-3.0.3/tests/test_handbacks.py +190 -0
- {chsum-3.0.2 → chsum-3.0.3}/.gitignore +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/CONTRIBUTING.md +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/LICENSE +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/chsum/__init__.py +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/chsum/__main__.py +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/chsum/checkpoints.py +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/chsum/sources/__init__.py +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/chsum/sources/claude.py +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/chsum/sources/codex.py +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/chsum.py +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/skills/chsum/SKILL.md +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/skills/chsum/references/checkpoints.md +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/skills/chsum/references/debugging.md +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/skills/chsum/references/naming.md +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/skills/chsum/references/noting.md +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/skills/chsum/references/recap.md +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/tests/harness.py +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/tests/test_checkpoints.py +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/tests/test_codex_source.py +0 -0
- {chsum-3.0.2 → chsum-3.0.3}/tests/test_views.py +0 -0
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: chsum
|
|
3
|
+
Version: 3.0.3
|
|
4
|
+
Summary: Work logs and reload-ready context from coding-agent conversations. Deterministic: no model, nothing invented.
|
|
5
|
+
Author: InDate
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: claude,claude-code,context,transcripts,work-log
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
1
12
|
# chsum
|
|
2
13
|
|
|
3
14
|
Work logs and reload-ready context from your coding-agent conversations —
|
|
@@ -452,8 +463,12 @@ chsum digest <ch_ref> --messages --stdout # print it rather than write it
|
|
|
452
463
|
chsum digest <ch_ref> --messages > out.md # or your own path
|
|
453
464
|
```
|
|
454
465
|
|
|
455
|
-
All three print in timestamp order across the transcript and its sidecars
|
|
456
|
-
|
|
466
|
+
All three print in timestamp order across the transcript and its sidecars.
|
|
467
|
+
Three kinds of record are removed or merged, each because it repeats a row
|
|
468
|
+
already printed or carries only harness text: a record holding nothing but a
|
|
469
|
+
`<system-reminder>`, the harness's retry after a malformed tool call, and the
|
|
470
|
+
task notification that points at a report delivered as a peer message — its
|
|
471
|
+
status and usage move onto that report's row. `--tools` and `--commands` print one
|
|
457
472
|
line per row — the call id, a `<session>:<line>` locator, local time, the tool
|
|
458
473
|
name and the first line of the call. A row runs long and lets the terminal
|
|
459
474
|
soft-wrap it rather than folding at a space: a command broken across lines can
|
|
@@ -465,6 +480,21 @@ cannot be used for. There is no flag or size limit behind that: the turn numbers
|
|
|
465
480
|
below already select the part of a conversation you want, and a second way to
|
|
466
481
|
ask for less would only be a worse one.
|
|
467
482
|
|
|
483
|
+
A message row is labelled by the sender its record names in `origin`, so a
|
|
484
|
+
report or an instruction another sender wrote never reads as yours:
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
- `410bfb1a:1157` 19:09:15 agent a3d9a4a28f0836307 returned · completed · 54k tokens · 7 tools · 56s
|
|
488
|
+
- `0d02851a:1026` 12:15:53 message from key-service-be
|
|
489
|
+
- `9cb6adfc/a29d93fd:216` 16:32:20 coordinator
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
A subagent's report prints once, from the parent's peer message where the
|
|
493
|
+
parent holds one and from the agent's own handback call where the report
|
|
494
|
+
arrived as an attachment. Only what you typed opens a turn in the parent;
|
|
495
|
+
scoped to an agent, its caller's and its coordinator's messages open the
|
|
496
|
+
agent's turns.
|
|
497
|
+
|
|
468
498
|
Between them it prints every call, each the same single clipped line `--tools`
|
|
469
499
|
gives it. A turn headed `2 tool calls · 8 commands` states how many ran and
|
|
470
500
|
names none of them, which leaves the work between two replies unreadable; the
|
|
@@ -486,7 +516,8 @@ prints one tool call and its captured output whole, which is where the text a
|
|
|
486
516
|
clipped call row actually lives.
|
|
487
517
|
|
|
488
518
|
`--agents` lists every subagent the session ran and each report it sent back,
|
|
489
|
-
numbered where an agent returned more than once
|
|
519
|
+
numbered where an agent returned more than once, each headed by the status and
|
|
520
|
+
usage of the stop that sent it. A report is the agent's own
|
|
490
521
|
document and runs to thousands of characters, so it is clipped with the cut
|
|
491
522
|
marked; the row it came from names where the whole text is.
|
|
492
523
|
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: chsum
|
|
3
|
-
Version: 3.0.2
|
|
4
|
-
Summary: Work logs and reload-ready context from coding-agent conversations. Deterministic: no model, nothing invented.
|
|
5
|
-
Author: InDate
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Keywords: claude,claude-code,context,transcripts,work-log
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
|
|
12
1
|
# chsum
|
|
13
2
|
|
|
14
3
|
Work logs and reload-ready context from your coding-agent conversations —
|
|
@@ -463,8 +452,12 @@ chsum digest <ch_ref> --messages --stdout # print it rather than write it
|
|
|
463
452
|
chsum digest <ch_ref> --messages > out.md # or your own path
|
|
464
453
|
```
|
|
465
454
|
|
|
466
|
-
All three print in timestamp order across the transcript and its sidecars
|
|
467
|
-
|
|
455
|
+
All three print in timestamp order across the transcript and its sidecars.
|
|
456
|
+
Three kinds of record are removed or merged, each because it repeats a row
|
|
457
|
+
already printed or carries only harness text: a record holding nothing but a
|
|
458
|
+
`<system-reminder>`, the harness's retry after a malformed tool call, and the
|
|
459
|
+
task notification that points at a report delivered as a peer message — its
|
|
460
|
+
status and usage move onto that report's row. `--tools` and `--commands` print one
|
|
468
461
|
line per row — the call id, a `<session>:<line>` locator, local time, the tool
|
|
469
462
|
name and the first line of the call. A row runs long and lets the terminal
|
|
470
463
|
soft-wrap it rather than folding at a space: a command broken across lines can
|
|
@@ -476,6 +469,21 @@ cannot be used for. There is no flag or size limit behind that: the turn numbers
|
|
|
476
469
|
below already select the part of a conversation you want, and a second way to
|
|
477
470
|
ask for less would only be a worse one.
|
|
478
471
|
|
|
472
|
+
A message row is labelled by the sender its record names in `origin`, so a
|
|
473
|
+
report or an instruction another sender wrote never reads as yours:
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
- `410bfb1a:1157` 19:09:15 agent a3d9a4a28f0836307 returned · completed · 54k tokens · 7 tools · 56s
|
|
477
|
+
- `0d02851a:1026` 12:15:53 message from key-service-be
|
|
478
|
+
- `9cb6adfc/a29d93fd:216` 16:32:20 coordinator
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
A subagent's report prints once, from the parent's peer message where the
|
|
482
|
+
parent holds one and from the agent's own handback call where the report
|
|
483
|
+
arrived as an attachment. Only what you typed opens a turn in the parent;
|
|
484
|
+
scoped to an agent, its caller's and its coordinator's messages open the
|
|
485
|
+
agent's turns.
|
|
486
|
+
|
|
479
487
|
Between them it prints every call, each the same single clipped line `--tools`
|
|
480
488
|
gives it. A turn headed `2 tool calls · 8 commands` states how many ran and
|
|
481
489
|
names none of them, which leaves the work between two replies unreadable; the
|
|
@@ -497,7 +505,8 @@ prints one tool call and its captured output whole, which is where the text a
|
|
|
497
505
|
clipped call row actually lives.
|
|
498
506
|
|
|
499
507
|
`--agents` lists every subagent the session ran and each report it sent back,
|
|
500
|
-
numbered where an agent returned more than once
|
|
508
|
+
numbered where an agent returned more than once, each headed by the status and
|
|
509
|
+
usage of the stop that sent it. A report is the agent's own
|
|
501
510
|
document and runs to thousands of characters, so it is clipped with the cut
|
|
502
511
|
marked; the row it came from names where the whole text is.
|
|
503
512
|
|
|
@@ -667,6 +667,9 @@ _NOISE_MARKERS = (
|
|
|
667
667
|
_NOISE_PREFIXES = (
|
|
668
668
|
"## Context Usage", # `/context` writes its report in as a user record
|
|
669
669
|
"[Your previous response had no visible output",
|
|
670
|
+
"The previous response failed to produce a valid tool call",
|
|
671
|
+
"Another Claude session sent a message",
|
|
672
|
+
"The coordinator sent a message",
|
|
670
673
|
)
|
|
671
674
|
|
|
672
675
|
|
|
@@ -679,6 +682,57 @@ def is_real_prompt(text: str) -> bool:
|
|
|
679
682
|
return not any(m in t for m in _NOISE_MARKERS)
|
|
680
683
|
|
|
681
684
|
|
|
685
|
+
def _origin(rec: dict) -> str:
|
|
686
|
+
"""Who put a user record into the conversation: `origin.kind`, or
|
|
687
|
+
`turnOrigin`, the later spelling of the same field. "" on a tool result and
|
|
688
|
+
on records older than both."""
|
|
689
|
+
origin = rec.get("origin")
|
|
690
|
+
if isinstance(origin, dict) and origin.get("kind"):
|
|
691
|
+
return str(origin["kind"])
|
|
692
|
+
return str(rec.get("turnOrigin") or "")
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _harness_sent(rec: dict) -> bool:
|
|
696
|
+
"""A user record another sender wrote: a peer session, a coordinator, a task
|
|
697
|
+
notification. Records without the field fall through to the text rules."""
|
|
698
|
+
kind = _origin(rec)
|
|
699
|
+
return bool(kind) and kind != "human"
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
_HANDBACK_LEAD = "The report follows:\n"
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _hand_back(rec: dict) -> tuple[str, str] | None:
|
|
706
|
+
"""(agent id, report) for a subagent's report delivered as a peer message.
|
|
707
|
+
The harness indents each report line by two spaces, which is removed here."""
|
|
708
|
+
origin = rec.get("origin")
|
|
709
|
+
if not (isinstance(origin, dict) and origin.get("kind") == "peer"
|
|
710
|
+
and origin.get("handback") and origin.get("from")):
|
|
711
|
+
return None
|
|
712
|
+
body = str(origin.get("body") or "")
|
|
713
|
+
_, lead, report = body.partition(_HANDBACK_LEAD)
|
|
714
|
+
report = report if lead else body
|
|
715
|
+
report = "\n".join(ln.removeprefix(" ") for ln in report.splitlines()).strip()
|
|
716
|
+
return str(origin["from"]), report
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
_COORDINATOR_LEAD = re.compile(r"^The coordinator sent a message[^:]*:\s*")
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _sent_message(rec: dict, text: str) -> tuple[str, str] | None:
|
|
723
|
+
"""(label, text) for a message another sender put in the conversation: a
|
|
724
|
+
peer session, by the name it sent under, or a coordinator. The harness's
|
|
725
|
+
lead-in is dropped, which leaves the message as its sender wrote it."""
|
|
726
|
+
origin = rec.get("origin")
|
|
727
|
+
kind = _origin(rec)
|
|
728
|
+
if kind == "peer" and isinstance(origin, dict) and origin.get("body"):
|
|
729
|
+
who = origin.get("name") or origin.get("from") or "peer"
|
|
730
|
+
return f"message from {who}", str(origin["body"]).strip()
|
|
731
|
+
if kind == "coordinator":
|
|
732
|
+
return "coordinator", _COORDINATOR_LEAD.sub("", text)
|
|
733
|
+
return None
|
|
734
|
+
|
|
735
|
+
|
|
682
736
|
def is_typed_prompt(text: str) -> bool:
|
|
683
737
|
"""`is_real_prompt`, minus `!` runs — something you did, not something you
|
|
684
738
|
said. Note paths stay on `is_real_prompt`, since notes are typed via `!`."""
|
|
@@ -1552,6 +1606,8 @@ def _is_typed_prompt(rec: dict, command_ids: frozenset[str] = frozenset()) -> bo
|
|
|
1552
1606
|
(`_command_prompt_ids`) — without it a slash command's body still reads as typed."""
|
|
1553
1607
|
if rec.get("isCompactSummary") or _tool_injected(rec, command_ids):
|
|
1554
1608
|
return False # Claude Code's own auto-summary, or a loaded body — not typed
|
|
1609
|
+
if _harness_sent(rec):
|
|
1610
|
+
return False
|
|
1555
1611
|
content = (rec.get("message") or {}).get("content")
|
|
1556
1612
|
if isinstance(content, str):
|
|
1557
1613
|
texts = [content]
|
|
@@ -2440,7 +2496,8 @@ def _drill_block(ref: str, path: pathlib.Path | None) -> list[str]:
|
|
|
2440
2496
|
sides = subagent_transcripts(path)
|
|
2441
2497
|
if sides:
|
|
2442
2498
|
out.append("\nThe subagents it ran, each in its own file")
|
|
2443
|
-
|
|
2499
|
+
parent = _split_agent_ref(ref)[0]
|
|
2500
|
+
out += [f"- `{parent}/{sc.stem.removeprefix('agent-')}` — `{sc}`" for sc in sides]
|
|
2444
2501
|
out += ["\nTo read one line of it, put the number in place of `<line>`",
|
|
2445
2502
|
f"- `sed -n '<line>p' {path} | jq`",
|
|
2446
2503
|
"\nTo read it in full, in order",
|
|
@@ -3032,6 +3089,18 @@ _ROW_KINDS = {
|
|
|
3032
3089
|
}
|
|
3033
3090
|
|
|
3034
3091
|
|
|
3092
|
+
# The tool a subagent returns its report through.
|
|
3093
|
+
_HANDBACK_TOOL = "SubagentHandback"
|
|
3094
|
+
_REMINDER_RE = re.compile(r"<system-reminder>.*?</system-reminder>", re.DOTALL)
|
|
3095
|
+
|
|
3096
|
+
|
|
3097
|
+
def _harness_reminder(rec: dict, text: str) -> bool:
|
|
3098
|
+
"""A user record the harness wrote whole: `isMeta`, and nothing left once its
|
|
3099
|
+
`<system-reminder>` blocks are removed. `isMeta` alone also marks an image
|
|
3100
|
+
paste, which is typed text and stays a row."""
|
|
3101
|
+
return bool(rec.get("isMeta")) and not _REMINDER_RE.sub("", text).strip()
|
|
3102
|
+
|
|
3103
|
+
|
|
3035
3104
|
def _short_id(tool_id: str) -> str:
|
|
3036
3105
|
"""Display form of a `tool_use` id. `toolu_` is on every one of them, so the
|
|
3037
3106
|
prefix carries no information and costs six columns on every row."""
|
|
@@ -3045,6 +3114,12 @@ def collect_rows(path: pathlib.Path, kinds: tuple[str, ...],
|
|
|
3045
3114
|
sections do all three, and these views are what they point at. `only_agent`
|
|
3046
3115
|
narrows to one sidecar, for an agent ref."""
|
|
3047
3116
|
out: list[_Row] = []
|
|
3117
|
+
# (agent, report) the parent holds as a peer message. The same report sits
|
|
3118
|
+
# in the agent's own handback call, and printing both repeats it. Keyed by
|
|
3119
|
+
# the report too: a return delivered as an attachment leaves no peer record,
|
|
3120
|
+
# and its handback call is then the only copy in any row.
|
|
3121
|
+
handed: set[tuple[str, str]] = set()
|
|
3122
|
+
stops: list[tuple[str, str, str]] = []
|
|
3048
3123
|
for src, agent in mark_sources(path):
|
|
3049
3124
|
if only_agent and agent != only_agent:
|
|
3050
3125
|
continue
|
|
@@ -3066,9 +3141,19 @@ def collect_rows(path: pathlib.Path, kinds: tuple[str, ...],
|
|
|
3066
3141
|
sidecars = _sidecar_ids(path)
|
|
3067
3142
|
for lineno, rec in parsed:
|
|
3068
3143
|
role = rec.get("type")
|
|
3144
|
+
ts = str(rec.get("timestamp") or "")
|
|
3145
|
+
# A notification delivered as an attachment leaves its text only in
|
|
3146
|
+
# the queue record, which is where most handback stops are found.
|
|
3147
|
+
body = rec.get("content")
|
|
3148
|
+
if (role == "queue-operation" and rec.get("operation") == "enqueue"
|
|
3149
|
+
and isinstance(body, str)
|
|
3150
|
+
and _is_notification(body)
|
|
3151
|
+
and _points_to_handback(body)
|
|
3152
|
+
and (aid := _agent_return(body, names, sidecars))):
|
|
3153
|
+
stops.append((aid, ts, _stop_note(body)))
|
|
3154
|
+
continue
|
|
3069
3155
|
if role not in ("user", "assistant"):
|
|
3070
3156
|
continue
|
|
3071
|
-
ts = str(rec.get("timestamp") or "")
|
|
3072
3157
|
content = (rec.get("message") or {}).get("content")
|
|
3073
3158
|
texts: list[str] = []
|
|
3074
3159
|
if isinstance(content, str):
|
|
@@ -3081,6 +3166,15 @@ def collect_rows(path: pathlib.Path, kinds: tuple[str, ...],
|
|
|
3081
3166
|
texts.append(part.get("text", ""))
|
|
3082
3167
|
elif part.get("type") == "tool_use":
|
|
3083
3168
|
name = str(part.get("name") or "?")
|
|
3169
|
+
report = (part.get("input") or {}).get("message")
|
|
3170
|
+
# The handback call's `message` is the agent's report and
|
|
3171
|
+
# the only copy of it in any user or assistant record: the
|
|
3172
|
+
# parent receives it as an attachment, which no row reads.
|
|
3173
|
+
if (name == _HANDBACK_TOOL and "message" in kinds
|
|
3174
|
+
and isinstance(report, str) and report.strip()):
|
|
3175
|
+
out.append(_Row(ts, agent, "message", "", report.strip(),
|
|
3176
|
+
f"agent {agent} returned", lineno, src))
|
|
3177
|
+
continue
|
|
3084
3178
|
cmd = (part.get("input") or {}).get("command")
|
|
3085
3179
|
bash = name == "Bash" and isinstance(cmd, str) and cmd.strip()
|
|
3086
3180
|
kind = "command" if bash else "tool"
|
|
@@ -3094,6 +3188,8 @@ def collect_rows(path: pathlib.Path, kinds: tuple[str, ...],
|
|
|
3094
3188
|
if role == "user" and _tool_injected(rec, command_ids):
|
|
3095
3189
|
continue
|
|
3096
3190
|
text = "\n".join(t for t in texts if t.strip()).strip()
|
|
3191
|
+
if role == "user" and _harness_reminder(rec, text):
|
|
3192
|
+
continue
|
|
3097
3193
|
# An answer to the question tool carries its text in a structured
|
|
3098
3194
|
# field and none in the body, so the view dropped it and a window
|
|
3099
3195
|
# numbered by turns would skip the turn a menu choice settled.
|
|
@@ -3112,25 +3208,49 @@ def collect_rows(path: pathlib.Path, kinds: tuple[str, ...],
|
|
|
3112
3208
|
# it, and dropping it would leave the run with no turn 1 at all.
|
|
3113
3209
|
starts = bool(role == "user" and (not agent or only_agent)
|
|
3114
3210
|
and not rec.get("isCompactSummary")
|
|
3211
|
+
and not _harness_sent(rec)
|
|
3115
3212
|
and (answered or is_typed_prompt(text)
|
|
3116
3213
|
or (only_agent and not started)))
|
|
3117
3214
|
returned = ""
|
|
3118
|
-
if role == "user" and text
|
|
3215
|
+
if role == "user" and _is_notification(text):
|
|
3119
3216
|
aid = _agent_return(text, names, sidecars)
|
|
3217
|
+
if aid and _points_to_handback(text):
|
|
3218
|
+
stops.append((aid, ts, _stop_note(text)))
|
|
3219
|
+
continue
|
|
3120
3220
|
got = _task_fields(text)
|
|
3121
3221
|
via = names.get(got["tool-use-id"], "")
|
|
3122
3222
|
returned = (f"agent {aid} returned" if aid
|
|
3123
3223
|
else f"{via or 'task'} {got['task-id'] or '?'} finished")
|
|
3124
3224
|
text = _report_text(text)
|
|
3125
3225
|
starts = False
|
|
3226
|
+
if role == "user" and (hand := _hand_back(rec)):
|
|
3227
|
+
handed.add(hand)
|
|
3228
|
+
returned = f"agent {hand[0]} returned"
|
|
3229
|
+
text = hand[1]
|
|
3230
|
+
starts = False
|
|
3231
|
+
elif role == "user" and (sent := _sent_message(rec, text)):
|
|
3232
|
+
returned, text = sent
|
|
3233
|
+
# Scoped to an agent, its caller's message is the agent's next
|
|
3234
|
+
# prompt, as the task it was spawned with is its first.
|
|
3235
|
+
starts = bool(only_agent)
|
|
3236
|
+
elif role == "user" and text.startswith(_NOISE_PREFIXES):
|
|
3237
|
+
continue
|
|
3126
3238
|
if text:
|
|
3127
3239
|
started = started or starts
|
|
3128
3240
|
out.append(_Row(ts, agent, "message", "", text,
|
|
3129
3241
|
returned or role, lineno, src, starts,
|
|
3130
3242
|
asked=tur if answered else None))
|
|
3243
|
+
out = [r for r in out if not ((r.agent, r.text) in handed and r.kind == "message"
|
|
3244
|
+
and r.label == f"agent {r.agent} returned")]
|
|
3131
3245
|
# A parent's line numbers and a sidecar's don't order against each other;
|
|
3132
3246
|
# only a clock does. Same reason `mark_sources`' readers sort by timestamp.
|
|
3133
3247
|
out.sort(key=lambda r: r.when)
|
|
3248
|
+
out = _fold_stops(
|
|
3249
|
+
out, stops,
|
|
3250
|
+
lambda r: (r.label.removeprefix("agent ").removesuffix(" returned")
|
|
3251
|
+
if r.kind == "message" and r.label.endswith(" returned") else ""),
|
|
3252
|
+
lambda r: r.when,
|
|
3253
|
+
lambda r, note: dataclasses.replace(r, label=f"{r.label} · {note}"))
|
|
3134
3254
|
TRACE.step("collect_rows", kinds=",".join(kinds), rows=len(out),
|
|
3135
3255
|
agent=only_agent or "(all)",
|
|
3136
3256
|
span=f"{out[0].when}→{out[-1].when}" if out else "—")
|
|
@@ -3189,6 +3309,17 @@ def _tool_names(parsed) -> dict[str, str]:
|
|
|
3189
3309
|
return names
|
|
3190
3310
|
|
|
3191
3311
|
|
|
3312
|
+
# How a notification record opens: the bare tag, or the tag behind the harness's
|
|
3313
|
+
# `[SYSTEM NOTIFICATION - NOT USER INPUT]` preamble. Anchored at the start
|
|
3314
|
+
# either way, since a message quoting the tag mid-text is not a notification.
|
|
3315
|
+
_NOTIFICATION_LEADS = ("<task-notification>", "[SYSTEM NOTIFICATION")
|
|
3316
|
+
|
|
3317
|
+
|
|
3318
|
+
def _is_notification(text: str) -> bool:
|
|
3319
|
+
return (text.lstrip().startswith(_NOTIFICATION_LEADS)
|
|
3320
|
+
and "<task-notification>" in text)
|
|
3321
|
+
|
|
3322
|
+
|
|
3192
3323
|
def _task_fields(text: str) -> dict[str, str]:
|
|
3193
3324
|
return {f: (m.group(1).strip() if (m := r.search(text)) else "")
|
|
3194
3325
|
for f, r in _TASK_FIELD_RE.items()}
|
|
@@ -3227,15 +3358,45 @@ class _AgentReport:
|
|
|
3227
3358
|
status: str
|
|
3228
3359
|
summary: str
|
|
3229
3360
|
result: str
|
|
3361
|
+
# The line is in the agent's own transcript, not the parent's: the report
|
|
3362
|
+
# reached the parent only as an attachment, and its handback call is the
|
|
3363
|
+
# one record holding it.
|
|
3364
|
+
in_sidecar: bool = False
|
|
3365
|
+
|
|
3366
|
+
|
|
3367
|
+
def _handback_reports(path: pathlib.Path) -> list[_AgentReport]:
|
|
3368
|
+
"""Every report an agent returned through its handback call, read from its
|
|
3369
|
+
own transcript."""
|
|
3370
|
+
out: list[_AgentReport] = []
|
|
3371
|
+
for side in subagent_transcripts(path):
|
|
3372
|
+
agent = side.stem.removeprefix("agent-")
|
|
3373
|
+
for lineno, raw in enumerate(side.read_text(errors="replace").splitlines(), 1):
|
|
3374
|
+
try:
|
|
3375
|
+
rec = json.loads(raw)
|
|
3376
|
+
except json.JSONDecodeError:
|
|
3377
|
+
continue
|
|
3378
|
+
content = (rec.get("message") or {}).get("content") if isinstance(rec, dict) else None
|
|
3379
|
+
for part in content if isinstance(content, list) else []:
|
|
3380
|
+
if not (isinstance(part, dict) and part.get("type") == "tool_use"
|
|
3381
|
+
and part.get("name") == _HANDBACK_TOOL):
|
|
3382
|
+
continue
|
|
3383
|
+
report = (part.get("input") or {}).get("message")
|
|
3384
|
+
if isinstance(report, str) and report.strip():
|
|
3385
|
+
out.append(_AgentReport(agent, str(rec.get("timestamp") or ""),
|
|
3386
|
+
lineno, "", "", report.strip(),
|
|
3387
|
+
in_sidecar=True))
|
|
3388
|
+
return out
|
|
3230
3389
|
|
|
3231
3390
|
|
|
3232
3391
|
def _agent_reports(path: pathlib.Path) -> list[_AgentReport]:
|
|
3233
3392
|
"""Every subagent return recorded in a parent transcript, in order. The
|
|
3234
|
-
`<task-notification>` record
|
|
3393
|
+
report lands in a `<task-notification>` record, or in a peer message where
|
|
3394
|
+
the agent returned it through its handback call: an async agent's
|
|
3235
3395
|
`tool_result` carries launch metadata, not the work."""
|
|
3236
3396
|
out: list[_AgentReport] = []
|
|
3237
3397
|
names: dict[str, str] = {}
|
|
3238
3398
|
sidecars = _sidecar_ids(path)
|
|
3399
|
+
stops: list[tuple[str, str, str]] = []
|
|
3239
3400
|
for lineno, raw in enumerate(path.read_text(errors="replace").splitlines(), 1):
|
|
3240
3401
|
try:
|
|
3241
3402
|
rec = json.loads(raw)
|
|
@@ -3244,6 +3405,10 @@ def _agent_reports(path: pathlib.Path) -> list[_AgentReport]:
|
|
|
3244
3405
|
if not isinstance(rec, dict):
|
|
3245
3406
|
continue
|
|
3246
3407
|
names.update(_tool_names([rec]))
|
|
3408
|
+
if rec.get("type") == "user" and (hand := _hand_back(rec)):
|
|
3409
|
+
out.append((True, _AgentReport(hand[0], str(rec.get("timestamp") or ""),
|
|
3410
|
+
lineno, "", "", hand[1])))
|
|
3411
|
+
continue
|
|
3247
3412
|
content = (rec.get("message") or {}).get("content")
|
|
3248
3413
|
texts = ([content] if isinstance(content, str)
|
|
3249
3414
|
else [p.get("text", "") for p in content
|
|
@@ -3263,26 +3428,80 @@ def _agent_reports(path: pathlib.Path) -> list[_AgentReport]:
|
|
|
3263
3428
|
# notifications reads back as one otherwise — a 52,293-char paste
|
|
3264
3429
|
# and an assistant message on the mechanism both matched a substring
|
|
3265
3430
|
# test, and every one of them collapsed into a single nameless agent.
|
|
3266
|
-
if not text
|
|
3431
|
+
if not _is_notification(text):
|
|
3267
3432
|
continue
|
|
3268
3433
|
if not _agent_return(text, names, sidecars):
|
|
3269
3434
|
continue
|
|
3270
3435
|
got = _task_fields(text)
|
|
3436
|
+
if _points_to_handback(text):
|
|
3437
|
+
stops.append((got["task-id"], str(rec.get("timestamp") or ""),
|
|
3438
|
+
_stop_note(text)))
|
|
3439
|
+
continue
|
|
3271
3440
|
out.append((rec.get("type") == "user",
|
|
3272
3441
|
_AgentReport(got["task-id"], str(rec.get("timestamp") or ""),
|
|
3273
3442
|
lineno, got["status"], got["summary"],
|
|
3274
3443
|
got["result"])))
|
|
3444
|
+
# Undelivered, so a peer message holding the same report wins the key below.
|
|
3445
|
+
out += [(False, r) for r in _handback_reports(path)]
|
|
3275
3446
|
kept: dict[tuple, _AgentReport] = {}
|
|
3276
3447
|
for delivered, rep in out:
|
|
3277
3448
|
key = (rep.agent, rep.status, rep.result)
|
|
3278
3449
|
if delivered or key not in kept:
|
|
3279
3450
|
kept[key] = rep
|
|
3280
|
-
|
|
3451
|
+
# By time: a sidecar's line numbers don't order against the parent's.
|
|
3452
|
+
reports = sorted(kept.values(), key=lambda r: (r.when, r.line))
|
|
3453
|
+
# A notification enqueued and then delivered is the same stop twice.
|
|
3454
|
+
stops = list(dict.fromkeys(stops))
|
|
3455
|
+
reports = _fold_stops(
|
|
3456
|
+
reports, [s for s in stops if s[1]], lambda r: r.agent, lambda r: r.when,
|
|
3457
|
+
lambda r, note: dataclasses.replace(r, status=note) if not r.status else r)
|
|
3281
3458
|
TRACE.step("_agent_reports", reports=len(reports), records=len(out),
|
|
3282
3459
|
agents=len({r.agent for r in reports}))
|
|
3283
3460
|
return reports
|
|
3284
3461
|
|
|
3285
3462
|
|
|
3463
|
+
# The `<result>` of a notification sent after a handback call. The report sits
|
|
3464
|
+
# in the peer message before it, so this notification adds only the stop's
|
|
3465
|
+
# status and usage, which are moved onto that report's row.
|
|
3466
|
+
_HANDBACK_POINTER = "This agent's report was delivered to you as a message from"
|
|
3467
|
+
_USAGE_RE = {f: re.compile(rf"<{f}>(\d+)</{f}>")
|
|
3468
|
+
for f in ("subagent_tokens", "tool_uses", "duration_ms")}
|
|
3469
|
+
|
|
3470
|
+
|
|
3471
|
+
def _stop_note(text: str) -> str:
|
|
3472
|
+
"""`completed · 54k tokens · 7 tools · 57s` out of a `<task-notification>`,
|
|
3473
|
+
each part present only where the notification carries it."""
|
|
3474
|
+
got = {f: int(m.group(1)) for f, r in _USAGE_RE.items() if (m := r.search(text))}
|
|
3475
|
+
parts = [_task_fields(text)["status"]]
|
|
3476
|
+
if "subagent_tokens" in got:
|
|
3477
|
+
parts.append(f"{got['subagent_tokens'] / 1000:.0f}k tokens")
|
|
3478
|
+
if "tool_uses" in got:
|
|
3479
|
+
parts.append(_plural(got["tool_uses"], "tool"))
|
|
3480
|
+
if "duration_ms" in got:
|
|
3481
|
+
parts.append(_fmt_secs(got["duration_ms"] // 1000))
|
|
3482
|
+
return " · ".join(p for p in parts if p)
|
|
3483
|
+
|
|
3484
|
+
|
|
3485
|
+
def _points_to_handback(text: str) -> bool:
|
|
3486
|
+
return _task_fields(text)["result"].startswith(_HANDBACK_POINTER)
|
|
3487
|
+
|
|
3488
|
+
|
|
3489
|
+
def _fold_stops(rows: list, stops: list[tuple[str, str, str]], agent_of, when_of,
|
|
3490
|
+
note) -> list:
|
|
3491
|
+
"""Each stop — (agent, time, note) of a notification that points at a
|
|
3492
|
+
handback — written onto the latest report of that agent at or before its
|
|
3493
|
+
time, through `note(row, text)`. A stop with no such report stays out:
|
|
3494
|
+
the notification carried nothing but the pointer and its usage."""
|
|
3495
|
+
rows = list(rows)
|
|
3496
|
+
for agent, when, text in stops:
|
|
3497
|
+
at = max((i for i, r in enumerate(rows)
|
|
3498
|
+
if agent_of(r) == agent and when_of(r) <= when), default=None,
|
|
3499
|
+
key=lambda i: when_of(rows[i]))
|
|
3500
|
+
if at is not None:
|
|
3501
|
+
rows[at] = note(rows[at], text)
|
|
3502
|
+
return rows
|
|
3503
|
+
|
|
3504
|
+
|
|
3286
3505
|
def _report_text(text: str) -> str:
|
|
3287
3506
|
"""The report out of a `<task-notification>`, clipped. Falls back to the
|
|
3288
3507
|
one-line summary where the notification carries no result."""
|
|
@@ -3325,7 +3544,8 @@ def render_agents(meta: Meta, ref: str, reports: list[_AgentReport],
|
|
|
3325
3544
|
out.append("*No report recorded.*\n")
|
|
3326
3545
|
continue
|
|
3327
3546
|
for i, rep in enumerate(runs, 1):
|
|
3328
|
-
|
|
3547
|
+
who = f"{meta.uuid[:8]}/{rep.agent[:8]}" if rep.in_sidecar else meta.uuid[:8]
|
|
3548
|
+
head = f"`{who}:{rep.line}` {_hhmmss(rep.when)}"
|
|
3329
3549
|
if len(runs) > 1:
|
|
3330
3550
|
head = f"**Return {i} of {len(runs)}** — {head}"
|
|
3331
3551
|
out.append(f"{head} · {rep.status or 'status not recorded'}\n")
|
|
@@ -4829,6 +5049,8 @@ def _failed(body: str, is_error: bool) -> bool:
|
|
|
4829
5049
|
def _typed_text(rec: dict) -> str:
|
|
4830
5050
|
"""Only the parts you typed. The harness rides `<system-reminder>` blocks in
|
|
4831
5051
|
the same record as a prompt, and `_record_text` would quote them with it."""
|
|
5052
|
+
if _harness_sent(rec):
|
|
5053
|
+
return ""
|
|
4832
5054
|
content = (rec.get("message") or {}).get("content")
|
|
4833
5055
|
if isinstance(content, str):
|
|
4834
5056
|
texts = [content]
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "chsum"
|
|
7
|
-
version = "3.0.
|
|
7
|
+
version = "3.0.3"
|
|
8
8
|
description = "Work logs and reload-ready context from coding-agent conversations. Deterministic: no model, nothing invented."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""A subagent's report, from the call that returns it to the rows that print it.
|
|
2
|
+
|
|
3
|
+
A report travels in up to three records: the agent's handback call in its own
|
|
4
|
+
transcript, a peer message in the parent where the harness delivered it as a
|
|
5
|
+
user record, and a task notification whose result points back at the peer
|
|
6
|
+
message. The parent keeps the peer message only for some returns — the rest
|
|
7
|
+
arrive as attachments, which no row reads — so each case below names which copy
|
|
8
|
+
a row is drawn from.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import pathlib
|
|
14
|
+
import sys
|
|
15
|
+
import tempfile
|
|
16
|
+
import unittest
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent))
|
|
19
|
+
|
|
20
|
+
from chsum import core # noqa: E402
|
|
21
|
+
|
|
22
|
+
AGENT = "a1b2c3d4e5f6a7b8c"
|
|
23
|
+
SPAWN = "toolu_01Spawn"
|
|
24
|
+
PREAMBLE = ("[Subagent hand-back] The text below is the final report of a subagent "
|
|
25
|
+
"this session delegated to. The report follows:\n")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _user(ts: str, content, **extra) -> dict:
|
|
29
|
+
return {"type": "user", "timestamp": ts, "message": {"role": "user", "content": content},
|
|
30
|
+
**extra}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _handback(ts: str, report: str) -> dict:
|
|
34
|
+
return {"type": "assistant", "timestamp": ts, "message": {"role": "assistant", "content": [
|
|
35
|
+
{"type": "tool_use", "id": f"toolu_{ts[-6:-1]}", "name": "SubagentHandback",
|
|
36
|
+
"input": {"message": report}}]}}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _pointer(tokens: int, tools: int, ms: int) -> str:
|
|
40
|
+
return (f"<task-notification>\n<task-id>{AGENT}</task-id>\n"
|
|
41
|
+
f"<tool-use-id>{SPAWN}</tool-use-id>\n<status>completed</status>\n"
|
|
42
|
+
f"<summary>Agent \"review\" finished</summary>\n"
|
|
43
|
+
f"<result>This agent's report was delivered to you as a message from "
|
|
44
|
+
f"\"{AGENT}\" (its SubagentHandback call). Read it there; it is not "
|
|
45
|
+
f"repeated here.\n</result>\n<usage><subagent_tokens>{tokens}</subagent_tokens>"
|
|
46
|
+
f"<tool_uses>{tools}</tool_uses><duration_ms>{ms}</duration_ms></usage>\n"
|
|
47
|
+
f"</task-notification>")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
PARENT = [
|
|
51
|
+
_user("2026-09-25T04:00:00.000Z", "review the route", origin={"kind": "human"}),
|
|
52
|
+
{"type": "assistant", "timestamp": "2026-09-25T04:00:01.000Z", "message": {
|
|
53
|
+
"role": "assistant", "content": [{"type": "tool_use", "id": SPAWN, "name": "Agent",
|
|
54
|
+
"input": {"description": "review"}}]}},
|
|
55
|
+
_user("2026-09-25T04:00:02.000Z",
|
|
56
|
+
[{"type": "tool_result", "tool_use_id": SPAWN, "content": "launched"}]),
|
|
57
|
+
# The first return reached the parent as an attachment: its pointer is left
|
|
58
|
+
# in the queue record alone, and no peer message holds its report.
|
|
59
|
+
{"type": "queue-operation", "operation": "enqueue", "timestamp": "2026-09-25T04:02:00.000Z",
|
|
60
|
+
"content": _pointer(54154, 7, 56677)},
|
|
61
|
+
_user("2026-09-25T04:03:00.000Z", "thanks", origin={"kind": "human"}),
|
|
62
|
+
_user("2026-09-25T04:05:00.000Z",
|
|
63
|
+
f"Another Claude session sent a message:\n<agent-message from=\"{AGENT}\">\n"
|
|
64
|
+
f"{PREAMBLE} Second report.\n - indented once\n</agent-message>",
|
|
65
|
+
isMeta=True, turnOrigin="peer",
|
|
66
|
+
origin={"kind": "peer", "from": AGENT, "handback": True,
|
|
67
|
+
"body": f"{PREAMBLE} Second report.\n - indented once\n"}),
|
|
68
|
+
_user("2026-09-25T04:05:10.000Z", _pointer(72000, 11, 61000),
|
|
69
|
+
origin={"kind": "task-notification"}, turnOrigin="task_notification"),
|
|
70
|
+
_user("2026-09-25T04:05:20.000Z",
|
|
71
|
+
"The previous response failed to produce a valid tool call. Please retry "
|
|
72
|
+
"the tool call now.", isMeta=True),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
SIDECAR = [
|
|
76
|
+
_user("2026-09-25T04:00:03.000Z", "Review one route."),
|
|
77
|
+
_user("2026-09-25T04:00:04.000Z",
|
|
78
|
+
"<system-reminder>\nYour final report is delivered through SubagentHandback."
|
|
79
|
+
"\n</system-reminder>", isMeta=True),
|
|
80
|
+
_handback("2026-09-25T04:01:59.000Z", "First report."),
|
|
81
|
+
_handback("2026-09-25T04:04:59.000Z", "Second report.\n- indented once"),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Senders(unittest.TestCase):
|
|
86
|
+
"""A message another sender put in an agent's conversation: labelled by its
|
|
87
|
+
sender, and opening the agent's next turn when the view is scoped to it."""
|
|
88
|
+
|
|
89
|
+
def setUp(self) -> None:
|
|
90
|
+
tmp = tempfile.TemporaryDirectory(prefix="chsum-test-")
|
|
91
|
+
self.addCleanup(tmp.cleanup)
|
|
92
|
+
self.path = pathlib.Path(tmp.name) / "sess.jsonl"
|
|
93
|
+
self.path.write_text(json.dumps(_user("2026-09-25T04:00:00.000Z", "go",
|
|
94
|
+
origin={"kind": "human"})) + "\n")
|
|
95
|
+
side = self.path.parent / self.path.stem / "subagents" / f"agent-{AGENT}.jsonl"
|
|
96
|
+
side.parent.mkdir(parents=True)
|
|
97
|
+
recs = [
|
|
98
|
+
_user("2026-09-25T04:00:01.000Z", "Research the question."),
|
|
99
|
+
_user("2026-09-25T04:01:00.000Z",
|
|
100
|
+
"Another Claude session sent a message while you were working:\n"
|
|
101
|
+
"<agent-message from=\"caller\">\nWrap up now.\n</agent-message>",
|
|
102
|
+
isMeta=True, origin={"kind": "peer", "from": "caller",
|
|
103
|
+
"name": "general-purpose", "body": "Wrap up now.\n"}),
|
|
104
|
+
_user("2026-09-25T04:02:00.000Z",
|
|
105
|
+
"The coordinator sent a message while you were working: Change of "
|
|
106
|
+
"direction.", isMeta=True, origin={"kind": "coordinator"}),
|
|
107
|
+
_user("2026-09-25T04:03:00.000Z",
|
|
108
|
+
"[SYSTEM NOTIFICATION - NOT USER INPUT]\nThis is an automated event.\n\n"
|
|
109
|
+
"<task-notification>\n<task-id>nested01</task-id>\n"
|
|
110
|
+
"<tool-use-id>toolu_01Nested</tool-use-id>\n<status>completed</status>\n"
|
|
111
|
+
"<result>Nested report.</result>\n</task-notification>",
|
|
112
|
+
isMeta=True, origin={"kind": "task-notification"}),
|
|
113
|
+
]
|
|
114
|
+
recs.insert(1, {"type": "assistant", "timestamp": "2026-09-25T04:00:02.000Z",
|
|
115
|
+
"message": {"role": "assistant", "content": [
|
|
116
|
+
{"type": "tool_use", "id": "toolu_01Nested", "name": "Agent",
|
|
117
|
+
"input": {"description": "nested"}}]}})
|
|
118
|
+
side.write_text("".join(json.dumps(r) + "\n" for r in recs))
|
|
119
|
+
|
|
120
|
+
def test_each_sender_is_named_and_opens_the_agents_turn(self):
|
|
121
|
+
rows = [(r.label, r.text, r.starts_turn)
|
|
122
|
+
for r in core.collect_rows(self.path, core._ROW_KINDS["messages"], AGENT)
|
|
123
|
+
if r.kind == "message"]
|
|
124
|
+
self.assertEqual(rows, [
|
|
125
|
+
("user", "Research the question.", True),
|
|
126
|
+
("message from general-purpose", "Wrap up now.", True),
|
|
127
|
+
("coordinator", "Change of direction.", True),
|
|
128
|
+
("agent nested01 returned", "Nested report.", False),
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Handbacks(unittest.TestCase):
|
|
133
|
+
|
|
134
|
+
def setUp(self) -> None:
|
|
135
|
+
tmp = tempfile.TemporaryDirectory(prefix="chsum-test-")
|
|
136
|
+
self.addCleanup(tmp.cleanup)
|
|
137
|
+
self.path = pathlib.Path(tmp.name) / "sess.jsonl"
|
|
138
|
+
side = self.path.parent / self.path.stem / "subagents" / f"agent-{AGENT}.jsonl"
|
|
139
|
+
side.parent.mkdir(parents=True)
|
|
140
|
+
for path, recs in ((self.path, PARENT), (side, SIDECAR)):
|
|
141
|
+
path.write_text("".join(json.dumps(r) + "\n" for r in recs))
|
|
142
|
+
|
|
143
|
+
def rows(self, only_agent: str = "") -> list:
|
|
144
|
+
return [r for r in core.collect_rows(self.path, core._ROW_KINDS["messages"],
|
|
145
|
+
only_agent)
|
|
146
|
+
if r.kind == "message"]
|
|
147
|
+
|
|
148
|
+
def test_each_report_prints_once_with_its_stop(self):
|
|
149
|
+
returned = [(r.agent, r.text, r.label) for r in self.rows()
|
|
150
|
+
if r.label.startswith("agent ")]
|
|
151
|
+
self.assertEqual(returned, [
|
|
152
|
+
(AGENT, "First report.",
|
|
153
|
+
f"agent {AGENT} returned · completed · 54k tokens · 7 tools · 56s"),
|
|
154
|
+
("", "Second report.\n- indented once",
|
|
155
|
+
f"agent {AGENT} returned · completed · 72k tokens · 11 tools · 1m"),
|
|
156
|
+
])
|
|
157
|
+
|
|
158
|
+
def test_no_row_carries_harness_text(self):
|
|
159
|
+
texts = "\n".join(r.text for r in self.rows())
|
|
160
|
+
for noise in ("system-reminder", "Another Claude session", "delivered to you",
|
|
161
|
+
"failed to produce a valid tool call"):
|
|
162
|
+
self.assertNotIn(noise, texts)
|
|
163
|
+
|
|
164
|
+
def test_scoped_to_the_agent_its_own_reports_stay(self):
|
|
165
|
+
returned = [r.text for r in self.rows(AGENT) if r.label.startswith("agent ")]
|
|
166
|
+
self.assertEqual(returned, ["First report.", "Second report.\n- indented once"])
|
|
167
|
+
|
|
168
|
+
def test_only_typed_prompts_open_a_turn(self):
|
|
169
|
+
self.assertEqual([r.text for r in self.rows() if r.starts_turn],
|
|
170
|
+
["review the route", "thanks"])
|
|
171
|
+
|
|
172
|
+
def test_the_anchor_is_the_last_typed_prompt(self):
|
|
173
|
+
line, _ = core._last_prompt(self.path)
|
|
174
|
+
self.assertEqual(line, 5)
|
|
175
|
+
|
|
176
|
+
def test_agents_view_lists_every_return_with_its_source(self):
|
|
177
|
+
got = [(r.result, r.in_sidecar, r.status) for r in core._agent_reports(self.path)]
|
|
178
|
+
self.assertEqual(got, [
|
|
179
|
+
("First report.", True, "completed · 54k tokens · 7 tools · 56s"),
|
|
180
|
+
("Second report.\n- indented once", False,
|
|
181
|
+
"completed · 72k tokens · 11 tools · 1m"),
|
|
182
|
+
])
|
|
183
|
+
|
|
184
|
+
def test_drill_block_names_each_agent_by_its_whole_ref(self):
|
|
185
|
+
block = "\n".join(core._drill_block("ch_0123", self.path))
|
|
186
|
+
self.assertIn(f"- `ch_0123/{AGENT}` — ", block)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__":
|
|
190
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|