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.
- pycodex/agent.py +51 -11
- pycodex/cli.py +109 -3
- pycodex/context.py +23 -0
- pycodex/model.py +362 -23
- pycodex/prompts/models.json +30 -0
- pycodex/tools/apply_patch_tool.py +2 -2
- pycodex/utils/__init__.py +4 -0
- pycodex/utils/compactor.py +189 -0
- pycodex/utils/session_persist.py +483 -0
- pycodex/utils/visualize.py +120 -6
- {python_codex-0.1.3.dist-info → python_codex-0.1.5.dist-info}/METADATA +18 -3
- {python_codex-0.1.3.dist-info → python_codex-0.1.5.dist-info}/RECORD +18 -16
- responses_server/app.py +4 -1
- responses_server/payload_processors.py +10 -1
- responses_server/stream_router.py +25 -6
- {python_codex-0.1.3.dist-info → python_codex-0.1.5.dist-info}/WHEEL +0 -0
- {python_codex-0.1.3.dist-info → python_codex-0.1.5.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.3.dist-info → python_codex-0.1.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
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 "
|
|
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)
|