Skip to content

Commit 82b9ad4

Browse files
committed
Extract shared compaction helpers from duplicated compact logic
1 parent 2ec3bca commit 82b9ad4

3 files changed

Lines changed: 56 additions & 25 deletions

File tree

python_agent_harness/agent.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
from . import config
3939
from .models import Message, ToolCall
40-
from .prompts import read_prompt_file, user_prompt_texts
40+
from .prompts import compact_summary, compacted_messages, user_prompt_texts
4141
from .token_estimator import context_window_for, estimate_payload_tokens
4242
from .tools.base import PendingToolResult
4343

@@ -281,26 +281,18 @@ def compact(self) -> bool:
281281
self.session.compacting = True
282282
try:
283283
conversation = "\n\n".join(f"{m.role}: {m.text()}" for m in self.messages if m.text())
284-
system = read_prompt_file("compact.md")
285-
resp, _ = self.session.client.chat_sync(
286-
[Message(role="user", content=conversation)],
287-
system=system,
288-
cancel_check=self._is_cancelled,
284+
summary = compact_summary(
285+
self.session.client, conversation, cancel_check=self._is_cancelled
289286
)
290-
summary = resp.text_without_reasoning()
291287
if not summary:
292288
return False
293-
frame = config.COMPACT_HEADER + summary + config.COMPACT_SEPARATOR
294289
# The summary replaces the whole conversation history EXCEPT
295290
# the system prompt (self.system is passed separately and
296291
# stays untouched): it is part of the user turn, never a
297292
# system message. Every real user prompt (nudges and other
298293
# harness-injected messages excluded) is preserved verbatim
299294
# after the frame, so the model keeps the actual requests.
300-
self.messages = [
301-
Message(role="user", content=frame.strip()),
302-
*[Message(role="user", content=p) for p in prompts],
303-
]
295+
self.messages = compacted_messages(summary, prompts)
304296
# The shared conversation now is the compacted one: mirror it
305297
# onto session.last_messages so the TUI (renders from it) and
306298
# a later manual /compact start from the summary, not the old

python_agent_harness/agent_session.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -715,8 +715,7 @@ def compact_conversation(self) -> tuple[bool, str]:
715715
manual command just replaces the history and waits for the next
716716
user message.
717717
"""
718-
from .models import Message as Msg
719-
from .prompts import read_prompt_file, user_prompt_texts
718+
from .prompts import compact_summary, compacted_messages, user_prompt_texts
720719

721720
# Replacing the conversation is a new generation: invalidate any
722721
# worker still winding down from a cancelled run, or its
@@ -730,12 +729,9 @@ def compact_conversation(self) -> tuple[bool, str]:
730729
self.compacting = True
731730
try:
732731
conversation = self._conversation_text(messages)
733-
system = read_prompt_file("compact.md")
734-
resp, _ = self.client.chat_sync([Msg(role="user", content=conversation)], system=system)
735-
summary = resp.text_without_reasoning()
732+
summary = compact_summary(self.client, conversation)
736733
if not summary:
737734
return False, "Compaction failed: empty summary."
738-
frame = config.COMPACT_HEADER + summary + config.COMPACT_SEPARATOR
739735
# The summary is part of the user turn (the original system
740736
# prompt is passed separately and stays untouched), so it
741737
# replaces the history as a user message — matching the
@@ -744,10 +740,7 @@ def compact_conversation(self) -> tuple[bool, str]:
744740
# (nudges and other harness-injected messages excluded) is
745741
# preserved verbatim after the frame, so the model keeps
746742
# the actual requests.
747-
self.last_messages = [
748-
Msg(role="user", content=frame.strip()),
749-
*[Msg(role="user", content=p) for p in user_prompt_texts(messages)],
750-
]
743+
self.last_messages = compacted_messages(summary, user_prompt_texts(messages))
751744
self.auto_save(self.last_messages, self.system_prompt)
752745
self.notify("compact")
753746
return True, "Buffer compacted successfully."

python_agent_harness/prompts.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@
44
(agent/subagent/commands), strips YAML frontmatter, discovers skills
55
for the {{SKILLS}} placeholder, assembles the effective system prompt
66
from project context files + task-completion rules + agent prompt, and
7-
provides user_prompt_texts() for the compaction flow (summarize the
8-
conversation and rebuild the history with every user prompt preserved
9-
verbatim).
7+
provides the compaction flow helpers (summarize the conversation with
8+
the compact prompt and rebuild the history with every user prompt
9+
preserved verbatim), shared by the in-loop compaction and the manual
10+
/compact command.
1011
"""
1112

1213
from __future__ import annotations
1314

1415
import os
1516
import re
1617
import subprocess
18+
from collections.abc import Callable
1719
from pathlib import Path
20+
from typing import Any
1821

1922
from . import config
23+
from .models import Message
2024

2125

2226
def read_prompt_file(name: str) -> str:
@@ -450,3 +454,45 @@ def user_prompt_texts(messages: list) -> list[str]:
450454
continue
451455
prompts.append(text)
452456
return prompts
457+
458+
459+
def compact_summary(
460+
client: Any,
461+
conversation: str,
462+
cancel_check: Callable[[], bool] | None = None,
463+
) -> str | None:
464+
"""Ask the model to summarize *conversation* using the compact prompt.
465+
466+
Shared by the in-loop compaction (``AgentLoop.compact``) and the
467+
manual /compact command (``AgentSession.compact_conversation``).
468+
Returns the summary text with the reasoning preamble stripped, or
469+
None when the response carries no text. Client exceptions
470+
propagate to the caller, which owns the failure handling
471+
(log/notify/status message).
472+
"""
473+
system = read_prompt_file("compact.md")
474+
kwargs: dict[str, Any] = {}
475+
if cancel_check is not None:
476+
kwargs["cancel_check"] = cancel_check
477+
resp, _ = client.chat_sync(
478+
[Message(role="user", content=conversation)],
479+
system=system,
480+
**kwargs,
481+
)
482+
summary = resp.text_without_reasoning()
483+
return summary or None
484+
485+
486+
def compacted_messages(summary: str, prompts: list[str]) -> list[Message]:
487+
"""The post-compaction history: the summary frame as a user message
488+
followed by every preserved user prompt, oldest first.
489+
490+
The summary lives in the user turn (the system prompt is passed
491+
separately and stays untouched); *prompts* are the real user
492+
requests (see ``user_prompt_texts``) that must survive compaction.
493+
"""
494+
frame = (config.COMPACT_HEADER + summary + config.COMPACT_SEPARATOR).strip()
495+
return [
496+
Message(role="user", content=frame),
497+
*[Message(role="user", content=p) for p in prompts],
498+
]

0 commit comments

Comments
 (0)