python-codex 0.1.3__py3-none-any.whl → 0.1.5__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.
@@ -330,11 +330,11 @@ class ApplyPatchTool(BaseTool):
330
330
  buckets = {"A": [], "M": [], "D": []}
331
331
  for path, status in summaries.items():
332
332
  buckets[status].append(path.relative_to(self._workspace_root).as_posix())
333
- lines = ["Success. Updated the following files:"]
333
+ lines = ["Success:"]
334
334
  for status in ("A", "M", "D"):
335
335
  for rel_path in sorted(buckets[status]):
336
336
  lines.append(f"{status} {rel_path}")
337
- return "\n".join(lines) + "\n"
337
+ return " ".join(lines) + "\n"
338
338
 
339
339
  def _format_result(self, output: 'str', exit_code: 'int') -> 'str':
340
340
  return (
pycodex/utils/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from .dotenv import DOTENV_FILENAME, load_codex_dotenv, parse_dotenv, parse_dotenv_value
2
2
  from .get_env import build_user_agent, get_shell_name, get_timezone_name
3
3
  from .random_ids import uuid7_string
4
+ from .compactor import DEFAULT_COMPACT_PROMPT, SUMMARY_PREFIX, compact
4
5
  from .visualize import (
5
6
  CliSessionView,
6
7
  Spinner,
@@ -18,7 +19,9 @@ from .visualize import (
18
19
 
19
20
  __all__ = [
20
21
  "CliSessionView",
22
+ "DEFAULT_COMPACT_PROMPT",
21
23
  "DOTENV_FILENAME",
24
+ "SUMMARY_PREFIX",
22
25
  "Spinner",
23
26
  "build_user_agent",
24
27
  "build_cli_spinner_frame",
@@ -36,5 +39,6 @@ __all__ = [
36
39
  "short_id",
37
40
  "shorten_title",
38
41
  "summarize_tool_event",
42
+ "compact",
39
43
  "uuid7_string",
40
44
  ]
@@ -0,0 +1,189 @@
1
+ from dataclasses import dataclass
2
+
3
+ from ..protocol import AssistantMessage, ConversationItem, ModelStreamEvent, UserMessage
4
+ from .random_ids import uuid7_string
5
+ import typing
6
+
7
+ if typing.TYPE_CHECKING:
8
+ from ..agent import AgentLoop
9
+
10
+ DEFAULT_COMPACT_PROMPT = """You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
11
+
12
+ Include:
13
+ - Current progress and key decisions made
14
+ - Important context, constraints, or user preferences
15
+ - What remains to be done (clear next steps)
16
+ - Any critical data, examples, or references needed to continue
17
+
18
+ Be concise, structured, and focused on helping the next LLM seamlessly continue the work."""
19
+
20
+ SUMMARY_PREFIX = (
21
+ "Another language model started to solve this problem and produced a summary "
22
+ "of its thinking process. You also have access to the state of the tools "
23
+ "that were used by that language model. Use this to build on the work that "
24
+ "has already been done and avoid duplicating work. Here is the summary "
25
+ "produced by the other language model, use the information in this summary "
26
+ "to assist with your own analysis:"
27
+ )
28
+
29
+ COMPACT_USER_MESSAGE_MAX_TOKENS = 20_000
30
+ _APPROX_CHARS_PER_TOKEN = 4
31
+ _SUBAGENT_NOTIFICATION_PREFIX = "<subagent_notification>\n"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class CompactResult:
36
+ history: 'typing.Tuple[ConversationItem, ...]'
37
+ original_item_count: 'int'
38
+
39
+ @property
40
+ def retained_item_count(self) -> 'int':
41
+ return max(len(self.history) - 1, 0)
42
+
43
+ def display_text(self) -> 'str':
44
+ retained_label = _pluralize("item", self.retained_item_count)
45
+ original_label = _pluralize("item", self.original_item_count)
46
+ return (
47
+ f"compact({self.original_item_count} {original_label}) -> "
48
+ f"{self.retained_item_count} {retained_label} + [summary]"
49
+ )
50
+
51
+
52
+ def compact(
53
+ history: 'typing.Sequence[ConversationItem]',
54
+ ) -> 'typing.Tuple[ConversationItem, ...]':
55
+ summary_text = _build_summary_message(_last_assistant_message(history))
56
+ user_messages = collect_user_messages(history)
57
+ return build_compacted_history(user_messages, summary_text)
58
+
59
+
60
+ async def compact_agent_loop(
61
+ agent_loop: 'AgentLoop',
62
+ stream_event_handler: 'typing.Union[typing.Callable[[ModelStreamEvent], None], None]' = None,
63
+ ) -> 'typing.Union[CompactResult, None]':
64
+ history = agent_loop.history
65
+ if not history:
66
+ return None
67
+ original_item_count = len(history)
68
+
69
+ compact_prompt = UserMessage(text=DEFAULT_COMPACT_PROMPT)
70
+ prompt = agent_loop._context_manager.build_prompt(
71
+ list(history) + [compact_prompt],
72
+ [],
73
+ False,
74
+ turn_id=uuid7_string(),
75
+ )
76
+ noop_stream_event_handler = lambda _event: None
77
+ response = await agent_loop._model_client.complete(
78
+ prompt,
79
+ stream_event_handler or noop_stream_event_handler,
80
+ )
81
+ compacted_history = compact(
82
+ list(history) + [compact_prompt] + list(response.items)
83
+ )
84
+ agent_loop.replace_history(compacted_history)
85
+ rollout_recorder = agent_loop._rollout_recorder
86
+ if rollout_recorder is not None:
87
+ rollout_recorder.append_compacted_history(compacted_history)
88
+ return CompactResult(
89
+ history=compacted_history,
90
+ original_item_count=original_item_count,
91
+ )
92
+
93
+
94
+ def collect_user_messages(
95
+ history: 'typing.Sequence[ConversationItem]',
96
+ ) -> 'typing.Tuple[str, ...]':
97
+ compact_prompt = _normalize_for_compare(DEFAULT_COMPACT_PROMPT)
98
+ collected: 'typing.List[str]' = []
99
+ for item in history:
100
+ if not isinstance(item, UserMessage):
101
+ continue
102
+ if is_summary_message(item.text):
103
+ continue
104
+ if _normalize_for_compare(item.text) == compact_prompt:
105
+ continue
106
+ if _is_synthetic_user_message(item.text):
107
+ continue
108
+ collected.append(item.text)
109
+ return tuple(collected)
110
+
111
+
112
+ def is_summary_message(message: 'str') -> 'bool':
113
+ return message.startswith(f"{SUMMARY_PREFIX}\n")
114
+
115
+
116
+ def build_compacted_history(
117
+ user_messages: 'typing.Sequence[str]',
118
+ summary_text: 'str',
119
+ max_tokens: 'int' = COMPACT_USER_MESSAGE_MAX_TOKENS,
120
+ ) -> 'typing.Tuple[ConversationItem, ...]':
121
+ selected_messages: 'typing.List[str]' = []
122
+ if max_tokens > 0:
123
+ remaining = max_tokens
124
+ for message in reversed(tuple(user_messages)):
125
+ if remaining <= 0:
126
+ break
127
+ tokens = _approx_token_count(message)
128
+ if tokens <= remaining:
129
+ selected_messages.append(message)
130
+ remaining -= tokens
131
+ continue
132
+ selected_messages.append(_truncate_text_to_tokens(message, remaining))
133
+ break
134
+ selected_messages.reverse()
135
+
136
+ compacted: 'typing.List[ConversationItem]' = [
137
+ UserMessage(text=message) for message in selected_messages
138
+ ]
139
+ compacted.append(UserMessage(text=summary_text or _build_summary_message(None)))
140
+ return tuple(compacted)
141
+
142
+
143
+ def _last_assistant_message(
144
+ history: 'typing.Sequence[ConversationItem]',
145
+ ) -> 'typing.Union[str, None]':
146
+ for item in reversed(tuple(history)):
147
+ if isinstance(item, AssistantMessage):
148
+ return item.text
149
+ return None
150
+
151
+
152
+ def _build_summary_message(summary_text: 'typing.Union[str, None]') -> 'str':
153
+ normalized = (summary_text or "").strip() or "(no summary available)"
154
+ return f"{SUMMARY_PREFIX}\n{normalized}"
155
+
156
+
157
+ def _approx_token_count(text: 'str') -> 'int':
158
+ if not text:
159
+ return 0
160
+ return max(1, (len(text) + _APPROX_CHARS_PER_TOKEN - 1) // _APPROX_CHARS_PER_TOKEN)
161
+
162
+
163
+ def _truncate_text_to_tokens(text: 'str', max_tokens: 'int') -> 'str':
164
+ if max_tokens <= 0:
165
+ return ""
166
+ max_chars = max(max_tokens, 1) * _APPROX_CHARS_PER_TOKEN
167
+ if len(text) <= max_chars:
168
+ return text
169
+
170
+ removed_tokens = _approx_token_count(text[max_chars:])
171
+ suffix = f"\n...[{removed_tokens} tokens truncated]..."
172
+ available = max_chars - len(suffix)
173
+ if available <= 0:
174
+ return suffix.lstrip()
175
+ return text[:available].rstrip() + suffix
176
+
177
+
178
+ def _normalize_for_compare(text: 'str') -> 'str':
179
+ return "\n".join(line.rstrip() for line in text.strip().splitlines()).strip()
180
+
181
+
182
+ def _pluralize(noun: 'str', count: 'int') -> 'str':
183
+ if count == 1:
184
+ return noun
185
+ return f"{noun}s"
186
+
187
+
188
+ def _is_synthetic_user_message(text: 'str') -> 'bool':
189
+ return text.startswith(_SUBAGENT_NOTIFICATION_PREFIX)