# ant


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

Claude Code stores every conversation as a JSONL transcript, and
`claude --resume <session-id>` rebuilds a conversation from one. It does
not care who wrote the file. A transcript assembled by hand, including
tool calls that never really ran, resumes like any other. So sessions
can be mined for data, saved as templates, or built synthetically to
give a fresh session worked examples of tool use already in its context.
This module finds, reads, and writes them.

``` python
from fastcore.test import *
from fastllm.chat import tool_dtls_tag, fmt2hist
from llmsurgery.dialog import *
from collections import Counter
import tempfile, shutil
```

## Where sessions live

Each project gets a folder under `~/.claude/projects`, named by the
project’s absolute path with every character that is not a letter or
digit replaced by `-`. The path is resolved first, which matters on
macOS, where `/tmp` and `/var` are symlinks into `/private`. The folder
for a project in `/tmp/foo` is therefore `-private-tmp-foo`.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L25"
target="_blank" style="float:right; font-size:smaller">source</a>

### sess_dir

``` python
def sess_dir(
    cwd:NoneType=None, # Project directory; the current directory if None
):
```

*The folder where Claude Code keeps session transcripts for the project
at `cwd`*

``` python
sess_dir()
```

    Path('/Users/jhoward/.claude/projects/-Users-jhoward-aai-ws-llmsurgery-nbs')

Underscores are replaced too, which is easy to get wrong when sanitizing
by hand:

``` python
test_eq(sess_dir('/a/b_c').name, '-a-b-c')
test_eq(sess_dir('~'), sess_dir(Path.home()))
```

Claude Code exports a session id as `CLAUDE_CODE_SESSION_ID` to every
process it spawns, shell commands and MCP servers alike. But it
identifies the current run, not the conversation: `claude --resume` and
post-compaction restarts put a fresh id in the environment, while
records keep appending to the original transcript, which is named by the
conversation’s first session id. So the env var names the transcript
only while a conversation is on its first run, and can advertise an id
that names no file at all. The reliable name for “this conversation” is
instead the most recently modified transcript in the project’s session
folder: appends keep the live transcript’s mtime freshest through
restarts and resumes alike.
[`cur_sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#cur_sess)
uses that heuristic, keeping the env var only as a fallback for when no
transcript exists yet. One caveat: two conversations open on the same
project trade the newest spot on every write, so under concurrent
sessions pass an explicit `sid` (clikernel sidesteps this by resolving
its host conversation once, at worker spawn, when the spawning
conversation’s transcript is freshest).

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L32"
target="_blank" style="float:right; font-size:smaller">source</a>

### cur_sess

``` python
def cur_sess(
    cwd:NoneType=None, # Project directory; the current directory if None
):
```

*The current conversation’s session id: the most recent transcript for
the project at `cwd`, else the advertised id*

``` python
cur_sess()
```

The newest transcript wins; with no transcripts at all, the advertised
id is used:

``` python
tp = Path(tempfile.mkdtemp())
sd = sess_dir(tp)
sd.mkdir(parents=True)
for i,n in enumerate(['older','newer']):
    (sd/f'{n}.jsonl').touch()
    os.utime(sd/f'{n}.jsonl', (i,i))
test_eq(cur_sess(tp), 'newer')
test_eq(cur_sess(tempfile.mkdtemp()), os.environ.get('CLAUDE_CODE_SESSION_ID'))
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L40"
target="_blank" style="float:right; font-size:smaller">source</a>

### sess_file

``` python
def sess_file(
    sid:NoneType=None, # Session id; `cur_sess(cwd)` if None
    cwd:NoneType=None, # Project directory; the current directory, then all projects, if None
):
```

*Path to the transcript of session `sid` for the project at `cwd`*

Under Claude Code,
[`sess_file()`](https://AnswerDotAI.github.io/llmsurgery/ant.html#sess_file)
with no arguments is therefore the running conversation’s own
transcript, surviving resumes and restarts. Session ids are unique
across projects, so when an explicit `sid`’s file is not in the current
directory’s folder,
[`sess_file`](https://AnswerDotAI.github.io/llmsurgery/ant.html#sess_file)
looks across all project folders, and the defaults work from anywhere,
including a notebook kernel whose working directory is not the project
root.

``` python
test_eq(sess_file('abc', '/a/b_c'), SESSIONS/'-a-b-c'/'abc.jsonl')
```

## Writing a session

Records are plain dicts, so writing a session comes down to filling the
envelope and linking the chain.
[`mk_rec`](https://AnswerDotAI.github.io/llmsurgery/ant.html#mk_rec)
fills the envelope for one message. It writes the optional bookkeeping a
real transcript carries (`version`, `gitBranch`, `userType`,
`permissionMode`, and API metadata on assistant records), not only the
six required fields: what Claude Code’s LLM side makes of a
sparse-but-valid record is close to untestable, so we err towards
realistic.

Two records with the same content get different files by default, since
ids and timestamps are fresh each call. Sometimes the opposite is
wanted: the same history should produce byte-identical records, so the
same session id maps to the same file however many times it is rebuilt.
[`canon`](https://AnswerDotAI.github.io/llmsurgery/ant.html#canon) gives
a canonical JSON rendering to hash, and
[`stable_uuid`](https://AnswerDotAI.github.io/llmsurgery/ant.html#stable_uuid)
turns any string into a deterministic uuid. `fastllm_claude_code.core`
derives its session and record ids this way, and a session template
built from a fixed script can too.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L57"
target="_blank" style="float:right; font-size:smaller">source</a>

### stable_uuid

``` python
def stable_uuid(
    s
):
```

*A uuid deterministically derived from string `s`*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L53"
target="_blank" style="float:right; font-size:smaller">source</a>

### canon

``` python
def canon(
    o
):
```

*Canonical compact JSON for `o`, key-sorted, for stable hashing*

``` python
test_eq(canon(dict(b=1, a=2)), canon(dict(a=2, b=1)))
test_eq(stable_uuid('x'), stable_uuid('x'))
assert stable_uuid('x') != stable_uuid('y')
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L74"
target="_blank" style="float:right; font-size:smaller">source</a>

### mk_rec

``` python
def mk_rec(
    role, # 'user' or 'assistant'
    content, # A string, or a list of content blocks
    cwd:str='.', # Project directory recorded in the envelope
    uid:NoneType=None, # Record uuid; random if None
    ts:NoneType=None, # ISO8601 timestamp; the current time if None
    model:str='claude-sonnet-4-6', # Recorded in assistant API metadata; None omits it, so resume uses the user's default
    input_toks:int=0, # `input_tokens` recorded in assistant usage, e.g. an estimate of the context so far
    **kwargs
):
```

*A transcript record for one conversation message, ready for
[`save_sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#save_sess)*

``` python
mk_rec('user', 'Hello!')
```

    {'type': 'user',
     'uuid': '1d377c05-0278-4605-afa7-29c0d9beb546',
     'parentUuid': None,
     'sessionId': None,
     'timestamp': '2026-07-14T04:12:07.703Z',
     'cwd': '/Users/jhoward/aai-ws/llmsurgery/nbs',
     'version': '2.1.206',
     'gitBranch': 'HEAD',
     'isSidechain': False,
     'userType': 'external',
     'permissionMode': 'default',
     'message': {'type': 'message', 'role': 'user', 'content': 'Hello!'}}

Assistant records get deterministic API metadata derived from the record
id, and `stop_reason` reflects a trailing tool call:

``` python
tu = [dict(type='tool_use', id='toolu_01', name='probe', input={})]
r = mk_rec('assistant', tu, uid=stable_uuid('demo'), ts='2026-01-01T00:00:00.000Z')
test_eq(r['message']['stop_reason'], 'tool_use')
test_eq(r, mk_rec('assistant', tu, uid=stable_uuid('demo'), ts='2026-01-01T00:00:00.000Z'))
assert 'requestId' in r and 'requestId' not in mk_rec('user', 'hi')
test_eq(r['message']['usage']['output_tokens'], _est_toks(tu))
assert 'model' not in mk_rec('assistant', tu, model=None)['message']
test_eq(mk_rec('user', [dict(type='text', text='hi')])['message']['content'], 'hi')  # lone text block: written as a string, as Claude Code does
test_eq(mk_rec('user', 'hi', cwd='~')['cwd'], str(Path.home()))
```

[`save_sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#save_sess)
assigns a session id, chains each record to the one before, and writes
the file where `claude --resume` will look for it. It re-links
`parentUuid` unconditionally, so it is for writing linear conversations.
To copy a session while keeping its branch structure, write the records
yourself.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L100"
target="_blank" style="float:right; font-size:smaller">source</a>

### save_sess

``` python
def save_sess(
    recs, # Records in conversation order, e.g. from `mk_rec`
    sid:NoneType=None, # Session id; a fresh uuid if None
    cwd:NoneType=None, # Project directory; the current directory if None
    ts:NoneType=None, # If given, stamp every record's timestamp: True for the current time, or an ISO8601 string
):
```

*Chain `recs`, write them as session `sid` for the project at `cwd`, and
return `sid`*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L118"
target="_blank" style="float:right; font-size:smaller">source</a>

### append_sess

``` python
def append_sess(
    recs, # Records to append, e.g. a munged template round
    sid:NoneType=None, # Session to append to; `cur_sess()` if None
    cwd:NoneType=None, # Project directory; the current directory if None
    ts:NoneType=None, # If given, stamp each appended record's timestamp: True for the current time, or an ISO8601 string
):
```

*Chain `recs` onto the tail of session `sid` and append them to its
transcript, returning `sid`*

[`append_sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#append_sess)
is the mid-life counterpart: it chains new records onto the transcript’s
tail without rewriting the existing bytes, so structures already in the
file (compaction records, sidechains) are left untouched. It’s how a
worked round gets spliced into an existing conversation, e.g. llmdojo’s
`claudedojo -r` refreshing a session after a compaction.

Whole conversations convert in one call:
[`msgs2recs`](https://AnswerDotAI.github.io/llmsurgery/ant.html#msgs2recs)
takes Anthropic-style messages (dicts with `role` and `content`, like
`claude_mk_msg` or fastllm’s `denorm_msgs` produce) and builds one
deterministic record per message, ready for
[`save_sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#save_sess).
The same messages and key give the same ids, so a rebuilt session file
is byte-identical.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L135"
target="_blank" style="float:right; font-size:smaller">source</a>

### msgs2recs

``` python
def msgs2recs(
    msgs, # Anthropic-style messages: dicts with `role` and `content`
    key:str='', # Salt: the same messages and key give the same ids
    cwd:str='.', # Project directory recorded in the envelopes
    ts:str='2026-01-01T00:00:00.000Z', # Timestamp for every record
    model:str='claude-sonnet-4-6', # Recorded in assistant API metadata; None omits it, so resume uses the user's default
    **kwargs
):
```

*Deterministic transcript records for `msgs`, one record per message*

``` python
den = [dict(role='user', content='Ping?'), dict(role='assistant', content=[dict(type='text', text='Pong.')])]
r1,r2 = msgs2recs(den, 'k'),msgs2recs(den, 'k')
test_eq(canon(r1), canon(r2))
test_ne(r1[0]['uuid'], msgs2recs(den, 'other')[0]['uuid'])
test_eq([r['type'] for r in r1], ['user','assistant'])
test_eq(r1[1]['message']['usage'], dict(input_tokens=1, output_tokens=3, cache_creation_input_tokens=0, cache_read_input_tokens=0))
```

## Synthetic tool calls

A worked tool call is two records joined by one id: a `tool_use` block
in an assistant record, answered by a `tool_result` block in the user
record that follows.
[`mk_tu`](https://AnswerDotAI.github.io/llmsurgery/ant.html#mk_tu) and
[`mk_tr`](https://AnswerDotAI.github.io/llmsurgery/ant.html#mk_tr) build
the pair so the ids cannot drift, and
[`tool_turn`](https://AnswerDotAI.github.io/llmsurgery/ant.html#tool_turn)
assembles the full exchange, from request to reply.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L166"
target="_blank" style="float:right; font-size:smaller">source</a>

### tool_turn

``` python
def tool_turn(
    prompt, # The user request
    name, # Tool name
    input, # Tool arguments
    output, # Tool result
    answer, # The assistant's closing text
    **kwargs
):
```

*A complete synthetic tool-use turn, as four records ready for
[`save_sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#save_sess)*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L159"
target="_blank" style="float:right; font-size:smaller">source</a>

### mk_tr

``` python
def mk_tr(
    tu, # The `tool_use` block being answered
    content, # The tool's output
):
```

*The `tool_result` content block answering `tu`*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L151"
target="_blank" style="float:right; font-size:smaller">source</a>

### mk_tu

``` python
def mk_tu(
    name, # Tool name, as the transcript records it
    input:NoneType=None, # Tool arguments
    tid:NoneType=None, # tool_use id; random if None
):
```

*A `tool_use` content block*

The sample session in the next section is built from exactly one such
turn.

## A sample session

The smallest useful synthetic history is a tool call that never ran,
whose result carries a fact the model could not know any other way. We
write it against a scratch project directory.

``` python
proj = Path(tempfile.mkdtemp())
sample = tool_turn('Measure the flux please.', 'flux_meter', {}, 'flux: 41.7 kilofinches',
    'The flux reading is 41.7 kilofinches.', cwd=proj)
sid = save_sess(sample, cwd=proj)
sid
```

    '4d5671f8-b730-43a2-b35e-6977f164f0e3'

## Reading a session

A transcript is one JSON object per line.
[`load_sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#load_sess)
wraps each in `dict2obj` so fields read as attributes.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L180"
target="_blank" style="float:right; font-size:smaller">source</a>

### load_sess

``` python
def load_sess(
    sid:NoneType=None, # Session id; the current session if None
    cwd:NoneType=None, # Project directory; the current directory if None
):
```

*The records of session `sid`, as an `L` of attribute-access dicts*

Reading it back gives exactly what we wrote:

``` python
back = load_sess(sid, proj)
test_eq(len(back), 4)
test_eq(back[-1].message.content[0].text, 'The flux reading is 41.7 kilofinches.')
test_eq(back[2].message.content[0].tool_use_id, back[1].message.content[0].id)
```

A record carries more than resume strictly needs. Only six fields are
required: `type`, `uuid`, `parentUuid`, `sessionId`, `timestamp`, and
`message`. The rest is optional bookkeeping. Strip `timestamp` and the
session is not even found. `message` is shaped exactly as the Anthropic
API shapes messages: a `role`, plus `content` as either a string or a
list of content blocks (`text`, `tool_use`, `tool_result`, `thinking`).
Assistant records in real transcripts also carry API metadata
(`requestId`, `message.id`, `model`, usage), and none of it is needed on
resume. In particular, synthetic histories work without `thinking`
blocks.

## The parent chain

Resume does not replay the file top to bottom. Reconstruction starts at
the last record and walks `parentUuid` links backwards, so a record
nothing links to is dropped (the CLI prints a warning). Rewinding a
conversation is what creates such records: the abandoned turns stay in
the file, off the final chain.
[`sess_thread`](https://AnswerDotAI.github.io/llmsurgery/ant.html#sess_thread)
performs the same walk.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L188"
target="_blank" style="float:right; font-size:smaller">source</a>

### sess_thread

``` python
def sess_thread(
    recs, # Session records, e.g. from `load_sess`
):
```

*The records on the active conversation chain, walking `parentUuid` back
from the last record*

On the sample, every record is on the chain:

``` python
test_eq(sess_thread(back).attrgot('uuid'), back.attrgot('uuid'))
```

Break a link and the walk stops early, mirroring what resume does with
unchained records:

``` python
broken = load_sess(sid, proj)
broken[2].parentUuid = None
test_eq(len(sess_thread(broken)), 2)
```

## Searching a session

A long conversation accumulates thousands of records, and
[`sess_thread`](https://AnswerDotAI.github.io/llmsurgery/ant.html#sess_thread)
deliberately stops at the last compaction boundary, where the parent
chain breaks. Finding where something was said or decided is a different
job: search every record’s text, then read the records around the hit.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L200"
target="_blank" style="float:right; font-size:smaller">source</a>

### rec_txt

``` python
def rec_txt(
    r, # A session record
):
```

*Every readable string in `r`’s message content, joined, for finding
records by text*

Content blocks nest (a `tool_result` can itself hold a list of text
blocks), so
[`rec_txt`](https://AnswerDotAI.github.io/llmsurgery/ant.html#rec_txt)
walks the whole message and joins every readable string it finds,
skipping bookkeeping fields — block types, tool ids, and thinking
signatures — that would otherwise pollute matches. On the sample, the
planted fact appears in the tool result and in the reply that quotes it:

``` python
test_eq([r['type'] for r in sample if 'kilofinches' in rec_txt(r)], ['user','assistant'])
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L213"
target="_blank" style="float:right; font-size:smaller">source</a>

### rec_role

``` python
def rec_role(
    r, # A session record
):
```

*The conversational role of `r`: a user record carrying tool results
counts as `tool`*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L207"
target="_blank" style="float:right; font-size:smaller">source</a>

### conv_recs

``` python
def conv_recs(
    recs, # Session records, e.g. from `load_sess`
):
```

*Just the records carrying conversation messages, dropping Claude Code’s
bookkeeping*

[`conv_recs`](https://AnswerDotAI.github.io/llmsurgery/ant.html#conv_recs)
is the searchable subset of a transcript, and
[`rec_role`](https://AnswerDotAI.github.io/llmsurgery/ant.html#rec_role)
labels each record the way
[`recs2chat`](https://AnswerDotAI.github.io/llmsurgery/ant.html#recs2chat)
classifies them: the `user` type covers both real prompts and the tool
results riding back, and telling them apart is the first thing every
reader of a transcript needs. On the sample session:

``` python
test_eq(len(conv_recs(sample)), 4)
test_eq([rec_role(r) for r in sample], ['user','assistant','tool','assistant'])
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L232"
target="_blank" style="float:right; font-size:smaller">source</a>

### sess_search

``` python
def sess_search(
    pat, # Regex to find in conversation text
    sid:NoneType=None, # Session id; the current session if None
    cwd:NoneType=None, # Project directory; the current directory if None
    maxlen:int=160, # Preview characters shown around a hit's first match
):
```

*Search every conversation record of a session, returning
[`SessHits`](https://AnswerDotAI.github.io/llmsurgery/ant.html#sesshits)
with the records on `.recs`*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L228"
target="_blank" style="float:right; font-size:smaller">source</a>

### SessHits

``` python
def SessHits(
    *args, **kwargs
):
```

*Search hits with a match-centered preview per line*

The result displays one line per hit — index, role, timestamp, and the
text around the first match — and keeps the searched records on `.recs`,
so a hit’s index leads straight to its neighbors. Searching the whole
file rather than the thread is deliberate: pre-compaction history is
exactly what archaeology is usually after. On the sample session the
planted fact appears in the tool result and the reply that reports it:

``` python
hits = sess_search('kilofinches', sid, proj)
test_eq([h.role for h in hits], ['tool','assistant'])
hits
```

        2 tool      2026-07-14T04:12 flux: 41.7 kilofinches
        3 assistant 2026-07-14T04:12 The flux reading is 41.7 kilofinches.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L247"
target="_blank" style="float:right; font-size:smaller">source</a>

### show_recs

``` python
def show_recs(
    recs, # Session records, e.g. a slice of `SessHits.recs`
    mx:int=500, # Characters of text shown per record
    showall:bool=False, # Include bookkeeping records?
):
```

*A readable transcript of records in `recs`; conversation records only
unless `showall`*

[`show_recs`](https://AnswerDotAI.github.io/llmsurgery/ant.html#show_recs)
is the reading half of the workflow: search, take a slice of `.recs`
around an interesting hit, and read it as a conversation. It is
deliberately tolerant where
[`recs2chat`](https://AnswerDotAI.github.io/llmsurgery/ant.html#recs2chat)
is strict — any stretch of any transcript renders, with long records
truncated:

``` python
s = show_recs(hits.recs[hits[0].i-2:], mx=60)
assert s.count('---\n')==4 and 'flux: 41.7' in s
s
sysr = dict(type='system', subtype='compact_boundary', timestamp='2026-01-01T00:00:00.000Z')
sa = show_recs([sample[0], sysr], showall=True)
assert sa.count('---\n')==2 and 'system:compact_boundary' in sa
```

    --- user 2026-07-14T04:12:07 ---
    Measure the flux please.
    --- assistant 2026-07-14T04:12:07 ---
    flux_meter
    --- tool 2026-07-14T04:12:07 ---
    flux: 41.7 kilofinches
    --- assistant 2026-07-14T04:12:07 ---
    The flux reading is 41.7 kilofinches.

## Curating a captured session

Synthetic construction suits a short demo. For anything longer, capture
beats writing: run a real session, then keep the span worth replaying.
The search tools above find the span; curation then needs two
operations: dropping what resume does not need, and re-deriving ids so
one capture gives one file.

Thinking blocks are usually the bulk of a captured assistant turn, and
resume rebuilds a conversation without them. Live transcripts write one
content block per assistant record, so stripping thinking means dropping
whole records;
[`save_sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#save_sess)
re-chains whatever survives.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L262"
target="_blank" style="float:right; font-size:smaller">source</a>

### strip_think

``` python
def strip_think(
    recs, # Session records
):
```

*Drop records whose message content is only `thinking` blocks; resume
does not need them*

``` python
think = mk_rec('assistant', [dict(type='thinking', thinking='Quiet planning.', signature='')])
test_eq(strip_think([*sample, think]), sample)
```

After thinking, tool traffic is the bulk: file dumps in results, whole
files in edit-tool inputs.
[`trunc_tools`](https://AnswerDotAI.github.io/llmsurgery/ant.html#trunc_tools)
caps every string inside `tool_use` inputs and `tool_result` content,
leaving structure intact and marking each cut with the count of
characters dropped. Records come back as fresh dicts, originals
untouched.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L278"
target="_blank" style="float:right; font-size:smaller">source</a>

### trunc_tools

``` python
def trunc_tools(
    recs, # Session records
    mx:int=2000, # Maximum characters per string in tool inputs and results
):
```

*Copies of `recs` with strings in tool_use inputs and tool_result
content truncated to `mx` characters*

A big tool result shrinks to the cap plus a marker; the small input
alongside it, and the original records, are untouched:

``` python
lt = tool_turn('Read it all.', 'reader', dict(path='/tmp/big.txt'), 'line\n'*500, 'Long.')
tr = trunc_tools(lt, 100)
assert rec_txt(tr[2]).endswith('…[+2400 chars]')
test_eq(tr[1]['message']['content'][0]['input'], dict(path='/tmp/big.txt'))
test_eq(lt[2]['message']['content'][0]['content'], 'line\n'*500)
```

Captured ids are random, so saving the same curated span twice gives two
files that differ in every line.
[`reid_recs`](https://AnswerDotAI.github.io/llmsurgery/ant.html#reid_recs)
re-derives them from position and a salt instead: record uuids,
`tool_use`/`tool_result` pairs, assistant API metadata, and any envelope
field that referenced a renamed uuid (such as
`sourceToolAssistantUUID`). Records come back as fresh dicts; the
originals are untouched.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L293"
target="_blank" style="float:right; font-size:smaller">source</a>

### reid_recs

``` python
def reid_recs(
    recs, # Records in conversation order
    key:str='', # Salt: the same records and key give the same ids
    ts:NoneType=None, # If given, set every record's timestamp to this
):
```

*Deterministically re-derive record uuids, tool_use ids, and API
metadata, so one capture gives one file*

Re-identified records keep the tool pairing, differ under a different
salt, and leave the originals alone:

``` python
r1,r2 = reid_recs(sample, 'tmpl'),reid_recs(sample, 'tmpl')
test_eq(canon(list(r1)), canon(list(r2)))
test_eq(r1[2]['message']['content'][0]['tool_use_id'], r1[1]['message']['content'][0]['id'])
test_ne(r1[0]['uuid'], reid_recs(sample, 'other')[0]['uuid'])
test_ne(r1[1]['message']['content'][0]['id'], sample[1]['message']['content'][0]['id'])
```

With the timestamps pinned too, the whole file is byte-for-byte
reproducible. A captured record can also carry a `session_id` field
alongside `sessionId`;
[`save_sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#save_sess)
keeps both in step:

``` python
t1,t2 = reid_recs(sample, 'tmpl', ts='2026-01-01T00:00:00.000Z'),reid_recs(sample, 'tmpl', ts='2026-01-01T00:00:00.000Z')
t1[0]['session_id'] = t2[0]['session_id'] = 'stale'
t1[0]['nested'] = t2[0]['nested'] = L([L(1,2)])
tid = save_sess(t1, stable_uuid('tmpl'), proj)
b = sess_file(tid, proj).read_bytes()
test_eq(save_sess(t2, stable_uuid('tmpl'), proj), tid)
test_eq(sess_file(tid, proj).read_bytes(), b)
test_eq(load_sess(tid, proj)[0].session_id, tid)
test_eq(json.loads(sess_file(tid, proj).read_text().splitlines()[0])['nested'], [[1,2]])
```

Appending chains onto the existing tail and leaves the prior bytes
untouched:

``` python
apsid = save_sess(reid_recs(sample, 'apbase'), stable_uuid('append-base'), proj)
with sess_file(apsid, proj).open('a') as f: f.write(json.dumps(dict(type='last-prompt', lastPrompt='hi'))+'\n')
more = tool_turn('And the humidity?', 'hygro', {}, '41%', 'Humid too.')
more[0]['nested'] = L(1,2)
test_eq(append_sess(more, apsid, proj), apsid)
apl = load_sess(apsid, proj)
test_eq(len(apl), 9)
test_eq(apl[5].parentUuid, apl[3].uuid)
test_eq([r.sessionId for r in apl[-4:]], [apsid]*4)
test_eq(apl[-4].nested, [1,2])
```

[`fork_sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#fork_sess)
puts the whole diet on one line: load a session, optionally drop
thinking and truncate tool traffic, and write the result under a fresh
id, leaving the original untouched. Resume the returned id to compare
the munged conversation against the original. With a `key`, ids
re-derive deterministically, so re-running the same munge overwrites its
fork instead of accumulating copies.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L321"
target="_blank" style="float:right; font-size:smaller">source</a>

### fork_sess

``` python
def fork_sess(
    sid:NoneType=None, # Session id to fork; `cur_sess()` if None
    cwd:NoneType=None, # Project directory; passed to `sess_file` via `load_sess`
    mx:NoneType=None, # If given, truncate tool input/output strings to `mx` characters
    think:bool=True, # Keep thinking records?
    key:NoneType=None, # If given, record and session ids re-derive deterministically from this salt
):
```

*Write a munged copy of session `sid` under a fresh id, returning the
new id to resume*

Forking the sample with a thinking record appended and a tight cap: the
fork loses the thinking record and truncates the tool result, the
original keeps all five records, and the same call gives the same fork
id:

``` python
tsid = save_sess(reid_recs([*sample, think], 'munge'), stable_uuid('munge'), proj)
fkid = fork_sess(tsid, proj, mx=10, think=False, key='fork1')
test_eq(len(load_sess(fkid, proj)), 4)
assert rec_txt(load_sess(fkid, proj)[2]).endswith('…[+12 chars]')
test_eq(len(load_sess(tsid, proj)), 5)
test_eq(fork_sess(tsid, proj, mx=10, think=False, key='fork1'), fkid)
```

## From dialogs

An authored dialog converts to a session in two steps:
[`dlg2chat`](https://AnswerDotAI.github.io/llmsurgery/hist.html#dlg2chat)
(in `hist`) recovers the canonical messages with real tool calls, and
`denorm_msgs` from `fastllm.anthropic` shapes them for Claude.
[`dlg2sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#dlg2sess)
then writes the records. The session id derives from the dialog name, so
re-converting an edited dialog overwrites its session rather than
accumulating copies.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L343"
target="_blank" style="float:right; font-size:smaller">source</a>

### dlg2sess

``` python
def dlg2sess(
    dlg, # The dialog to convert
    cwd:NoneType=None, # Project directory for the session; the current directory if None
    key:str='dlg2sess', # Salt for deterministic record ids
    aim_info:NoneType=None, # Model capability dict; images enabled if None
):
```

*Write `dlg` as a Claude Code session for the project at `cwd`,
returning the session id to resume; tagged raw messages re-emit their
original records*

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L336"
target="_blank" style="float:right; font-size:smaller">source</a>

### dlg2msgs

``` python
def dlg2msgs(
    dlg, # A `Dialog`, ending with a prompt
    aim_info:NoneType=None, # Model capability dict for media handling; images enabled if None
):
```

*Anthropic-style messages for `dlg`, with each reply’s tool calls
recovered as real blocks*

A dialog whose reply used a tool, with a referenced image attachment,
exercises the whole path. The tool call comes back as paired
`tool_use`/`tool_result` blocks, and the attachment becomes a base64
image block, both exactly as real transcripts carry them:

``` python
def tool_dtl(func, args, result):
    "A tool-call details block in the reply format `fmt2hist` parses"
    d = json.dumps(dict(id='call1', server=False, call=dict(function=func, arguments=args), result=result))
    return f"{tool_dtls_tag}\n<summary><code>{func}(...)</code></summary>\n\n```json\n{d}\n```\n\n</details>"

png = tiny_png
fdlg = Dialog('flux')
fatt = Attachment(png, 'image/png')
fdlg.mk_message(f'The rig: ![](attachment:{fatt.id})', msg_type=snote, attachments=[fatt])
freply = f"Let me check.\n\n{tool_dtl('flux_meter', {'unit':'kf'}, 'flux: 41.7 kilofinches')}\n\nThe flux is 41.7 kilofinches."
fdlg.mk_message('Measure the flux please.', msg_type=sprompt, output=freply)
fmsgs = dlg2msgs(fdlg)
[(m['role'], [b['type'] for b in m['content']] if isinstance(m['content'], list) else 'str') for m in fmsgs]
```

    [('user', ['text', 'image', 'text', 'text']),
     ('assistant', ['text', 'tool_use']),
     ('user', ['tool_result']),
     ('assistant', ['text'])]

``` python
test_eq([m['role'] for m in fmsgs], ['user','assistant','user','assistant'])
ftu = first(b for b in fmsgs[1]['content'] if b['type']=='tool_use')
ftr = first(b for b in fmsgs[2]['content'] if b['type']=='tool_result')
test_eq(ftr['tool_use_id'], ftu['id'])
fimg = first(b for b in fmsgs[0]['content'] if b['type']=='image')
test_eq(fimg['source']['media_type'], 'image/png')
```

Roundtrip and determinism, into the same scratch project as the sample
session:

``` python
fsid = dlg2sess(fdlg, proj)
fback = load_sess(fsid, proj)
test_eq(len(fback), 4)
test_eq(sess_thread(fback).attrgot('uuid'), fback.attrgot('uuid'))
test_eq(dlg2sess(fdlg, proj), fsid)
```

And the live proof, resuming the dialog-built session and asking about
the planted fact (spends tokens, so out of CI):

``` python
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
opts = ClaudeAgentOptions(resume=fsid, cwd=str(proj), model='haiku')
async for m in query(prompt='What did the flux_meter tool report, exactly?', options=opts):
    if isinstance(m, ResultMessage): print(m.result)
```

    The flux_meter tool reported exactly:

    ```
    flux: 41.7 kilofinches
    ```

## Back to dialogs

The reverse direction turns a recorded session into a dialog, ready for
editing before it becomes a template.
[`recs2chat`](https://AnswerDotAI.github.io/llmsurgery/ant.html#recs2chat)
normalizes conversation records to canonical fastllm messages: one `Msg`
per record, with tool calls and results carrying the same part data that
`fmt2hist` produces, and `tool_result` records taking the `tool` role.
Thinking blocks are kept as `thinking` parts (run
[`strip_think`](https://AnswerDotAI.github.io/llmsurgery/ant.html#strip_think)
first to drop them), and any block type we cannot faithfully convert
raises rather than silently degrading.

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L382"
target="_blank" style="float:right; font-size:smaller">source</a>

### recs2chat

``` python
def recs2chat(
    recs, # Session records, e.g. from `load_sess`
):
```

*Canonical fastllm messages for the conversation records in `recs`*

On the sample session, the four records become user, assistant, tool,
and assistant messages, with the tool call and its result joined by one
id:

``` python
smsgs = recs2chat(sample)
test_eq([m.role for m in smsgs], ['user','assistant','tool','assistant'])
test_eq(smsgs[0].text, 'Measure the flux please.')
test_eq(smsgs[1].content[0].data['id'], smsgs[2].content[0].data['id'])
test_eq(_tr_txt(dict(content=[dict(type='tool_reference', tool_name='probe')])), '<tool_reference tool="probe"/>')
test_eq(recs2chat([mk_rec('assistant', 'hi')])[0].text, 'hi')
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L410"
target="_blank" style="float:right; font-size:smaller">source</a>

### chat2dlg

``` python
def chat2dlg(
    msgs, # Canonical messages, e.g. from `recs2chat`
    name, # Dialog name
    cls:type=Dialog, # Dialog class to create
    mx:int=2000, # Maximum characters per rendered tool input/output string; None disables truncation (see `hist2fmt`)
):
```

*A dialog for `msgs`: one prompt per user turn, replies rendered in the
format `fmt2hist` parses*

Prompt content is the user turn verbatim: text parts joined by blank
lines, and images pulled out as attachments with a reference left in the
text. A session that was itself built from a dialog therefore converts
back with its rendered XML wrapping (`<prompt>`,
`<message type="note">`) still in place, not the original notes; the
reverse direction is for sessions recorded live, where user content is
plain.

``` python
mdlg = chat2dlg(smsgs, 'flux back')
test_eq(len(mdlg.messages), 1)
test_eq(mdlg.messages[0].content, 'Measure the flux please.')

big = 'z'*9999
bdlg = chat2dlg(recs2chat(tool_turn('Big!', 'probe', {}, big, 'Done.')), 'untruncated', mx=None)
assert big in bdlg.messages[0].ai_res
```

The reply renders as text plus a tool-call details block, and `fmt2hist`
parses it back to exactly the canonical messages it came from. This is
the law that makes the conversion safe to edit: the dialog holds nothing
the session format cannot round-trip.

``` python
test_eq(fmt2hist(mdlg.messages[0].ai_output), smsgs[1:])
```

A pasted image survives the full circle: it becomes an attachment on the
prompt, and converting the dialog onwards to messages yields the same
base64 block the record held.

``` python
ib = [dict(type='text', text='The rig:'), dict(type='image', source=dict(type='base64', media_type='image/png', data=base64.b64encode(tiny_png).decode()))]
irecs = msgs2recs([dict(role='user', content=ib), dict(role='assistant', content=[dict(type='text', text='Nice rig.')])])
idlg = chat2dlg(recs2chat(irecs), 'rig')
test_eq(idlg.messages[0].attachments[0].data, tiny_png)
assert f'attachment:{idlg.messages[0].attachments[0].id}' in idlg.messages[0].content
test_eq(first(b for b in dlg2msgs(idlg)[0]['content'] if b['type']=='image')['source']['data'], base64.b64encode(tiny_png).decode())
```

------------------------------------------------------------------------

<a
href="https://github.com/AnswerDotAI/llmsurgery/blob/main/llmsurgery/ant.py#L435"
target="_blank" style="float:right; font-size:smaller">source</a>

### sess2dlg

``` python
def sess2dlg(
    sid:NoneType=None, # Session id; `cur_sess()` if None
    cwd:NoneType=None, # Project directory; passed to `sess_file` via `load_sess`
    name:NoneType=None, # Dialog name; the session id if None
    mx:int=2000, # Maximum characters per rendered tool input/output string; None disables truncation (see `hist2fmt`)
):
```

*The conversation of session `sid` as a dialog, one prompt per user
turn; `system` records ride along as tagged raws*

[`sess2dlg`](https://AnswerDotAI.github.io/llmsurgery/ant.html#sess2dlg)
composes the whole read path: load the transcript, walk the parent
chain, drop thinking records, and convert. Reading the sample session
back gives the same dialog we built from its messages by hand:

``` python
rdlg = sess2dlg(sid, proj, 'flux back')
test_eq(rdlg.messages[0].content, mdlg.messages[0].content)
test_eq(rdlg.messages[0].ai_output, mdlg.messages[0].ai_output)
```

Claude Code’s bookkeeping record kinds (injected-context `attachment`
records, per-turn state, file snapshots) are dropped: they’re
regenerated live and nothing reads them back. `system` records are the
one kind kept: each becomes a `raw` message tagged with `rec_kind` in
its meta and the original record verbatim under `rec`, so
[`dlg2sess`](https://AnswerDotAI.github.io/llmsurgery/ant.html#dlg2sess)
can re-emit it unchanged (envelope re-chained, everything else
byte-for-byte) and history projections know to skip it. Each prompt also
picks up its turn’s request `timestamp` and the reply’s `usage` in meta:

``` python
sysrec = dict(type='system', subtype='demo_note', uuid=stable_uuid('sys-demo'), timestamp='2026-01-01T00:00:01.000Z', level='info')
ssid = save_sess(list(reid_recs(sample, 'sysdemo')) + [sysrec], stable_uuid('sysdemo'), proj)
sysdlg = sess2dlg(ssid, proj, 'with system')
sysm = sysdlg.messages[-1]
test_eq((sysm.msg_type, sysm.meta['rec_kind'], sysm.meta['rec']['subtype']), (sraw, 'system', 'demo_note'))
sback = load_sess(ssid, proj)
test_eq(sysdlg.messages[0].meta['timestamp'], sback[0].timestamp)  # the turn's request time
test_eq(sysdlg.messages[0].meta['usage'], obj2dict(sback[3].message.usage))  # the reply's usage
```

Building the dialog back into a session re-emits the tagged record
verbatim: only the envelope fields the chaining owns (`sessionId`,
`parentUuid`) change, and the conversation records around it are
untouched by its presence, since
[`dlg2hist`](https://AnswerDotAI.github.io/llmsurgery/hist.html#dlg2hist)
skips tagged raws:

``` python
sysid2 = dlg2sess(sysdlg, proj, key='sysback')
b2 = load_sess(sysid2, proj)
test_eq(b2[-1].type, 'system')
env = ('sessionId','parentUuid')
test_eq({k:v for k,v in obj2dict(b2[-1]).items() if k not in env}, {k:v for k,v in obj2dict(sback[-1]).items() if k not in env})
test_eq([r.type for r in b2[:-1]], [r.type for r in sback[:-1]])  # conversation records unaffected
```

## Inside a live session

Since Claude Code exports `CLAUDE_CODE_SESSION_ID` to child processes,
code running inside a session can read the very transcript it is part
of. The cells below do that when a live transcript exists, and fall back
to the sample session otherwise (a plain shell, CI, or a resumed session
whose advertised id names no file).

``` python
recs = load_sess() if sess_file().exists() else load_sess(sid, proj)
len(recs)
```

    4

The conversation itself is the `user` and `assistant` records. The rest
is bookkeeping Claude Code adds as it runs: `attachment` for injected
context such as skills and file contents, `system` for hook output, and
assorted prompt, mode, and snapshot markers. Compaction adds a `user`
record flagged `isCompactSummary`, carrying a summary of everything
before it.

``` python
Counter(recs.attrgot('type'))
```

    Counter({'user': 2, 'assistant': 2})

Here is one user record in full:

``` python
first(recs, lambda r: r.type=='user' and isinstance(r.get('message',{}).get('content'), str))
```

<div class="prose" markdown="1">

``` python
{ 'cwd': '/private/var/folders/51/b2_szf2945n072c0vj2cyty40000gn/T/tmpw6hbvcbu',
  'gitBranch': 'HEAD',
  'isSidechain': False,
  'message': { 'content': 'Measure the flux please.',
               'role': 'user',
               'type': 'message'},
  'parentUuid': None,
  'permissionMode': 'default',
  'sessionId': '4d5671f8-b730-43a2-b35e-6977f164f0e3',
  'timestamp': '2026-07-14T04:12:07.848Z',
  'type': 'user',
  'userType': 'external',
  'uuid': 'd1b64443-322f-4a42-873f-4f2084145e5a',
  'version': '2.1.206'}
```

</div>

``` python
t = sess_thread(recs)
len(t), len(recs)
```

    (4, 4)

In a live transcript the gap between the two counts is bookkeeping
records that carry no `uuid`, plus any abandoned branches; the sample
has neither. Consecutive records on the chain link up:

``` python
assert all(b.parentUuid==a.uuid for a,b in zip(t, t[1:]))
```

The proof that the sample is a working session: resume it and ask about
the planted fact. The Claude Agent SDK drives the same CLI, so `resume`
there reads the same files. This spends tokens, so it is excluded from
automated tests.

``` python
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
opts = ClaudeAgentOptions(resume=sid, cwd=str(proj), model='haiku')
async for m in query(prompt='What is the flux reading? Reply with only the value.', options=opts):
    if isinstance(m, ResultMessage): print(m.result)
```

    41.7 kilofinches

## Cleanup

Remove the sample from `~/.claude/projects`, along with the scratch
project.

``` python
shutil.rmtree(sess_dir(proj))
shutil.rmtree(proj)
```
