from fastcore.test import *
from fastllm.chat import mk_msgcompact
llmsurgery.compact renders conversation history as concise text for continuing a long session. It keeps a record of the reasoning, decisions, tool calls, and evidence within token budgets. Renderers handle fastllm’s canonical messages and aidialog dialogs. You can also combine an earlier compacted segment with new history without truncating that segment again.
Compact DSL
We’ll use a small conversation built from fastllm’s canonical Msg and part objects. It includes Python, Bash, a generic tool, and a skill load. These examples show the compact DSL without running any tools or calling a model.
tool_result
def tool_result(
text, tool_name:NoneType=None
):Create a canonical tool-result message; pass tool_name to pair it with its tool_part call
python_str = 'files = ["a.py", "b.py"]'
bash_str = 'printf "ready\\n"\nls -la'
generic_args = {'path':'fixture.json', 'lines':[1, 2]}
res_str = 'first line\n\nsecond line\nfinal line'
python_part = tool_part('mcp__clikernel__execute', code=python_str)
bash_part = tool_part('Bash', command=bash_str)
generic_part = tool_part('inspect', **generic_args)turn1 = [mk_msg('List the project files, use the project skill, then run a shell check.'),
mk_msg(['I will inspect the project.', python_part], role='assistant'),
tool_result('files = ["a.py", "b.py"]', 'mcp__clikernel__execute'),
mk_msg([tool_part('Skill', skill='project-files')], role='assistant'),
tool_result('Skill loaded: project-files', 'Skill'),
mk_msg('Base directory for this skill: /skills/project-files\n\nInspect project files before editing.'),
mk_msg([bash_part], role='assistant'),
tool_result('ready', 'Bash')]turn2 = [mk_msg('Keep the agreed names; do not rename the fixture.'), # chkstyle: ignore-node
mk_msg(['Understood. I will verify the final state.', generic_part], role='assistant'),
tool_result(res_str, 'inspect'),
mk_msg('The fixture is short, readable, and ready for the renderer.', role='assistant')]Join the two synthetic exchanges to make our example conversation:
conversation = turn1 + turn2
len(conversation)12
turn1[Msg(role='user', content=[Text(raw=None, cache_control=None, text='List the project files, use the project skill, then run a shell check.', citations=None)], raw=None),
Msg(role='assistant', content=[Text(raw=None, cache_control=None, text='I will inspect the project.', citations=None), ToolUse(raw=None, cache_control=None, id='call_mcp__clikernel__execute', name='mcp__clikernel__execute', arguments={'code': 'files = ["a.py", "b.py"]'}, server=False, text=None)], raw=None),
Msg(role='tool', content=[ToolResult(raw=None, cache_control=None, id='call_mcp__clikernel__execute', name='mcp__clikernel__execute', arguments={}, server=False, text='files = ["a.py", "b.py"]')], raw=None),
Msg(role='assistant', content=[ToolUse(raw=None, cache_control=None, id='call_Skill', name='Skill', arguments={'skill': 'project-files'}, server=False, text=None)], raw=None),
Msg(role='tool', content=[ToolResult(raw=None, cache_control=None, id='call_Skill', name='Skill', arguments={}, server=False, text='Skill loaded: project-files')], raw=None),
Msg(role='user', content=[Text(raw=None, cache_control=None, text='Base directory for this skill: /skills/project-files\n\nInspect project files before editing.', citations=None)], raw=None),
Msg(role='assistant', content=[ToolUse(raw=None, cache_control=None, id='call_Bash', name='Bash', arguments={'command': 'printf "ready\\n"\nls -la'}, server=False, text=None)], raw=None),
Msg(role='tool', content=[ToolResult(raw=None, cache_control=None, id='call_Bash', name='Bash', arguments={}, server=False, text='ready')], raw=None)]
turn2[Msg(role='user', content=[Text(raw=None, cache_control=None, text='Keep the agreed names; do not rename the fixture.', citations=None)], raw=None),
Msg(role='assistant', content=[Text(raw=None, cache_control=None, text='Understood. I will verify the final state.', citations=None), ToolUse(raw=None, cache_control=None, id='call_inspect', name='inspect', arguments={'path': 'fixture.json', 'lines': [1, 2]}, server=False, text=None)], raw=None),
Msg(role='tool', content=[ToolResult(raw=None, cache_control=None, id='call_inspect', name='inspect', arguments={}, server=False, text='first line\n\nsecond line\nfinal line')], raw=None),
Msg(role='assistant', content=[Text(raw=None, cache_control=None, text='The fixture is short, readable, and ready for the renderer.', citations=None)], raw=None)]
We use token budgets to control the size of a compacted conversation. OpenAI’s o200k_base tokenizer gives us exact counts for that encoding and an estimate for models with other tokenizers. Truncation removes the middle: the beginning and end are often the interesting bits.
enc = compact_enc()trunctoks_mid
def trunctoks_mid(
s, max_toks, enc:NoneType=None, mark:str=' … '
):Truncate the middle of s to at most max_toks tokens.
Use compact_text for user or assistant prose. It encloses the text in the supplied marker and applies the token limit to the result. If the text is too long, it removes the middle:
compact_text
def compact_text(
s, mark, max_toks, enc:NoneType=None
):Render delimited prose within max_toks.
text = compact_text('beginning ' + 'middle ' * 100 + ' ending', '§', 20)
test(text, '§ beginning', str.startswith)
test(text, 'ending §', str.endswith)
assert len_toks(text) <= 20, text
print(text)§ beginning middle middle middle middle middle middle middle … middle middle middle middle middle middle ending §
compact_result prefixes a tool result with > and joins its non-empty lines with ¶. It strips whitespace around each line. Middle truncation keeps the formatted result within the token budget. An empty result contributes > alone.
Before formatting, it removes <warn> and <note> documentation-state blocks. Those checks run again after a restart. Retaining their old warnings would also give the resumed model examples of calling tools before reading their documentation.
A result enclosed in 𝍁 markers bypasses the token limit. The renderer removes the markers and keeps the content, subject to the same warning removal and line formatting. This follows fastllm’s FullResponse convention for output that is costly or impossible to regenerate. The markers preserve that instruction through serialization.
compact_result
def compact_result(
s, max_toks, enc:NoneType=None
):Render a tool result within max_toks; <warn>/<note> doc-state blocks are dropped, and a 𝍁-wrapped result is kept whole.
result = compact_result(res_str, 20)
empty = compact_result('', 20)
test_eq(result, '> first line ¶ second line ¶ final line')
test_eq(empty, '>')
print(result)
print(empty)> first line ¶ second line ¶ final line
>
Warning blocks disappear before rendering. A result containing only a note becomes >. The marked long result bypasses its 20-token budget:
warned = compact_result('<warn>\nRule violation: docs not read.\n</warn>\n' + res_str, 20)
test_eq(warned, result)
test_eq(compact_result('<note>\nA doc-state nudge.\n</note>', 20), '>')
full = compact_result('𝍁' + 'keep me whole ' * 30 + '𝍁', 20)
test_eq(full, '> ' + ('keep me whole ' * 30).strip())fenced_code wraps Python in a Markdown fence. It checks for lines that could close that fence and chooses a longer one when needed. Inline backticks do not require a longer fence. The token budget includes both fences and the code. A budget too small for the fences raises ValueError.
fenced_code
def fenced_code(
code, max_toks, enc:NoneType=None
):Render Python in a safe Markdown fence within max_toks.
simple = fenced_code(python_str, 40)
with_inline_backticks = fenced_code("s = '```text```'", 40)
test_eq(simple, f'```python\n{python_str}\n```')
test_eq(with_inline_backticks, "```python\ns = '```text```'\n```")
print(simple)```python
files = ["a.py", "b.py"]
```
A fence line inside the code requires a longer outer fence. Truncating the body still leaves both outer fences intact:
with_fence_line = fenced_code("before\n```\nafter", 40)
truncated = fenced_code(python_str * 20, 20)
test_eq(with_fence_line, "````python\nbefore\n```\nafter\n````")
test(truncated, '```python\n', str.startswith)
test(truncated, '\n```', str.endswith)
assert len_toks(truncated) <= 20, truncated
print(with_fence_line)````python
before
```
after
````
print(with_fence_line)````python
before
```
after
````
bash_call prefixes a command with !. It joins non-empty lines with ¶ and applies the token limit:
bash_call
def bash_call(
command, max_toks, enc:NoneType=None
):Render a Bash command within max_toks.
bash = bash_call(bash_str, 30)
test_eq(bash, '! printf "ready\\n" ¶ ls -la')
print(bash)! printf "ready\n" ¶ ls -la
generic_call uses ▶ name(args) for tools without a specialized renderer. It includes named arguments, replaces embedded newline escapes with ¶, and applies the token limit:
generic_call
def generic_call(
name, args, max_toks, enc:NoneType=None
):Render a generic tool call within max_toks.
call = generic_call(generic_part.name, generic_part.arguments, 40)
test_eq(call, "▶ inspect(path='fixture.json', lines=[1, 2])")
print(call)▶ inspect(path='fixture.json', lines=[1, 2])
compact_call chooses a renderer from call_renderers. Each entry names the renderer and the argument containing its input. Clikernel and Solveit’s py use fenced Python. Claude Code’s Bash and Solveit’s bash use the ! form.
Names can include the tools. prefix. Unregistered tools use ▶ name(args). A host can register another tool by adding a dictionary entry.
compact_call
def compact_call(
p, max_toks, enc:NoneType=None
):Render a tool-use part within max_toks, dispatching on call_renderers.
codex_part = tool_part('tools.mcp__clikernel__execute', code=python_str)
parts = [python_part, codex_part, bash_part, generic_part]
rendered = [compact_call(p, 40) for p in parts]
test_eq(rendered, [simple, simple, bash, call])
print('\n'.join(rendered))```python
files = ["a.py", "b.py"]
```
```python
files = ["a.py", "b.py"]
```
! printf "ready\n" ¶ ls -la
▶ inspect(path='fixture.json', lines=[1, 2])
Solveit’s py and bash use the same renderers as the other hosts. The mapping handles their argument names:
test_eq(compact_call(tool_part('bash', cmd=bash_str), 40), bash)
compact_call(tool_part('py', code=python_str), 40)'```python\nfiles = ["a.py", "b.py"]\n```'
Message rendering
User-role messages can contain host records as well as human requests. These helpers recognize slash commands, command output, hook output, system reminders, and injected skill text. The renderers and recent-prompt selection use the same checks.
compact_user encloses each text part in § markers. It omits host records such as slash commands and their output, hooks, and system reminders. These describe the old host state rather than the conversation to continue.
Injected skill text becomes a short instruction to load the skill again. Resumed models don’t reliably reread skills without a reminder. The document wrapper below also lists the exact Skill(...) calls found in the compacted history.
compact_user
def compact_user(
m, max_toks, enc:NoneType=None
):Render the text parts of a user message, dropping plumbing parts.
compact_user_part
def compact_user_part(
p, max_toks, enc:NoneType=None
):Render one user-text part, replacing injected skill contents.
user = compact_user(turn1[0], 100)
skill = compact_user(turn1[5], 100)
test_eq(user, '§ List the project files, use the project skill, then run a shell check. §')
test_eq(skill, '§ …skill text compacted: re-invoke before relying on it… §')
cmd = mk_msg('<command-name>/compact</command-name>\n<command-message>compact</command-message>')
test_eq(compact_user(cmd, 100), '')
test_eq(compact_user(mk_msg('<local-command-stdout>Compacted</local-command-stdout>'), 100), '')
print(user)
print(skill)§ List the project files, use the project skill, then run a shell check. §
§ …skill text compacted: re-invoke before relying on it… §
An assistant can explain what it’s doing, call a tool, then continue its explanation. compact_asst keeps those parts in order. Prose uses » markers and tool calls use compact_call:
compact_asst
def compact_asst(
m, text_toks, call_toks, enc:NoneType=None
):Render an assistant message.
compact_asst_part
def compact_asst_part(
p, text_toks, call_toks, enc:NoneType=None
):Render one assistant part.
assistant = compact_asst(turn1[1], 100, 40)
expected = f'» I will inspect the project. »\n{simple}'
test_eq(assistant, expected)
print(assistant)» I will inspect the project. »
```python
files = ["a.py", "b.py"]
```
compact_tool renders the result parts of a tool message. compact_msg selects the user, assistant, or tool renderer from the canonical message’s role.
compact_msg
def compact_msg(
m, user_toks, asst_toks, call_toks, result_toks, enc:NoneType=None
):Render one canonical message.
compact_tool
def compact_tool(
m, max_toks, enc:NoneType=None
):Render the result parts of a tool message.
policy = dict(user_toks=100, asst_toks=100, call_toks=40, result_toks=20)
rendered_turn1 = [compact_msg(m, **policy) for m in turn1]
test_eq(rendered_turn1[0], user)
test_eq(rendered_turn1[1], assistant)
test_eq(rendered_turn1[2], '> files = ["a.py", "b.py"]')
test_eq(rendered_turn1[5], skill)
print('\n'.join(rendered_turn1))§ List the project files, use the project skill, then run a shell check. §
» I will inspect the project. »
```python
files = ["a.py", "b.py"]
```
> files = ["a.py", "b.py"]
▶ Skill(skill='project-files')
> Skill loaded: project-files
§ …skill text compacted: re-invoke before relying on it… §
! printf "ready\n" ¶ ls -la
> ready
Conversation rendering
compact_chat starts a turn at each user-role message and separates non-empty turns with ***. By default, all text uses the supplied budgets.
Set last_n to protect recent exchanges. It lifts the prose limits for the last last_n human prompts and the final assistant text reply to each. Tool calls, tool results, and earlier assistant narration still use their normal budgets. A large file read near the end therefore cannot consume the whole compaction.
Host records such as /compact, its stdout, and context reports do not count as prompts. Injected skill bodies do not count either. Neither uses a last_n slot.
compact_chat
def compact_chat(
msgs, user_toks, asst_toks, call_toks, result_toks, enc:NoneType=None, last_n:int=0
):Render canonical messages as compact user-led turns.
Our fixture has three user-role messages: the first request, injected skill text, and the second request. The skill message starts an intermediate turn containing its replacement reminder.
compact = compact_chat(conversation, enc=enc, last_n=5, **policy)
test_eq(compact.count('\n\n***\n\n'), 2)
test('§ …skill text compacted: re-invoke before relying on it… §', compact, in_)
test('The fixture is short, readable, and ready for the renderer.', compact, in_)
plumbed = conversation + [mk_msg('<local-command-stdout>Compacted</local-command-stdout>'), cmd]
test_eq(compact_chat(plumbed, enc=enc, last_n=5, **policy), compact)
print(compact)§ List the project files, use the project skill, then run a shell check. §
» I will inspect the project. »
```python
files = ["a.py", "b.py"]
```
> files = ["a.py", "b.py"]
▶ Skill(skill='project-files')
> Skill loaded: project-files
***
§ …skill text compacted: re-invoke before relying on it… §
! printf "ready\n" ¶ ls -la
> ready
***
§ Keep the agreed names; do not rename the fixture. §
» Understood. I will verify the final state. »
▶ inspect(path='fixture.json', lines=[1, 2])
> first line ¶ second line ¶ final line
» The fixture is short, readable, and ready for the renderer. »
The standard policy gives user text 2,000 tokens, assistant prose 150, tool calls 60, and results 35. User decisions and corrections need more room because a resumed model cannot reconstruct them safely. Assistant prose and calls are easier to reproduce. Results retain a smaller sample of the evidence.
Recent-prompt protection is separate from these budgets. Request it with last_n; compact_chat defaults to zero.
We’ll use smaller budgets to show truncation in this short fixture. last_n=0 applies them to every message. These are demonstration values, not the standard policy.
small_policy = dict(user_toks=8, asst_toks=8, call_toks=8, result_toks=6)
small = compact_chat(conversation, last_n=0, **small_policy)
test(' … ', small, in_)
assert len_toks(small) < len_toks(compact)
print(small)§ List the … check. §
» I will inspect the project. »
```python
files … "]
```
> files … .py"]
▶ Skill(skill='project-files')
> Skill loaded: project-files
***
§ …skill … it… §
! printf " … ls -la
> ready
***
§ Keep the … fixture. §
» Understood … state. »
▶ inspect(path … 2])
> first … final line
» The fixture … renderer. »
With last_n=1, the final prompt and final reply retain their full text. The narration, tool call, and result between them still use the small budgets:
small1 = compact_chat(conversation, last_n=1, **small_policy)
test('§ Keep the agreed names; do not rename the fixture. §', small1, in_)
test('» The fixture is short, readable, and ready for the renderer. »', small1, in_)
test('» Understood … state. »', small1, in_)
test('> first … final line', small1, in_)
print(small1.split('***')[-1])
§ Keep the agreed names; do not rename the fixture. §
» Understood … state. »
▶ inspect(path … 2])
> first … final line
» The fixture is short, readable, and ready for the renderer. »
Compaction documents
A compaction document adds a legend and the path to the complete transcript. Use it to understand the earlier work, not as a source of executable code. Truncation and newline replacement change code and commands. Source files can also change after the transcript. Reopen the transcript or source file before reusing them.
compact_content
def compact_content(
chat, path, notes:str=''
):Wrap compact conversation text as a continuation document.
compact_content includes the legend, ## Conversation, and the transcript path. Pass notes to include an auto-generated continuation-notes section.
Choose the budgets and last_n when rendering the conversation body. This wrapper adds context for the reader without changing that body.
content = compact_content(small, '/tmp/session.jsonl')
test('## Conversation', content, in_)
test('/tmp/session.jsonl', content, in_)
print(content.split('\n\n## Conversation', 1)[0])This session is being continued from an earlier conversation. `§ … §` encloses user text; `» … »` encloses assistant text; fenced `python` is persistent-kernel execution; `!` marks Bash; `▶` marks another tool call; `>` marks its result; `¶` replaces a newline; `***` separates user-led turns; and ` … ` marks removed middle content. The final five messages are preserved untruncated. This representation is for orientation only: never copy code or commands from it; reopen the original transcript or source file first.
compact_body
def compact_body(
content
):Extract the conversation body from a compaction document.
compact_body extracts the conversation DSL without the wrapper or continuation notes. Repeated compaction can reuse this text as an unchanged earlier segment instead of truncating it again.
test_eq(compact_body(content), small)
with_notes = compact_content(small, '/tmp/session.jsonl', 'A useful note.')
test_eq(compact_body(with_notes), small)The document ends with instructions to reload skills whose contents the DSL omitted. It finds their names in ▶ Skill(...) calls, including calls in earlier unchanged segments. Repeated compaction therefore retains the combined skill list. compact_body removes these instructions with the rest of the wrapper.
tailed = compact_content(compact, '/tmp/session.jsonl')
test('re-invoke it: Skill(project-files)', tailed, in_)
test_eq(compact_body(tailed), compact)
assert 'Skill(' not in compact_content('§ hi §', '/tmp/session.jsonl')Use join_compacts to combine conversation segments with *** separators. It ignores empty segments, including the empty earlier-history segment of a session that has not yet been compacted.
joined = join_compacts('first', '', 'second')
test_eq(joined, 'first\n\n***\n\nsecond')
print(joined)first
***
second
Dialog compaction
In a dialog, the author has chosen which notes, code cells, and stored outputs to keep. Those cells explain the work. Hosts and notebook tools already provide ways to trim their outputs. For in-place compaction, we therefore concentrate on the AI replies inside prompt messages. A large user_toks allowance also caps unusually long questions.
Dialog.compact changes prompt messages in memory. It retains ids and leaves every other message type unchanged. The dialog’s structure already distinguishes requests from replies, without § and » markers or *** separators. Reply prose appears directly alongside compact tool calls and results. Authored code cells remain runnable and the message tools can still address them.
dlg2compact returns a separate text document instead. It includes role markers and turn separators because the text has no notebook structure. Its budgets also apply to notes and authored code. It does not change the dialog.
from aidialog.hist import chat2dlgchat2dlg converts our fixture to a dialog with one prompt per user turn. The replies include tool calls as fenced JSON blocks.
d = chat2dlg(conversation, 'demo')
test_eq([m.msg_type for m in d.messages], ['prompt']*3)
ddemo
- List the project files, use the project skill, then run a shell check. ⇒ [{‘output_type’: ‘display_data’, ‘metadata’: {‘is_ai_res’: …
- Base directory for this skill: /skills/project-files
Inspect project files before editing. ⇒ [{‘output_type’: ‘display_data’, ‘metadata’: {‘is_ai_res’: … - Keep the agreed names; do not rename the fixture. ⇒ [{‘output_type’: ‘display_data’, ‘metadata’: {‘is_ai_res’: …
A reply contains prose and fenced JSON tool blocks. fmt2hist parses it into canonical parts, as it does for dlg2chat. compact_reply then renders each call followed by its result, matched by id.
Prose has no marker by default because a prompt message already identifies the reply. Use mark='»' for a flat document that needs role markers. When a reply ends in a tool result, fmt2hist adds a '.' assistant-text placeholder to satisfy the wire format. The compact renderer omits that placeholder.
compact_reply
def compact_reply(
reply, asst_toks:NoneType=None, call_toks:NoneType=None, result_toks:NoneType=None, enc:NoneType=None,
mark:str=''
):Render a prompt’s reply in the DSL: prose (bare, or delimited by mark), each tool call followed by its result
creply = compact_reply(d.messages[0].ai_res)
test_eq(creply, '''I will inspect the project.
```python
files = ["a.py", "b.py"]
```
> files = ["a.py", "b.py"]
▶ Skill(skill='project-files')
> Skill loaded: project-files''')
assert '»' not in creply and '§' not in creplyPass mark to enclose reply prose in markers. Calls and results remain together:
test_eq(compact_reply(d.messages[0].ai_res, mark='»').splitlines()[0], '» I will inspect the project. »')
print(compact_reply(d.messages[1].ai_res))! printf "ready\n" ¶ ls -la
> ready
Message.compact changes prompt messages in place. It middle-truncates the question to user_toks and replaces the reply with its DSL rendering. Other message types return unchanged. Omitted budgets use compact_policy.
Message.compact
def compact(
user_toks:NoneType=None, asst_toks:NoneType=None, call_toks:NoneType=None, result_toks:NoneType=None,
enc:NoneType=None
):Compact a prompt message in place: cap the question, re-render the reply in the DSL; other types are left untouched
m = d.messages[0]
mid = m.id
m.compact()
test_eq(m.content, 'List the project files, use the project skill, then run a shell check.')
test_eq(m.ai_res, creply)
test_eq(m.id, mid)
nte = d.mk_message('A curated note with **important** rationale.', msg_type=snote)
test_eq(nte.compact(), nte)
test_eq(nte.content, 'A curated note with **important** rationale.')
m2facec68:p:List the project files, use the project skill, then run a shell check. > I will inspect the project.¶python¶files = ["a.py", "b.py"]¶¶> files = [“a.py”, “b.py”]¶▶ Skill(skill=‘project-files’)¶> Skill loaded: project-files
Dialog.compact applies Message.compact to every message and returns the dialog. Here the small reply budgets make truncation visible. We raise user_toks to 100 to keep the example questions intact. The ids remain unchanged and the result passes validation.
Dialog.compact
def compact(
user_toks:NoneType=None, asst_toks:NoneType=None, call_toks:NoneType=None, result_toks:NoneType=None,
enc:NoneType=None
):Compact every prompt message in place; the rest of the notebook is untouched
d2 = chat2dlg(conversation, 'demo2')
ids = [m.id for m in d2.messages]
befor = sum(len_toks(m.ai_res) for m in d2.messages)
d2.compact(**dict(small_policy, user_toks=100))
test_eq([m.id for m in d2.messages], ids)
test_eq(d2.messages[2].content, 'Keep the agreed names; do not rename the fixture.')
assert sum(len_toks(m.ai_res) for m in d2.messages) < befor
test(' … ', d2.messages[2].ai_res, in_)
d2.validate()
print(d2.messages[2].ai_res)Understood. … final state.
▶ inspect(path … 2])
> first … final line
The fixture is … the renderer.
For pasting a dialog into another conversation, dlg2compact produces a single text document. It reads the messages directly. dlg2chat serves a different purpose: preparing wire messages with XML for notes and code, including ids, times, and metadata. The compact document omits that XML.
compact_code renders an authored code cell with a #_id Solveit link, fenced source, and a result line. The source uses call_toks and its output uses result_toks. The id distinguishes authored cells from Python tool calls, which also use fences.
compact_code
def compact_code(
m, call_toks:NoneType=None, result_toks:NoneType=None, enc:NoneType=None
):Render a code message: #_id, its source fenced, and its output as a result line
cm = Message('files = ["a.py", "b.py"]', msg_type=scode, output=code_output('2 files'))
ccode = compact_code(cm)
test_eq(ccode.splitlines()[0], f'#_{cm.id}')
test('```python', ccode, in_)
test_eq(ccode.splitlines()[-1], '> 2 files')
print(ccode)#_938cff6d
```python
files = ["a.py", "b.py"]
```
> 2 files
dlg2compact encloses notes and prompt questions in § markers. Replies use » and code cells use fences. Each prompt ends a turn, including the notes and cells before it.
Skipped messages and raw messages with rec_kind metadata do not appear in the document. These exclusions match the conversion to wire messages.
dlg2compact
def dlg2compact(
dlg, user_toks:NoneType=None, asst_toks:NoneType=None, call_toks:NoneType=None, result_toks:NoneType=None,
enc:NoneType=None
):Render a whole dialog as one compact document, breaking turns after each prompt
In this mixed dialog, the note contributes user text without a reply. Reply prose has » markers. Only the authored code cell has an id link, and the document contains no wire XML:
d3 = chat2dlg(conversation, 'mixed')
d3.mk_message('## Project notes', msg_type=snote, before=d3.messages[0])
d3.mk_message('files = ["a.py", "b.py"]', msg_type=scode, output=code_output('2 files'), after=d3.messages[1])
doc3 = dlg2compact(d3)
test_eq(doc3.count('\n\n***\n\n'), 2)
test('§ ## Project notes §', doc3, in_)
test('» I will inspect the project. »', doc3, in_)
assert doc3.count('#_') == 1 and '<markdown' not in doc3
print(doc3[:420])§ ## Project notes §
§ List the project files, use the project skill, then run a shell check. §
» I will inspect the project. »
```python
files = ["a.py", "b.py"]
```
> files = ["a.py", "b.py"]
▶ Skill(skill='project-files')
> Skill loaded: project-files
***
#_102eeab2
```python
files = ["a.py", "b.py"]
```
> 2 files
§ Base directory for this skill: /skills/project-files
Inspect project files before editing. §
! p
Both forms lose information. After compaction, fmt2hist cannot recover the reply’s tool calls as structured parts. Neither dlg2chat nor reply2dlg can restore them.
In-place Dialog.compact preserves authored cells and their outputs. The notebook remains runnable. Flat dlg2compact leaves the input untouched but truncates authored content in its returned text. Results enclosed in 𝍁 bypass the token limit in both forms, though result formatting still changes line breaks and removes warning blocks.
Keep the original file and compact a copy when you need the full history.