llmsurgery 0.0.1__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.
llmsurgery/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
llmsurgery/_modidx.py ADDED
@@ -0,0 +1,166 @@
1
+ # Autogenerated by nbdev
2
+
3
+ d = { 'settings': { 'branch': 'main',
4
+ 'doc_baseurl': '/llmsurgery',
5
+ 'doc_host': 'https://AnswerDotAI.github.io',
6
+ 'git_url': 'https://github.com/AnswerDotAI/llmsurgery',
7
+ 'lib_path': 'llmsurgery'},
8
+ 'syms': { 'llmsurgery.ant': { 'llmsurgery.ant.SessHits': ('ant.html#sesshits', 'llmsurgery/ant.py'),
9
+ 'llmsurgery.ant.SessHits.__repr__': ('ant.html#sesshits.__repr__', 'llmsurgery/ant.py'),
10
+ 'llmsurgery.ant._est_toks': ('ant.html#_est_toks', 'llmsurgery/ant.py'),
11
+ 'llmsurgery.ant._norm_user': ('ant.html#_norm_user', 'llmsurgery/ant.py'),
12
+ 'llmsurgery.ant._now': ('ant.html#_now', 'llmsurgery/ant.py'),
13
+ 'llmsurgery.ant._preview': ('ant.html#_preview', 'llmsurgery/ant.py'),
14
+ 'llmsurgery.ant._tr_txt': ('ant.html#_tr_txt', 'llmsurgery/ant.py'),
15
+ 'llmsurgery.ant._trunc_deep': ('ant.html#_trunc_deep', 'llmsurgery/ant.py'),
16
+ 'llmsurgery.ant._txts': ('ant.html#_txts', 'llmsurgery/ant.py'),
17
+ 'llmsurgery.ant.append_sess': ('ant.html#append_sess', 'llmsurgery/ant.py'),
18
+ 'llmsurgery.ant.canon': ('ant.html#canon', 'llmsurgery/ant.py'),
19
+ 'llmsurgery.ant.conv_recs': ('ant.html#conv_recs', 'llmsurgery/ant.py'),
20
+ 'llmsurgery.ant.cur_sess': ('ant.html#cur_sess', 'llmsurgery/ant.py'),
21
+ 'llmsurgery.ant.dlg2msgs': ('ant.html#dlg2msgs', 'llmsurgery/ant.py'),
22
+ 'llmsurgery.ant.dlg2sess': ('ant.html#dlg2sess', 'llmsurgery/ant.py'),
23
+ 'llmsurgery.ant.fork_sess': ('ant.html#fork_sess', 'llmsurgery/ant.py'),
24
+ 'llmsurgery.ant.load_sess': ('ant.html#load_sess', 'llmsurgery/ant.py'),
25
+ 'llmsurgery.ant.mk_rec': ('ant.html#mk_rec', 'llmsurgery/ant.py'),
26
+ 'llmsurgery.ant.mk_tr': ('ant.html#mk_tr', 'llmsurgery/ant.py'),
27
+ 'llmsurgery.ant.mk_tu': ('ant.html#mk_tu', 'llmsurgery/ant.py'),
28
+ 'llmsurgery.ant.msgs2dlg': ('ant.html#msgs2dlg', 'llmsurgery/ant.py'),
29
+ 'llmsurgery.ant.msgs2recs': ('ant.html#msgs2recs', 'llmsurgery/ant.py'),
30
+ 'llmsurgery.ant.rec_role': ('ant.html#rec_role', 'llmsurgery/ant.py'),
31
+ 'llmsurgery.ant.rec_txt': ('ant.html#rec_txt', 'llmsurgery/ant.py'),
32
+ 'llmsurgery.ant.recs2msgs': ('ant.html#recs2msgs', 'llmsurgery/ant.py'),
33
+ 'llmsurgery.ant.reid_recs': ('ant.html#reid_recs', 'llmsurgery/ant.py'),
34
+ 'llmsurgery.ant.save_sess': ('ant.html#save_sess', 'llmsurgery/ant.py'),
35
+ 'llmsurgery.ant.sess2dlg': ('ant.html#sess2dlg', 'llmsurgery/ant.py'),
36
+ 'llmsurgery.ant.sess_dir': ('ant.html#sess_dir', 'llmsurgery/ant.py'),
37
+ 'llmsurgery.ant.sess_file': ('ant.html#sess_file', 'llmsurgery/ant.py'),
38
+ 'llmsurgery.ant.sess_search': ('ant.html#sess_search', 'llmsurgery/ant.py'),
39
+ 'llmsurgery.ant.sess_thread': ('ant.html#sess_thread', 'llmsurgery/ant.py'),
40
+ 'llmsurgery.ant.show_recs': ('ant.html#show_recs', 'llmsurgery/ant.py'),
41
+ 'llmsurgery.ant.stable_uuid': ('ant.html#stable_uuid', 'llmsurgery/ant.py'),
42
+ 'llmsurgery.ant.strip_think': ('ant.html#strip_think', 'llmsurgery/ant.py'),
43
+ 'llmsurgery.ant.tool_turn': ('ant.html#tool_turn', 'llmsurgery/ant.py'),
44
+ 'llmsurgery.ant.trunc_tools': ('ant.html#trunc_tools', 'llmsurgery/ant.py')},
45
+ 'llmsurgery.dialog': { 'llmsurgery.dialog.Attachment': ('dialog.html#attachment', 'llmsurgery/dialog.py'),
46
+ 'llmsurgery.dialog.Attachment.__init__': ('dialog.html#attachment.__init__', 'llmsurgery/dialog.py'),
47
+ 'llmsurgery.dialog.Dialog': ('dialog.html#dialog', 'llmsurgery/dialog.py'),
48
+ 'llmsurgery.dialog.Dialog.__init__': ('dialog.html#dialog.__init__', 'llmsurgery/dialog.py'),
49
+ 'llmsurgery.dialog.Dialog._repr_markdown_': ( 'dialog.html#dialog._repr_markdown_',
50
+ 'llmsurgery/dialog.py'),
51
+ 'llmsurgery.dialog.Dialog.find_msg': ('dialog.html#dialog.find_msg', 'llmsurgery/dialog.py'),
52
+ 'llmsurgery.dialog.Dialog.mk_message': ('dialog.html#dialog.mk_message', 'llmsurgery/dialog.py'),
53
+ 'llmsurgery.dialog.Dialog.mk_messages': ('dialog.html#dialog.mk_messages', 'llmsurgery/dialog.py'),
54
+ 'llmsurgery.dialog.Dialog.remove_msgs': ('dialog.html#dialog.remove_msgs', 'llmsurgery/dialog.py'),
55
+ 'llmsurgery.dialog.Dialog.todict': ('dialog.html#dialog.todict', 'llmsurgery/dialog.py'),
56
+ 'llmsurgery.dialog.Message': ('dialog.html#message', 'llmsurgery/dialog.py'),
57
+ 'llmsurgery.dialog.Message.__deepcopy__': ('dialog.html#message.__deepcopy__', 'llmsurgery/dialog.py'),
58
+ 'llmsurgery.dialog.Message.__flds__': ('dialog.html#message.__flds__', 'llmsurgery/dialog.py'),
59
+ 'llmsurgery.dialog.Message.__init__': ('dialog.html#message.__init__', 'llmsurgery/dialog.py'),
60
+ 'llmsurgery.dialog.Message._neighbor': ('dialog.html#message._neighbor', 'llmsurgery/dialog.py'),
61
+ 'llmsurgery.dialog.Message._repr_markdown_': ( 'dialog.html#message._repr_markdown_',
62
+ 'llmsurgery/dialog.py'),
63
+ 'llmsurgery.dialog.Message.ai_res': ('dialog.html#message.ai_res', 'llmsurgery/dialog.py'),
64
+ 'llmsurgery.dialog.Message.clear_inp_cache': ( 'dialog.html#message.clear_inp_cache',
65
+ 'llmsurgery/dialog.py'),
66
+ 'llmsurgery.dialog.Message.clear_out_cache': ( 'dialog.html#message.clear_out_cache',
67
+ 'llmsurgery/dialog.py'),
68
+ 'llmsurgery.dialog.Message.content': ('dialog.html#message.content', 'llmsurgery/dialog.py'),
69
+ 'llmsurgery.dialog.Message.dlg': ('dialog.html#message.dlg', 'llmsurgery/dialog.py'),
70
+ 'llmsurgery.dialog.Message.flds': ('dialog.html#message.flds', 'llmsurgery/dialog.py'),
71
+ 'llmsurgery.dialog.Message.has_error': ('dialog.html#message.has_error', 'llmsurgery/dialog.py'),
72
+ 'llmsurgery.dialog.Message.insert_after': ('dialog.html#message.insert_after', 'llmsurgery/dialog.py'),
73
+ 'llmsurgery.dialog.Message.mk_attachment': ('dialog.html#message.mk_attachment', 'llmsurgery/dialog.py'),
74
+ 'llmsurgery.dialog.Message.msg_type': ('dialog.html#message.msg_type', 'llmsurgery/dialog.py'),
75
+ 'llmsurgery.dialog.Message.next': ('dialog.html#message.next', 'llmsurgery/dialog.py'),
76
+ 'llmsurgery.dialog.Message.output': ('dialog.html#message.output', 'llmsurgery/dialog.py'),
77
+ 'llmsurgery.dialog.Message.previous': ('dialog.html#message.previous', 'llmsurgery/dialog.py'),
78
+ 'llmsurgery.dialog.Message.rel_msg': ('dialog.html#message.rel_msg', 'llmsurgery/dialog.py'),
79
+ 'llmsurgery.dialog.Message.remove_from_dlg': ( 'dialog.html#message.remove_from_dlg',
80
+ 'llmsurgery/dialog.py'),
81
+ 'llmsurgery.dialog.Message.summ': ('dialog.html#message.summ', 'llmsurgery/dialog.py'),
82
+ 'llmsurgery.dialog.Message.update': ('dialog.html#message.update', 'llmsurgery/dialog.py'),
83
+ 'llmsurgery.dialog.add_id_hash': ('dialog.html#add_id_hash', 'llmsurgery/dialog.py'),
84
+ 'llmsurgery.dialog.code_output': ('dialog.html#code_output', 'llmsurgery/dialog.py'),
85
+ 'llmsurgery.dialog.displayobj': ('dialog.html#displayobj', 'llmsurgery/dialog.py'),
86
+ 'llmsurgery.dialog.get_msg': ('dialog.html#get_msg', 'llmsurgery/dialog.py'),
87
+ 'llmsurgery.dialog.get_output_mds': ('dialog.html#get_output_mds', 'llmsurgery/dialog.py'),
88
+ 'llmsurgery.dialog.mk_code_output': ('dialog.html#mk_code_output', 'llmsurgery/dialog.py'),
89
+ 'llmsurgery.dialog.mk_displayobj': ('dialog.html#mk_displayobj', 'llmsurgery/dialog.py'),
90
+ 'llmsurgery.dialog.mk_output': ('dialog.html#mk_output', 'llmsurgery/dialog.py'),
91
+ 'llmsurgery.dialog.prompt_output': ('dialog.html#prompt_output', 'llmsurgery/dialog.py'),
92
+ 'llmsurgery.dialog.ruuid4': ('dialog.html#ruuid4', 'llmsurgery/dialog.py')},
93
+ 'llmsurgery.hist': { 'llmsurgery.hist.Message._xml_content': ('hist.html#message._xml_content', 'llmsurgery/hist.py'),
94
+ 'llmsurgery.hist.Message.ai_output': ('hist.html#message.ai_output', 'llmsurgery/hist.py'),
95
+ 'llmsurgery.hist.Message.local_time': ('hist.html#message.local_time', 'llmsurgery/hist.py'),
96
+ 'llmsurgery.hist.Message.media_extra': ('hist.html#message.media_extra', 'llmsurgery/hist.py'),
97
+ 'llmsurgery.hist.Message.prep_img': ('hist.html#message.prep_img', 'llmsurgery/hist.py'),
98
+ 'llmsurgery.hist.Message.prompt_txt': ('hist.html#message.prompt_txt', 'llmsurgery/hist.py'),
99
+ 'llmsurgery.hist.Message.render_out': ('hist.html#message.render_out', 'llmsurgery/hist.py'),
100
+ 'llmsurgery.hist.Message.to_media': ('hist.html#message.to_media', 'llmsurgery/hist.py'),
101
+ 'llmsurgery.hist.Message.to_parts': ('hist.html#message.to_parts', 'llmsurgery/hist.py'),
102
+ 'llmsurgery.hist.Message.to_xml': ('hist.html#message.to_xml', 'llmsurgery/hist.py'),
103
+ 'llmsurgery.hist.Message.todict': ('hist.html#message.todict', 'llmsurgery/hist.py'),
104
+ 'llmsurgery.hist._MissError': ('hist.html#_misserror', 'llmsurgery/hist.py'),
105
+ 'llmsurgery.hist._explode': ('hist.html#_explode', 'llmsurgery/hist.py'),
106
+ 'llmsurgery.hist._img_id': ('hist.html#_img_id', 'llmsurgery/hist.py'),
107
+ 'llmsurgery.hist._img_item': ('hist.html#_img_item', 'llmsurgery/hist.py'),
108
+ 'llmsurgery.hist._img_output': ('hist.html#_img_output', 'llmsurgery/hist.py'),
109
+ 'llmsurgery.hist._media_atts': ('hist.html#_media_atts', 'llmsurgery/hist.py'),
110
+ 'llmsurgery.hist._media_unavailable': ('hist.html#_media_unavailable', 'llmsurgery/hist.py'),
111
+ 'llmsurgery.hist._media_xml': ('hist.html#_media_xml', 'llmsurgery/hist.py'),
112
+ 'llmsurgery.hist._mime_kind': ('hist.html#_mime_kind', 'llmsurgery/hist.py'),
113
+ 'llmsurgery.hist._mime_supported': ('hist.html#_mime_supported', 'llmsurgery/hist.py'),
114
+ 'llmsurgery.hist._seq_tools': ('hist.html#_seq_tools', 'llmsurgery/hist.py'),
115
+ 'llmsurgery.hist.ai_fmt': ('hist.html#ai_fmt', 'llmsurgery/hist.py'),
116
+ 'llmsurgery.hist.dlg2canon': ('hist.html#dlg2canon', 'llmsurgery/hist.py'),
117
+ 'llmsurgery.hist.join_out': ('hist.html#join_out', 'llmsurgery/hist.py'),
118
+ 'llmsurgery.hist.media_item': ('hist.html#media_item', 'llmsurgery/hist.py'),
119
+ 'llmsurgery.hist.msgs2hist': ('hist.html#msgs2hist', 'llmsurgery/hist.py'),
120
+ 'llmsurgery.hist.render_output_ai': ('hist.html#render_output_ai', 'llmsurgery/hist.py'),
121
+ 'llmsurgery.hist.render_outputs_ai': ('hist.html#render_outputs_ai', 'llmsurgery/hist.py'),
122
+ 'llmsurgery.hist.to_local_time': ('hist.html#to_local_time', 'llmsurgery/hist.py'),
123
+ 'llmsurgery.hist.try_eval': ('hist.html#try_eval', 'llmsurgery/hist.py')},
124
+ 'llmsurgery.ipynb': { 'llmsurgery.ipynb.Dialog.cell2msg': ('ipynb.html#dialog.cell2msg', 'llmsurgery/ipynb.py'),
125
+ 'llmsurgery.ipynb.Dialog.from_cells': ('ipynb.html#dialog.from_cells', 'llmsurgery/ipynb.py'),
126
+ 'llmsurgery.ipynb.Dialog.write': ('ipynb.html#dialog.write', 'llmsurgery/ipynb.py'),
127
+ 'llmsurgery.ipynb.Message.cell_meta': ('ipynb.html#message.cell_meta', 'llmsurgery/ipynb.py'),
128
+ 'llmsurgery.ipynb.Message.to_cell': ('ipynb.html#message.to_cell', 'llmsurgery/ipynb.py'),
129
+ 'llmsurgery.ipynb._clean_out_meta': ('ipynb.html#_clean_out_meta', 'llmsurgery/ipynb.py'),
130
+ 'llmsurgery.ipynb._output_from_cell': ('ipynb.html#_output_from_cell', 'llmsurgery/ipynb.py'),
131
+ 'llmsurgery.ipynb.att2dict': ('ipynb.html#att2dict', 'llmsurgery/ipynb.py'),
132
+ 'llmsurgery.ipynb.dict2att': ('ipynb.html#dict2att', 'llmsurgery/ipynb.py'),
133
+ 'llmsurgery.ipynb.get_ipynb': ('ipynb.html#get_ipynb', 'llmsurgery/ipynb.py'),
134
+ 'llmsurgery.ipynb.ipynb_cells': ('ipynb.html#ipynb_cells', 'llmsurgery/ipynb.py'),
135
+ 'llmsurgery.ipynb.read_ipynb': ('ipynb.html#read_ipynb', 'llmsurgery/ipynb.py'),
136
+ 'llmsurgery.ipynb.split_cell_src': ('ipynb.html#split_cell_src', 'llmsurgery/ipynb.py'),
137
+ 'llmsurgery.ipynb.write_ipynb': ('ipynb.html#write_ipynb', 'llmsurgery/ipynb.py')},
138
+ 'llmsurgery.oai': { 'llmsurgery.oai.CodexAppServer': ('oai.html#codexappserver', 'llmsurgery/oai.py'),
139
+ 'llmsurgery.oai.CodexAppServer.__aenter__': ('oai.html#codexappserver.__aenter__', 'llmsurgery/oai.py'),
140
+ 'llmsurgery.oai.CodexAppServer.__aexit__': ('oai.html#codexappserver.__aexit__', 'llmsurgery/oai.py'),
141
+ 'llmsurgery.oai.CodexAppServer.__init__': ('oai.html#codexappserver.__init__', 'llmsurgery/oai.py'),
142
+ 'llmsurgery.oai.CodexAppServer._send': ('oai.html#codexappserver._send', 'llmsurgery/oai.py'),
143
+ 'llmsurgery.oai.CodexAppServer.close': ('oai.html#codexappserver.close', 'llmsurgery/oai.py'),
144
+ 'llmsurgery.oai.CodexAppServer.create_thread': ( 'oai.html#codexappserver.create_thread',
145
+ 'llmsurgery/oai.py'),
146
+ 'llmsurgery.oai.CodexAppServer.fork_thread': ('oai.html#codexappserver.fork_thread', 'llmsurgery/oai.py'),
147
+ 'llmsurgery.oai.CodexAppServer.inject_items': ('oai.html#codexappserver.inject_items', 'llmsurgery/oai.py'),
148
+ 'llmsurgery.oai.CodexAppServer.notify': ('oai.html#codexappserver.notify', 'llmsurgery/oai.py'),
149
+ 'llmsurgery.oai.CodexAppServer.request': ('oai.html#codexappserver.request', 'llmsurgery/oai.py'),
150
+ 'llmsurgery.oai.CodexAppServer.start': ('oai.html#codexappserver.start', 'llmsurgery/oai.py'),
151
+ 'llmsurgery.oai.CodexAppServer.start_thread': ('oai.html#codexappserver.start_thread', 'llmsurgery/oai.py'),
152
+ 'llmsurgery.oai._item_txts': ('oai.html#_item_txts', 'llmsurgery/oai.py'),
153
+ 'llmsurgery.oai._reid': ('oai.html#_reid', 'llmsurgery/oai.py'),
154
+ 'llmsurgery.oai.codex_call': ('oai.html#codex_call', 'llmsurgery/oai.py'),
155
+ 'llmsurgery.oai.codex_msg': ('oai.html#codex_msg', 'llmsurgery/oai.py'),
156
+ 'llmsurgery.oai.codex_output': ('oai.html#codex_output', 'llmsurgery/oai.py'),
157
+ 'llmsurgery.oai.curate_items': ('oai.html#curate_items', 'llmsurgery/oai.py'),
158
+ 'llmsurgery.oai.dlg2items': ('oai.html#dlg2items', 'llmsurgery/oai.py'),
159
+ 'llmsurgery.oai.dlg2thread': ('oai.html#dlg2thread', 'llmsurgery/oai.py'),
160
+ 'llmsurgery.oai.item_txt': ('oai.html#item_txt', 'llmsurgery/oai.py'),
161
+ 'llmsurgery.oai.load_rollout': ('oai.html#load_rollout', 'llmsurgery/oai.py'),
162
+ 'llmsurgery.oai.reid_items': ('oai.html#reid_items', 'llmsurgery/oai.py'),
163
+ 'llmsurgery.oai.response_items': ('oai.html#response_items', 'llmsurgery/oai.py'),
164
+ 'llmsurgery.oai.rollout_file': ('oai.html#rollout_file', 'llmsurgery/oai.py'),
165
+ 'llmsurgery.oai.strip_reasoning': ('oai.html#strip_reasoning', 'llmsurgery/oai.py'),
166
+ 'llmsurgery.oai.tool_turn': ('oai.html#tool_turn', 'llmsurgery/oai.py')}}}
llmsurgery/ant.py ADDED
@@ -0,0 +1,427 @@
1
+ """Read, write, and build Claude Code session transcripts
2
+
3
+ Docs: https://AnswerDotAI.github.io/llmsurgery/ant.html.md"""
4
+
5
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/03_ant.ipynb.
6
+
7
+ # %% auto #0
8
+ __all__ = ['SESSIONS', 'CC_VERSION', 'sess_dir', 'cur_sess', 'sess_file', 'canon', 'stable_uuid', 'mk_rec', 'save_sess',
9
+ 'append_sess', 'msgs2recs', 'mk_tu', 'mk_tr', 'tool_turn', 'load_sess', 'sess_thread', 'rec_txt',
10
+ 'conv_recs', 'rec_role', 'SessHits', 'sess_search', 'show_recs', 'strip_think', 'trunc_tools', 'reid_recs',
11
+ 'fork_sess', 'dlg2msgs', 'dlg2sess', 'recs2msgs', 'msgs2dlg', 'sess2dlg']
12
+
13
+ # %% ../nbs/03_ant.ipynb #09dc6bf6
14
+ import base64, json, os, re, uuid
15
+ from datetime import datetime, timezone
16
+ from fastcore.utils import *
17
+ from fastllm.anthropic import denorm_msgs
18
+ from fastllm.chat import Msg, Part, PartType, mk_msg, hist2fmt, data_url
19
+ from .hist import dlg2canon
20
+ from .dialog import *
21
+
22
+ # %% ../nbs/03_ant.ipynb #66bb9972
23
+ SESSIONS = Path.home()/'.claude'/'projects'
24
+
25
+ def sess_dir(
26
+ cwd=None, # Project directory; the current directory if None
27
+ ):
28
+ "The folder where Claude Code keeps session transcripts for the project at `cwd`"
29
+ return SESSIONS/re.sub(r'[^a-zA-Z0-9]', '-', str(Path(cwd or '.').resolve()))
30
+
31
+ # %% ../nbs/03_ant.ipynb #a86a78a2
32
+ def cur_sess(
33
+ cwd=None, # Project directory; the current directory if None
34
+ ):
35
+ "The current conversation's session id: the most recent transcript for the project at `cwd`, else the advertised id"
36
+ try: return max(sess_dir(cwd).glob('*.jsonl'), key=lambda p: p.stat().st_mtime).stem
37
+ except ValueError: return os.environ.get('CLAUDE_CODE_SESSION_ID')
38
+
39
+ # %% ../nbs/03_ant.ipynb #d031edb9
40
+ def sess_file(
41
+ sid=None, # Session id; `cur_sess(cwd)` if None
42
+ cwd=None, # Project directory; the current directory, then all projects, if None
43
+ ):
44
+ "Path to the transcript of session `sid` for the project at `cwd`"
45
+ sid = sid or cur_sess(cwd)
46
+ p = sess_dir(cwd)/f'{sid}.jsonl'
47
+ if cwd is not None or p.exists(): return p
48
+ return first(SESSIONS.glob(f'*/{sid}.jsonl')) or p
49
+
50
+ # %% ../nbs/03_ant.ipynb #5850201a
51
+ CC_VERSION = '2.1.206'
52
+
53
+ def canon(o):
54
+ "Canonical compact JSON for `o`, key-sorted, for stable hashing"
55
+ return json.dumps(o, sort_keys=True, separators=(',', ':'), ensure_ascii=False)
56
+
57
+ def stable_uuid(s):
58
+ "A uuid deterministically derived from string `s`"
59
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, s))
60
+
61
+ # %% ../nbs/03_ant.ipynb #aec44130
62
+ def _now(): return datetime.now(timezone.utc).strftime(r'%Y-%m-%dT%H:%M:%S.%f')[:-3]+'Z'
63
+
64
+ def _txts(o, skip=()):
65
+ if isinstance(o, str): yield o
66
+ elif isinstance(o, dict): yield from (t for k,v in o.items() if k not in skip for t in _txts(v, skip))
67
+ elif is_listy(o): yield from (t for x in o for t in _txts(x, skip))
68
+
69
+ def _est_toks(o):
70
+ "Rule-of-thumb token estimate for every string in `o`: words * 1.5"
71
+ return int(sum(len(s.split()) for s in _txts(o))*1.5)
72
+
73
+ # %% ../nbs/03_ant.ipynb #9da9f61e
74
+ def mk_rec(
75
+ role, # 'user' or 'assistant'
76
+ content, # A string, or a list of content blocks
77
+ cwd='.', # Project directory recorded in the envelope
78
+ uid=None, # Record uuid; random if None
79
+ ts=None, # ISO8601 timestamp; the current time if None
80
+ model='claude-sonnet-4-6', # Recorded in assistant API metadata; None omits it, so resume uses the user's default
81
+ input_toks=0, # `input_tokens` recorded in assistant usage, e.g. an estimate of the context so far
82
+ **kwargs, # Extra or overriding envelope fields, e.g. `isCompactSummary=True`
83
+ ):
84
+ "A transcript record for one conversation message, ready for `save_sess`"
85
+ uid = uid or str(uuid.uuid4())
86
+ if role=='user' and isinstance(content, list) and len(content)==1 and content[0].get('type')=='text': content = content[0]['text']
87
+ msg = dict(type='message', role=role, content=content)
88
+ r = dict(type=role, uuid=uid, parentUuid=None, sessionId=None, timestamp=ts or _now(), cwd=str(Path(cwd).resolve()),
89
+ version=CC_VERSION, gitBranch='HEAD', isSidechain=False, userType='external', permissionMode='default', message=msg)
90
+ if role=='assistant':
91
+ tu = isinstance(content, list) and any(isinstance(b, dict) and b.get('type')=='tool_use' for b in content)
92
+ r['requestId'] = 'req_'+stable_uuid(f'{uid}:req').replace('-', '')[:24]
93
+ usage = dict(input_tokens=input_toks, output_tokens=_est_toks(content), cache_creation_input_tokens=0, cache_read_input_tokens=0)
94
+ msg.update(id='msg_'+stable_uuid(f'{uid}:msg').replace('-', '')[:24],
95
+ stop_reason='tool_use' if tu else 'end_turn', stop_sequence=None, stop_details=None, usage=usage)
96
+ if model: msg['model'] = model
97
+ return dict(r, **kwargs)
98
+
99
+ # %% ../nbs/03_ant.ipynb #ba9f2c9f
100
+ def save_sess(
101
+ recs, # Records in conversation order, e.g. from `mk_rec`
102
+ sid=None, # Session id; a fresh uuid if None
103
+ cwd=None, # Project directory; the current directory if None
104
+ ts=None, # If given, stamp every record's timestamp: True for the current time, or an ISO8601 string
105
+ ):
106
+ "Chain `recs`, write them as session `sid` for the project at `cwd`, and return `sid`"
107
+ sid,prev = sid or str(uuid.uuid4()),None
108
+ for r in recs:
109
+ r['sessionId'],r['parentUuid'],prev = sid,prev,r['uuid']
110
+ if ts: r['timestamp'] = _now() if ts is True else ts
111
+ if 'session_id' in r: r['session_id'] = sid
112
+ f = sess_file(sid, cwd or '.')
113
+ f.parent.mkdir(parents=True, exist_ok=True)
114
+ f.write_text(''.join(json.dumps(r)+'\n' for r in recs))
115
+ return sid
116
+
117
+ # %% ../nbs/03_ant.ipynb #c161a097
118
+ def append_sess(
119
+ recs, # Records to append, e.g. a munged template round
120
+ sid=None, # Session to append to; `cur_sess()` if None
121
+ cwd=None, # Project directory; the current directory if None
122
+ ts=None, # If given, stamp each appended record's timestamp: True for the current time, or an ISO8601 string
123
+ ):
124
+ "Chain `recs` onto the tail of session `sid` and append them to its transcript, returning `sid`"
125
+ sid = sid or cur_sess(cwd)
126
+ prev = last(r['uuid'] for r in load_sess(sid, cwd) if 'uuid' in r)
127
+ for r in recs:
128
+ r['sessionId'],r['parentUuid'],prev = sid,prev,r['uuid']
129
+ if ts: r['timestamp'] = _now() if ts is True else ts
130
+ if 'session_id' in r: r['session_id'] = sid
131
+ with sess_file(sid, cwd).open('a') as f: f.writelines(json.dumps(r)+'\n' for r in recs)
132
+ return sid
133
+
134
+ # %% ../nbs/03_ant.ipynb #53b019df
135
+ def msgs2recs(
136
+ msgs, # Anthropic-style messages: dicts with `role` and `content`
137
+ key='', # Salt: the same messages and key give the same ids
138
+ cwd='.', # Project directory recorded in the envelopes
139
+ ts='2026-01-01T00:00:00.000Z', # Timestamp for every record
140
+ model='claude-sonnet-4-6', # Recorded in assistant API metadata; None omits it, so resume uses the user's default
141
+ **kwargs, # Extra envelope fields for every record, e.g. `entrypoint`
142
+ ):
143
+ "Deterministic transcript records for `msgs`, one record per message"
144
+ out,tot = [],0
145
+ for i,m in enumerate(msgs):
146
+ out.append(mk_rec(m['role'], m['content'], cwd=cwd, uid=stable_uuid(f'{key}:{i}:{canon(m)}'), ts=ts, model=model, input_toks=tot, **kwargs))
147
+ tot += _est_toks(m['content'])
148
+ return out
149
+
150
+ # %% ../nbs/03_ant.ipynb #fb2e37dc
151
+ def mk_tu(
152
+ name, # Tool name, as the transcript records it
153
+ input=None, # Tool arguments
154
+ tid=None, # tool_use id; random if None
155
+ ):
156
+ "A `tool_use` content block"
157
+ return dict(type='tool_use', id=tid or 'toolu_'+uuid.uuid4().hex[:24], name=name, input=input or {})
158
+
159
+ def mk_tr(
160
+ tu, # The `tool_use` block being answered
161
+ content, # The tool's output
162
+ ):
163
+ "The `tool_result` content block answering `tu`"
164
+ return dict(type='tool_result', tool_use_id=tu['id'], content=content)
165
+
166
+ def tool_turn(
167
+ prompt, # The user request
168
+ name, # Tool name
169
+ input, # Tool arguments
170
+ output, # Tool result
171
+ answer, # The assistant's closing text
172
+ **kwargs, # Passed to each `mk_rec`, e.g. `cwd`
173
+ ):
174
+ "A complete synthetic tool-use turn, as four records ready for `save_sess`"
175
+ tu = mk_tu(name, input)
176
+ return [mk_rec('user', prompt, **kwargs), mk_rec('assistant', [tu], **kwargs),
177
+ mk_rec('user', [mk_tr(tu, output)], **kwargs), mk_rec('assistant', [dict(type='text', text=answer)], **kwargs)]
178
+
179
+ # %% ../nbs/03_ant.ipynb #e3ef0c32
180
+ def load_sess(
181
+ sid=None, # Session id; the current session if None
182
+ cwd=None, # Project directory; the current directory if None
183
+ ):
184
+ "The records of session `sid`, as an `L` of attribute-access dicts"
185
+ return L(dict2obj(json.loads(l)) for l in sess_file(sid, cwd).read_text().splitlines())
186
+
187
+ # %% ../nbs/03_ant.ipynb #a5f38b3d
188
+ def sess_thread(
189
+ recs, # Session records, e.g. from `load_sess`
190
+ ):
191
+ "The records on the active conversation chain, walking `parentUuid` back from the last record"
192
+ byid = {r.uuid:r for r in recs if 'uuid' in r}
193
+ cur,res = recs.filter(lambda r: 'uuid' in r)[-1],[]
194
+ while cur is not None:
195
+ res.append(cur)
196
+ cur = byid.get(cur.get('parentUuid'))
197
+ return L(reversed(res))
198
+
199
+ # %% ../nbs/03_ant.ipynb #5bfa0829
200
+ def rec_txt(
201
+ r, # A session record
202
+ ):
203
+ "Every readable string in `r`'s message content, joined, for finding records by text"
204
+ return '\n'.join(_txts(obj2dict(r).get('message', {}).get('content', ''), skip=('type','id','tool_use_id','signature')))
205
+
206
+ # %% ../nbs/03_ant.ipynb #f9d47bbc
207
+ def conv_recs(
208
+ recs, # Session records, e.g. from `load_sess`
209
+ ):
210
+ "Just the records carrying conversation messages, dropping Claude Code's bookkeeping"
211
+ return L(r for r in recs if r.get('type') in ('user','assistant') and 'message' in r)
212
+
213
+ def rec_role(
214
+ r, # A session record
215
+ ):
216
+ "The conversational role of `r`: a user record carrying tool results counts as `tool`"
217
+ r = obj2dict(r)
218
+ c = r['message']['content']
219
+ if r['type']=='user' and isinstance(c, list) and any(b.get('type')=='tool_result' for b in c): return 'tool'
220
+ return r['type']
221
+
222
+ # %% ../nbs/03_ant.ipynb #defb1a03
223
+ def _preview(t, m, maxlen):
224
+ h = maxlen//2
225
+ s,e = max(0, m.start()-h), min(len(t), m.end()+h)
226
+ return ('…' if s else '') + t[s:e].replace('\n',' ') + ('…' if e<len(t) else '')
227
+
228
+ class SessHits(list):
229
+ "Search hits with a match-centered preview per line"
230
+ def __repr__(self): return '\n'.join(f"{h.i:5} {h.role:9} {h.ts[:16]} {h.preview}" for h in self)
231
+
232
+ def sess_search(
233
+ pat, # Regex to find in conversation text
234
+ sid=None, # Session id; the current session if None
235
+ cwd=None, # Project directory; the current directory if None
236
+ maxlen=160, # Preview characters shown around a hit's first match
237
+ ):
238
+ "Search every conversation record of a session, returning `SessHits` with the records on `.recs`"
239
+ recs = conv_recs(load_sess(sid, cwd))
240
+ res = SessHits()
241
+ for i,r in enumerate(recs):
242
+ if m := re.search(pat, t:=rec_txt(r)): res.append(AttrDict(i=i, role=rec_role(r), ts=r['timestamp'], preview=_preview(t, m, maxlen), rec=r))
243
+ res.recs = recs
244
+ return res
245
+
246
+ # %% ../nbs/03_ant.ipynb #58f5cf47
247
+ def show_recs(
248
+ recs, # Session records, e.g. a slice of `SessHits.recs`
249
+ mx=500, # Characters of text shown per record
250
+ ):
251
+ "A readable transcript of the conversation records in `recs`"
252
+ def _s(r):
253
+ t = rec_txt(r)
254
+ if len(t)>mx: t = t[:mx]+f'…[+{len(t)-mx} chars]'
255
+ return f"--- {rec_role(r)} {r['timestamp'][:19]} ---\n{t}"
256
+ return PrettyString('\n'.join(_s(r) for r in conv_recs(recs)))
257
+
258
+ # %% ../nbs/03_ant.ipynb #9e265e88
259
+ def strip_think(
260
+ recs, # Session records
261
+ ):
262
+ "Drop records whose message content is only `thinking` blocks; resume does not need them"
263
+ def _keep(r):
264
+ c = r.get('message', {}).get('content', '')
265
+ return isinstance(c, str) or not all(b.get('type')=='thinking' for b in c)
266
+ return L(recs).filter(_keep)
267
+
268
+ # %% ../nbs/03_ant.ipynb #a56fc348
269
+ def _trunc_deep(o, mx):
270
+ if isinstance(o, str): return o if len(o)<=mx else o[:mx]+f'…[+{len(o)-mx} chars]'
271
+ if isinstance(o, dict): return {k:_trunc_deep(v, mx) for k,v in o.items()}
272
+ if is_listy(o): return [_trunc_deep(x, mx) for x in o]
273
+ return o
274
+
275
+ def trunc_tools(
276
+ recs, # Session records
277
+ mx=2000, # Maximum characters per string in tool inputs and results
278
+ ):
279
+ "Copies of `recs` with strings in tool_use inputs and tool_result content truncated to `mx` characters"
280
+ recs = [obj2dict(r) for r in recs]
281
+ for r in recs:
282
+ c = r.get('message', {}).get('content', '')
283
+ if isinstance(c, str): continue
284
+ for b in c:
285
+ if b.get('type')=='tool_use': b['input'] = _trunc_deep(b.get('input', {}), mx)
286
+ elif b.get('type')=='tool_result' and 'content' in b: b['content'] = _trunc_deep(b['content'], mx)
287
+ return L(recs)
288
+
289
+ # %% ../nbs/03_ant.ipynb #7ffe1bb4
290
+ def reid_recs(
291
+ recs, # Records in conversation order
292
+ key='', # Salt: the same records and key give the same ids
293
+ ts=None, # If given, set every record's timestamp to this
294
+ ):
295
+ "Deterministically re-derive record uuids, tool_use ids, and API metadata, so one capture gives one file"
296
+ recs,ids = [obj2dict(r) for r in recs],{}
297
+ for i,r in enumerate(recs):
298
+ c = r.get('message', {}).get('content', '')
299
+ if not isinstance(c, str):
300
+ for j,b in enumerate(c):
301
+ if b.get('type')=='tool_use':
302
+ nid = 'toolu_'+stable_uuid(f'{key}:{i}:{j}').replace('-', '')[:24]
303
+ ids[b['id']],b['id'] = nid,nid
304
+ elif b.get('type')=='tool_result' and b.get('tool_use_id') in ids: b['tool_use_id'] = ids[b['tool_use_id']]
305
+ uid = stable_uuid(f'{key}:{i}')
306
+ if 'uuid' in r: ids[r['uuid']] = uid
307
+ r['uuid'] = uid
308
+ if ts: r['timestamp'] = ts
309
+ if r.get('type')=='assistant':
310
+ r['requestId'] = 'req_'+stable_uuid(f'{uid}:req').replace('-', '')[:24]
311
+ r['message']['id'] = 'msg_'+stable_uuid(f'{uid}:msg').replace('-', '')[:24]
312
+ for r in recs:
313
+ for k,v in r.items():
314
+ if isinstance(v, str) and v in ids: r[k] = ids[v]
315
+ return L(recs)
316
+
317
+ # %% ../nbs/03_ant.ipynb #6db09ecc
318
+ def fork_sess(
319
+ sid=None, # Session id to fork; `cur_sess()` if None
320
+ cwd=None, # Project directory; passed to `sess_file` via `load_sess`
321
+ mx=None, # If given, truncate tool input/output strings to `mx` characters
322
+ think=True, # Keep thinking records?
323
+ key=None, # If given, record and session ids re-derive deterministically from this salt
324
+ ):
325
+ "Write a munged copy of session `sid` under a fresh id, returning the new id to resume"
326
+ recs = sess_thread(load_sess(sid, cwd))
327
+ if not think: recs = strip_think(recs)
328
+ if mx: recs = trunc_tools(recs, mx)
329
+ if key: recs = reid_recs(recs, key)
330
+ return save_sess(list(recs), stable_uuid(key) if key else None, cwd)
331
+
332
+ # %% ../nbs/03_ant.ipynb #7b8d57fa
333
+ def dlg2msgs(
334
+ dlg, # A `Dialog`, ending with a prompt
335
+ aim_info=None, # Model capability dict for media handling; images enabled if None
336
+ ):
337
+ "Anthropic-style messages for `dlg`, with each reply's tool calls recovered as real blocks"
338
+ return denorm_msgs(dlg2canon(dlg, aim_info))
339
+
340
+ def dlg2sess(
341
+ dlg, # The dialog to convert
342
+ cwd=None, # Project directory for the session; the current directory if None
343
+ key='dlg2sess', # Salt for deterministic record ids
344
+ aim_info=None, # Model capability dict; images enabled if None
345
+ ):
346
+ "Write `dlg` as a Claude Code session for the project at `cwd`, returning the session id to resume"
347
+ recs = msgs2recs(dlg2msgs(dlg, aim_info), key=key, cwd=str(Path(cwd or '.').resolve()))
348
+ return save_sess(recs, stable_uuid(f'{key}:{dlg.name}'), cwd)
349
+
350
+ # %% ../nbs/03_ant.ipynb #16af5ac6
351
+ def _tr_txt(b):
352
+ c = b.get('content', '')
353
+ if isinstance(c, str): return c
354
+ if any(x.get('type') not in ('text','tool_reference') for x in c): raise ValueError('unsupported tool_result block')
355
+ return '\n'.join(x['text'] if x['type']=='text' else f"<tool_reference tool=\"{x['tool_name']}\"/>" for x in c)
356
+
357
+ def _norm_user(content):
358
+ if isinstance(content, str): return mk_msg(content)
359
+ items = []
360
+ for b in content:
361
+ if b['type']=='text': items.append(b['text'])
362
+ elif b['type']=='image': items.append(base64.b64decode(b['source']['data']))
363
+ else: raise ValueError(f"unsupported user block: {b['type']}")
364
+ return mk_msg(items)
365
+
366
+ def recs2msgs(
367
+ recs, # Session records, e.g. from `load_sess`
368
+ ):
369
+ "Canonical fastllm messages for the conversation records in `recs`"
370
+ msgs,names = [],{}
371
+ for r in recs:
372
+ r = obj2dict(r)
373
+ if r.get('type') not in ('user','assistant'): continue
374
+ c = r['message']['content']
375
+ if isinstance(c, str) and r['type']=='assistant': c = [dict(type='text', text=c)]
376
+ if r['type']=='assistant':
377
+ parts = []
378
+ for b in c:
379
+ if b['type']=='text': parts.append(Part(type=PartType.text, text=b['text']))
380
+ elif b['type']=='thinking': parts.append(Part(type=PartType.thinking, text=b.get('thinking','')))
381
+ elif b['type']=='tool_use':
382
+ names[b['id']] = b['name']
383
+ parts.append(Part(type=PartType.tool_use, data=dict(id=b['id'], name=b['name'], arguments=b.get('input', {}))))
384
+ else: raise ValueError(f"unsupported assistant block: {b['type']}")
385
+ msgs.append(Msg(role='assistant', content=parts))
386
+ elif rec_role(r)=='tool':
387
+ if not all(b.get('type')=='tool_result' for b in c): raise ValueError('record mixes tool_result with other blocks')
388
+ msgs.append(Msg(role='tool', content=[Part(type=PartType.tool_result, text=_tr_txt(b),
389
+ data=dict(id=b['tool_use_id'], name=names.get(b['tool_use_id']))) for b in c]))
390
+ else: msgs.append(_norm_user(c))
391
+ return msgs
392
+
393
+ # %% ../nbs/03_ant.ipynb #ac13833a
394
+ def msgs2dlg(
395
+ msgs, # Canonical messages, e.g. from `recs2msgs`
396
+ name, # Dialog name
397
+ cls=Dialog, # Dialog class to create
398
+ mx=2000, # Maximum characters per rendered tool input/output string; None disables truncation (see `hist2fmt`)
399
+ ):
400
+ "A dialog for `msgs`: one prompt per user turn, replies rendered in the format `fmt2hist` parses"
401
+ dlg,turns = cls(name),[]
402
+ for m in msgs:
403
+ if m.role=='user': turns.append((m,[]))
404
+ elif turns: turns[-1][1].append(m)
405
+ else: raise ValueError('msgs must start with a user message')
406
+ for u,rs in turns:
407
+ segs,atts = [],[]
408
+ for p in u.content:
409
+ if p.type==PartType.text: segs.append(p.text)
410
+ elif p.type==PartType.input_image:
411
+ mime,data = data_url(p.text)
412
+ atts.append(Attachment(base64.b64decode(data), mime))
413
+ segs.append(f'![](attachment:{atts[-1].id})')
414
+ else: raise ValueError(f'unsupported user part: {p.type}')
415
+ dlg.mk_message('\n\n'.join(segs), msg_type=sprompt, output=hist2fmt(rs, mx=mx), attachments=atts)
416
+ return dlg
417
+
418
+ # %% ../nbs/03_ant.ipynb #abc4a08a
419
+ def sess2dlg(
420
+ sid=None, # Session id; `cur_sess()` if None
421
+ cwd=None, # Project directory; passed to `sess_file` via `load_sess`
422
+ name=None, # Dialog name; the session id if None
423
+ mx=2000, # Maximum characters per rendered tool input/output string; None disables truncation (see `hist2fmt`)
424
+ ):
425
+ "The conversation of session `sid` as a dialog, one prompt per user turn"
426
+ recs = strip_think(sess_thread(load_sess(sid, cwd)))
427
+ return msgs2dlg(recs2msgs(recs), name or sid or cur_sess(), mx=mx)