codex-agent-framework 0.1.2__tar.gz → 0.1.4__tar.gz
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.
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/CHANGELOG.md +21 -0
- {codex_agent_framework-0.1.2/codex_agent_framework.egg-info → codex_agent_framework-0.1.4}/PKG-INFO +4 -2
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/README.md +2 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/__init__.py +4 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/__main__.py +1 -1
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/agent.py +21 -12
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/ai.py +1 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/builtin_commands.py +19 -0
- codex_agent_framework-0.1.4/codex_agent/chat.py +356 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/event.py +17 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4/codex_agent_framework.egg-info}/PKG-INFO +4 -2
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent_framework.egg-info/SOURCES.txt +1 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent_framework.egg-info/requires.txt +1 -1
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/pyproject.toml +2 -2
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/tests/test_agent.py +29 -3
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/tests/test_chat.py +4 -29
- codex_agent_framework-0.1.4/tests/test_cli.py +22 -0
- codex_agent_framework-0.1.2/codex_agent/chat.py +0 -146
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/LICENSE +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/MANIFEST.in +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/builtin_providers.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/builtin_tools.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/command.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/get_text/__init__.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/get_text/default_gitignore +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/get_text/get_text.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/get_text/simpler_get_text.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/get_webdriver.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/image.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/latex.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/memory.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/message.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/prompts/system_prompt.txt +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/provider.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/stream_utils.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/tool.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/utils.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/voice.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/worker.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent_framework.egg-info/entry_points.txt +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent_framework.egg-info/top_level.txt +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/setup.cfg +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/tests/test_ai.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/tests/test_events.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/tests/test_image_message.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/tests/test_memory.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/tests/test_messages.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/tests/test_utils.py +0 -0
- {codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/tests/test_worker.py +0 -0
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
5
|
This project loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning where practical.
|
|
6
|
+
## [0.1.4] - 2026-05-03
|
|
7
|
+
### Added
|
|
8
|
+
- Make the Textual REPL UI the default chat interface, with multiline input, subdued colors, visual speaker separators, step-end markers, and cleaner tool-call rendering.
|
|
9
|
+
- Add assistant turn and assistant step lifecycle events for UI and runtime integrations.
|
|
10
|
+
- Add a `/voice` command to show, toggle, or select the configured voice mode.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Remove the legacy prompt-toolkit chat UI and the temporary `--textual` CLI flag.
|
|
14
|
+
|
|
15
|
+
### Tests
|
|
16
|
+
- Add CLI coverage for starting the default chat UI and update chat status formatting tests.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## [0.1.3] - 2026-05-03
|
|
20
|
+
### Added
|
|
21
|
+
- Send the current session id as `prompt_cache_key` to the Codex backend, enabling server-side prompt cache reuse across calls from the same session.
|
|
22
|
+
|
|
23
|
+
### Tests
|
|
24
|
+
- Add regression coverage for prompt cache key propagation through agent response calls and Codex stream parameters.
|
|
25
|
+
|
|
26
|
+
|
|
6
27
|
## [0.1.2] - 2026-05-03
|
|
7
28
|
### Added
|
|
8
29
|
- Add a `/clear` terminal chat command that clears the screen without touching session history.
|
{codex_agent_framework-0.1.2/codex_agent_framework.egg-info → codex_agent_framework-0.1.4}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-agent-framework
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A lightweight event-driven Codex agent runtime.
|
|
5
5
|
Author: Baptiste
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,7 +26,6 @@ Requires-Dist: openai
|
|
|
26
26
|
Requires-Dist: openpyxl
|
|
27
27
|
Requires-Dist: pathspec
|
|
28
28
|
Requires-Dist: pillow
|
|
29
|
-
Requires-Dist: prompt_toolkit
|
|
30
29
|
Requires-Dist: pydub
|
|
31
30
|
Requires-Dist: PyPDF2
|
|
32
31
|
Requires-Dist: pynteract
|
|
@@ -36,6 +35,7 @@ Requires-Dist: regex
|
|
|
36
35
|
Requires-Dist: requests
|
|
37
36
|
Requires-Dist: rich
|
|
38
37
|
Requires-Dist: selenium
|
|
38
|
+
Requires-Dist: textual
|
|
39
39
|
Requires-Dist: tiktoken
|
|
40
40
|
Requires-Dist: trafilatura
|
|
41
41
|
Provides-Extra: dev
|
|
@@ -342,6 +342,8 @@ python -m build
|
|
|
342
342
|
The distribution includes prompt text files and `codex_agent/get_text/default_gitignore` through package data and `MANIFEST.in`.
|
|
343
343
|
|
|
344
344
|
## Recent changes
|
|
345
|
+
- `0.1.4`: make the Textual REPL UI the default, add multiline input, assistant turn/step events, cleaner tool-call rendering, and `/voice` configuration.
|
|
346
|
+
- `0.1.3`: pass the current session id as `prompt_cache_key` to the Codex backend for prompt cache reuse.
|
|
345
347
|
- `0.1.2`: add `/clear` in the terminal chat to clear the screen without changing session history.
|
|
346
348
|
- `0.1.1`: image observations are now validated before session persistence, preventing broken `ImageMessage` entries from poisoning later turns.
|
|
347
349
|
|
|
@@ -295,6 +295,8 @@ python -m build
|
|
|
295
295
|
The distribution includes prompt text files and `codex_agent/get_text/default_gitignore` through package data and `MANIFEST.in`.
|
|
296
296
|
|
|
297
297
|
## Recent changes
|
|
298
|
+
- `0.1.4`: make the Textual REPL UI the default, add multiline input, assistant turn/step events, cleaner tool-call rendering, and `/voice` configuration.
|
|
299
|
+
- `0.1.3`: pass the current session id as `prompt_cache_key` to the Codex backend for prompt cache reuse.
|
|
298
300
|
- `0.1.2`: add `/clear` in the terminal chat to clear the screen without changing session history.
|
|
299
301
|
- `0.1.1`: image observations are now validated before session persistence, preventing broken `ImageMessage` entries from poisoning later turns.
|
|
300
302
|
|
|
@@ -19,7 +19,7 @@ from .ai import AIClient
|
|
|
19
19
|
from .worker import TurnSummaryWorker
|
|
20
20
|
from .voice import VoiceProcessor
|
|
21
21
|
from .latex import LaTeXProcessor
|
|
22
|
-
from .event import AgentInterrupted, AgentInterruptedEvent, AudioPlaybackEvent, EventBus, MessageAddedEvent, ToolCallDoneEvent, ToolCallStartEvent
|
|
22
|
+
from .event import AgentInterrupted, AgentInterruptedEvent, AssistantStepEndEvent, AssistantStepStartEvent, AssistantTurnEndEvent, AssistantTurnStartEvent, AudioPlaybackEvent, EventBus, MessageAddedEvent, ToolCallDoneEvent, ToolCallStartEvent
|
|
23
23
|
from pynteract import Shell
|
|
24
24
|
from datetime import datetime
|
|
25
25
|
from threading import Event as ThreadEvent, Thread as BackgroundThread
|
|
@@ -906,6 +906,7 @@ class Agent:
|
|
|
906
906
|
message=self.ai.run(
|
|
907
907
|
messages=messages,
|
|
908
908
|
tools=self.get_response_tools(),
|
|
909
|
+
prompt_cache_key=self.current_session_id,
|
|
909
910
|
**self.config.extract('model','reasoning_effort','verbosity')
|
|
910
911
|
)
|
|
911
912
|
self.add_message(message)
|
|
@@ -1055,15 +1056,24 @@ class Agent:
|
|
|
1055
1056
|
|
|
1056
1057
|
self.clear_interrupt()
|
|
1057
1058
|
self.age_context_messages()
|
|
1059
|
+
turn_message = None
|
|
1060
|
+
self.emit(AssistantTurnStartEvent)
|
|
1058
1061
|
try:
|
|
1059
1062
|
while True:
|
|
1060
1063
|
self.check_interrupt()
|
|
1061
1064
|
self.dump_pending()
|
|
1062
|
-
message=
|
|
1063
|
-
|
|
1064
|
-
self.
|
|
1065
|
-
|
|
1066
|
-
|
|
1065
|
+
message = None
|
|
1066
|
+
called_tools = False
|
|
1067
|
+
self.emit(AssistantStepStartEvent)
|
|
1068
|
+
try:
|
|
1069
|
+
message=self.get_response()
|
|
1070
|
+
turn_message = message
|
|
1071
|
+
ThreadPool.join_all()
|
|
1072
|
+
self.check_interrupt()
|
|
1073
|
+
called_tools=self.call_tools(message)
|
|
1074
|
+
self.dump_pending()
|
|
1075
|
+
finally:
|
|
1076
|
+
self.emit(AssistantStepEndEvent, message=message, called_tools=called_tools)
|
|
1067
1077
|
|
|
1068
1078
|
if called_tools:
|
|
1069
1079
|
if self.config.get('auto_proceed',True):
|
|
@@ -1076,6 +1086,8 @@ class Agent:
|
|
|
1076
1086
|
self.dump_pending()
|
|
1077
1087
|
self.add_message(DeveloperMessage(content="The previous agent response was interrupted by the user."))
|
|
1078
1088
|
return False
|
|
1089
|
+
finally:
|
|
1090
|
+
self.emit(AssistantTurnEndEvent, message=turn_message)
|
|
1079
1091
|
|
|
1080
1092
|
def speak(self,text,**kwargs):
|
|
1081
1093
|
from .voice import silent_play
|
|
@@ -1167,13 +1179,10 @@ class Agent:
|
|
|
1167
1179
|
return self.process()
|
|
1168
1180
|
|
|
1169
1181
|
def interact(self):
|
|
1170
|
-
"""Starts
|
|
1171
|
-
|
|
1172
|
-
Uses Alt+Enter for multi-line input submission.
|
|
1173
|
-
"""
|
|
1182
|
+
"""Starts the interactive Textual chat UI."""
|
|
1174
1183
|
from .utils import set_root_path
|
|
1175
1184
|
import os
|
|
1176
1185
|
if os.environ.get('AGENT_ROOT_PATH') is None:
|
|
1177
1186
|
set_root_path(file=__file__)
|
|
1178
|
-
from .chat import
|
|
1179
|
-
|
|
1187
|
+
from .chat import Chat
|
|
1188
|
+
Chat(self).run()
|
|
@@ -289,6 +289,7 @@ class AIClient:
|
|
|
289
289
|
"reasoning": reasoning,
|
|
290
290
|
"reasoning_summary": "auto" if reasoning else None,
|
|
291
291
|
"include_reasoning": bool(reasoning),
|
|
292
|
+
"prompt_cache_key": params.get("prompt_cache_key"),
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
def _codex_event_to_message_chunk(self, event, tool_call_index=0):
|
|
@@ -134,3 +134,22 @@ def verbosity(value=None):
|
|
|
134
134
|
if value is None:
|
|
135
135
|
return f"verbosity: {get_agent().config.verbosity!r}"
|
|
136
136
|
return update_model_config(verbosity=value)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@command
|
|
140
|
+
def voice(value=None):
|
|
141
|
+
agent = get_agent()
|
|
142
|
+
if value is None:
|
|
143
|
+
state = "on" if agent.config.get("voice_enabled", False) else "off"
|
|
144
|
+
return f"voice: {state}; voice={agent.config.get('voice')!r}"
|
|
145
|
+
|
|
146
|
+
lowered = str(value).lower()
|
|
147
|
+
if lowered in ("on", "true", "yes", "1"):
|
|
148
|
+
agent.update_config(voice_enabled=True)
|
|
149
|
+
return f"Updated config: voice_enabled={agent.config.voice_enabled!r}"
|
|
150
|
+
if lowered in ("off", "false", "no", "0"):
|
|
151
|
+
agent.update_config(voice_enabled=False)
|
|
152
|
+
return f"Updated config: voice_enabled={agent.config.voice_enabled!r}"
|
|
153
|
+
|
|
154
|
+
agent.update_config(voice=str(value), voice_enabled=True)
|
|
155
|
+
return f"Updated config: voice={agent.config.voice!r}, voice_enabled={agent.config.voice_enabled!r}"
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from rich.text import Text
|
|
2
|
+
|
|
3
|
+
from .event import (
|
|
4
|
+
AssistantStepEndEvent,
|
|
5
|
+
AssistantStepStartEvent,
|
|
6
|
+
AssistantTurnEndEvent,
|
|
7
|
+
AssistantTurnStartEvent,
|
|
8
|
+
ResponseContentDeltaEvent,
|
|
9
|
+
ServerToolCallEvent,
|
|
10
|
+
ToolCallStartEvent,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
class Chat:
|
|
14
|
+
"""Textual-based chat UI styled as a quiet modern REPL."""
|
|
15
|
+
|
|
16
|
+
CSS = """
|
|
17
|
+
Screen {
|
|
18
|
+
background: #171a1f;
|
|
19
|
+
color: #c6c8c4;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#main {
|
|
23
|
+
height: 100%;
|
|
24
|
+
padding: 0 1;
|
|
25
|
+
background: #171a1f;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#conversation {
|
|
29
|
+
height: 1fr;
|
|
30
|
+
padding: 1 1;
|
|
31
|
+
background: #171a1f;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#status {
|
|
35
|
+
height: 1;
|
|
36
|
+
color: #7d8590;
|
|
37
|
+
background: #171a1f;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#input {
|
|
41
|
+
height: 5;
|
|
42
|
+
margin-top: 0;
|
|
43
|
+
padding: 0 1;
|
|
44
|
+
background: #1f242c;
|
|
45
|
+
color: #c6c8c4;
|
|
46
|
+
border: blank #1f242c;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
TextArea > .text-area--cursor {
|
|
50
|
+
background: #6f98bd;
|
|
51
|
+
color: #0d1117;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Footer {
|
|
55
|
+
background: #171a1f;
|
|
56
|
+
color: #7d8590;
|
|
57
|
+
}
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, agent):
|
|
61
|
+
self.agent = agent
|
|
62
|
+
self.app = None
|
|
63
|
+
self._seen_tool_calls = set()
|
|
64
|
+
self._agent_buffer = ""
|
|
65
|
+
self._pending_tools = []
|
|
66
|
+
self._transcript = []
|
|
67
|
+
|
|
68
|
+
def attach(self):
|
|
69
|
+
self.agent.on(AssistantTurnStartEvent, self._on_assistant_turn_start)
|
|
70
|
+
self.agent.on(AssistantStepStartEvent, self._on_assistant_step_start)
|
|
71
|
+
self.agent.on(AssistantStepEndEvent, self._on_assistant_step_end)
|
|
72
|
+
self.agent.on(AssistantTurnEndEvent, self._on_assistant_turn_end)
|
|
73
|
+
self.agent.on(ResponseContentDeltaEvent, self._on_content_delta)
|
|
74
|
+
self.agent.on(ToolCallStartEvent, self._on_tool_call_start)
|
|
75
|
+
self.agent.on(ServerToolCallEvent, self._on_server_tool_call)
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def run(self):
|
|
79
|
+
from textual.app import App, ComposeResult
|
|
80
|
+
from textual.containers import Vertical
|
|
81
|
+
from textual.widgets import Footer, RichLog, Static, TextArea
|
|
82
|
+
|
|
83
|
+
chat = self
|
|
84
|
+
|
|
85
|
+
class AgentTextualApp(App):
|
|
86
|
+
CSS = chat.CSS
|
|
87
|
+
TITLE = "Codex Agent"
|
|
88
|
+
BINDINGS = [
|
|
89
|
+
("ctrl+c", "interrupt", "Interrupt"),
|
|
90
|
+
("ctrl+l", "clear", "Clear"),
|
|
91
|
+
("ctrl+q", "quit", "Quit"),
|
|
92
|
+
("ctrl+s", "submit", "Submit"),
|
|
93
|
+
("alt+enter", "submit", "Submit"),
|
|
94
|
+
("ctrl+enter", "submit", "Submit"),
|
|
95
|
+
("f2", "submit", "Submit"),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
def compose(self) -> ComposeResult:
|
|
99
|
+
with Vertical(id="main"):
|
|
100
|
+
yield RichLog(id="conversation", wrap=True, highlight=False, markup=True)
|
|
101
|
+
yield Static("", id="status")
|
|
102
|
+
yield TextArea(
|
|
103
|
+
"",
|
|
104
|
+
placeholder="› message · Entrée = newline · Ctrl+S/F2 = envoyer · Ctrl+Q = quitter",
|
|
105
|
+
id="input",
|
|
106
|
+
show_line_numbers=False,
|
|
107
|
+
soft_wrap=True,
|
|
108
|
+
)
|
|
109
|
+
yield Footer()
|
|
110
|
+
|
|
111
|
+
def on_mount(self):
|
|
112
|
+
chat.app = self
|
|
113
|
+
chat.attach()
|
|
114
|
+
self.refresh_status()
|
|
115
|
+
self.query_one("#input", TextArea).focus()
|
|
116
|
+
self.write_info("Codex Agent prêt.")
|
|
117
|
+
|
|
118
|
+
def action_quit(self):
|
|
119
|
+
self.exit()
|
|
120
|
+
|
|
121
|
+
def action_submit(self):
|
|
122
|
+
input_widget = self.query_one("#input", TextArea)
|
|
123
|
+
prompt = input_widget.text
|
|
124
|
+
input_widget.load_text("")
|
|
125
|
+
command = prompt.strip().lower()
|
|
126
|
+
if not command:
|
|
127
|
+
return
|
|
128
|
+
if command in ["exit", "quit", "/exit", "/quit"]:
|
|
129
|
+
self.exit()
|
|
130
|
+
return
|
|
131
|
+
if command == "/clear":
|
|
132
|
+
self.action_clear()
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
self.write_user(prompt)
|
|
136
|
+
self.refresh_status()
|
|
137
|
+
self.run_worker(lambda: self._run_agent(prompt), thread=True)
|
|
138
|
+
|
|
139
|
+
def _run_agent(self, prompt):
|
|
140
|
+
try:
|
|
141
|
+
result = chat.agent(prompt)
|
|
142
|
+
if isinstance(result, str) and result:
|
|
143
|
+
self.call_from_thread(self.write_info, result)
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
self.call_from_thread(self.write_error, str(exc))
|
|
146
|
+
finally:
|
|
147
|
+
self.call_from_thread(self.refresh_status)
|
|
148
|
+
|
|
149
|
+
def action_clear(self):
|
|
150
|
+
chat._transcript.clear()
|
|
151
|
+
self.query_one("#conversation", RichLog).clear()
|
|
152
|
+
self.write_info("écran nettoyé · historique conservé")
|
|
153
|
+
self.refresh_status()
|
|
154
|
+
|
|
155
|
+
def action_interrupt(self):
|
|
156
|
+
chat.agent.interrupt("keyboard_interrupt")
|
|
157
|
+
self.write_error("interruption demandée")
|
|
158
|
+
|
|
159
|
+
def log_width(self):
|
|
160
|
+
# Keep visual separators a few columns narrower than the RichLog
|
|
161
|
+
# viewport: Textual reserves space for padding/scrollbar.
|
|
162
|
+
raw_width = self.query_one("#conversation", RichLog).size.width or 80
|
|
163
|
+
return max(32, raw_width - 6)
|
|
164
|
+
|
|
165
|
+
def speaker_rule(self, name, style):
|
|
166
|
+
width = self.log_width()
|
|
167
|
+
label = f" {name} "
|
|
168
|
+
available = max(2, width - len(label))
|
|
169
|
+
left = available // 2
|
|
170
|
+
right = available - left
|
|
171
|
+
line = "─" * left + label + "─" * right
|
|
172
|
+
return Text(line, style=style)
|
|
173
|
+
|
|
174
|
+
def qed_marker(self, style="#6f98bd"):
|
|
175
|
+
return Text(" " * max(0, self.log_width() - 1) + "▪", style=style)
|
|
176
|
+
|
|
177
|
+
def render_transcript(self):
|
|
178
|
+
log = self.query_one("#conversation", RichLog)
|
|
179
|
+
log.clear()
|
|
180
|
+
for entry in chat._transcript:
|
|
181
|
+
kind = entry[0]
|
|
182
|
+
if kind == "speaker":
|
|
183
|
+
_, name, style = entry
|
|
184
|
+
log.write(self.speaker_rule(name, style))
|
|
185
|
+
log.write("")
|
|
186
|
+
elif kind == "user":
|
|
187
|
+
log.write(Text(entry[1], style="#c6c8c4"))
|
|
188
|
+
elif kind == "agent":
|
|
189
|
+
log.write(Text(entry[1], style="#c6c8c4"))
|
|
190
|
+
elif kind == "tool":
|
|
191
|
+
_, name, call_id, status = entry
|
|
192
|
+
suffix = f" · {status}" if status else ""
|
|
193
|
+
call = f" · {call_id}" if call_id else ""
|
|
194
|
+
log.write(
|
|
195
|
+
Text("tool › ", style="bold #6f98bd")
|
|
196
|
+
+ Text(name or "tool", style="#c6c8c4")
|
|
197
|
+
+ Text(f"{call}{suffix}", style="#7d8590")
|
|
198
|
+
)
|
|
199
|
+
elif kind == "step_end":
|
|
200
|
+
log.write(self.qed_marker())
|
|
201
|
+
elif kind == "user_end":
|
|
202
|
+
log.write(self.qed_marker("#88b38a"))
|
|
203
|
+
elif kind == "info":
|
|
204
|
+
log.write(Text("info › ", style="bold #7d8590") + Text(str(entry[1]), style="#8b949e"))
|
|
205
|
+
elif kind == "error":
|
|
206
|
+
log.write(Text("error › ", style="bold #ff7b72") + Text(str(entry[1]), style="#ff7b72"))
|
|
207
|
+
|
|
208
|
+
def write_user(self, prompt):
|
|
209
|
+
username = chat.agent.config.get("username") or "Vous"
|
|
210
|
+
chat._transcript.append(("speaker", username, "#88b38a"))
|
|
211
|
+
chat._transcript.append(("user", prompt))
|
|
212
|
+
chat._transcript.append(("user_end",))
|
|
213
|
+
self.render_transcript()
|
|
214
|
+
|
|
215
|
+
def write_agent_start(self, name):
|
|
216
|
+
chat._agent_buffer = ""
|
|
217
|
+
chat._pending_tools.clear()
|
|
218
|
+
chat._seen_tool_calls.clear()
|
|
219
|
+
agent_name = name or chat.agent.config.get("name") or "Pandora"
|
|
220
|
+
chat._transcript.append(("speaker", agent_name, "#789fbe"))
|
|
221
|
+
self.render_transcript()
|
|
222
|
+
|
|
223
|
+
def start_agent_step(self):
|
|
224
|
+
self.render_transcript()
|
|
225
|
+
|
|
226
|
+
def write_agent_delta(self, delta):
|
|
227
|
+
chat._agent_buffer += delta
|
|
228
|
+
if chat._transcript and chat._transcript[-1][0] == "agent":
|
|
229
|
+
chat._transcript[-1] = ("agent", chat._agent_buffer)
|
|
230
|
+
else:
|
|
231
|
+
chat._transcript.append(("agent", chat._agent_buffer))
|
|
232
|
+
self.render_transcript()
|
|
233
|
+
|
|
234
|
+
def write_tool(self, name, call_id=None, status=None):
|
|
235
|
+
chat._pending_tools.append(("tool", name, call_id, status))
|
|
236
|
+
|
|
237
|
+
def end_agent_step(self):
|
|
238
|
+
had_content = bool(chat._agent_buffer or chat._pending_tools)
|
|
239
|
+
if chat._transcript and chat._transcript[-1][0] == "agent":
|
|
240
|
+
if chat._agent_buffer:
|
|
241
|
+
chat._transcript[-1] = ("agent", chat._agent_buffer)
|
|
242
|
+
else:
|
|
243
|
+
chat._transcript.pop()
|
|
244
|
+
if chat._pending_tools:
|
|
245
|
+
chat._transcript.extend(chat._pending_tools)
|
|
246
|
+
chat._pending_tools.clear()
|
|
247
|
+
if had_content and (not chat._transcript or chat._transcript[-1][0] != "step_end"):
|
|
248
|
+
chat._transcript.append(("step_end",))
|
|
249
|
+
self.render_transcript()
|
|
250
|
+
|
|
251
|
+
def write_info(self, text):
|
|
252
|
+
chat._transcript.append(("info", str(text)))
|
|
253
|
+
self.render_transcript()
|
|
254
|
+
|
|
255
|
+
def write_error(self, text):
|
|
256
|
+
chat._transcript.append(("error", str(text)))
|
|
257
|
+
self.render_transcript()
|
|
258
|
+
|
|
259
|
+
def refresh_status(self):
|
|
260
|
+
try:
|
|
261
|
+
status = chat.agent.context_status()
|
|
262
|
+
text = chat.format_status(status).plain
|
|
263
|
+
except Exception as exc:
|
|
264
|
+
text = f"status unavailable: {exc}"
|
|
265
|
+
self.query_one("#status", Static).update(text)
|
|
266
|
+
|
|
267
|
+
AgentTextualApp().run()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@classmethod
|
|
271
|
+
def format_status(cls, status):
|
|
272
|
+
percent = status.percent
|
|
273
|
+
style = "green"
|
|
274
|
+
if percent >= 90:
|
|
275
|
+
style = "bold red"
|
|
276
|
+
elif percent >= 75:
|
|
277
|
+
style = "yellow"
|
|
278
|
+
elif percent >= 50:
|
|
279
|
+
style = "cyan"
|
|
280
|
+
|
|
281
|
+
used = cls._format_int(status.used_tokens)
|
|
282
|
+
limit = cls._format_int(status.input_token_limit)
|
|
283
|
+
session = str(status.session_id or "-")[-15:]
|
|
284
|
+
compact_hint = " auto-compact:on" if status.auto_compact else " auto-compact:off"
|
|
285
|
+
if status.auto_compact and status.used_tokens >= status.auto_compact_threshold:
|
|
286
|
+
compact_hint = " auto-compact:due"
|
|
287
|
+
|
|
288
|
+
return Text.assemble(
|
|
289
|
+
("ctx ", "dim"),
|
|
290
|
+
(f"{percent:.1f}%", style),
|
|
291
|
+
(f" {used}/{limit} tok", "dim"),
|
|
292
|
+
(f" · hist {status.visible_history_messages}/{status.total_history_messages}", "dim"),
|
|
293
|
+
(f" · msgs {status.total_session_messages}", "dim"),
|
|
294
|
+
(f" · img {status.image_messages}", "dim"),
|
|
295
|
+
(f" · mem {status.memory_entries}", "dim"),
|
|
296
|
+
(f" · pending {status.pending_messages}", "dim"),
|
|
297
|
+
(f" · {session}", "dim"),
|
|
298
|
+
(compact_hint, "dim"),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
@staticmethod
|
|
302
|
+
def _format_int(value):
|
|
303
|
+
try:
|
|
304
|
+
return f"{int(value):,}".replace(",", " ")
|
|
305
|
+
except Exception:
|
|
306
|
+
return str(value)
|
|
307
|
+
|
|
308
|
+
def _call_app(self, method, *args):
|
|
309
|
+
if self.app is None:
|
|
310
|
+
return
|
|
311
|
+
callback = getattr(self.app, method)
|
|
312
|
+
try:
|
|
313
|
+
self.app.call_from_thread(callback, *args)
|
|
314
|
+
except RuntimeError:
|
|
315
|
+
callback(*args)
|
|
316
|
+
|
|
317
|
+
def _on_assistant_turn_start(self, event):
|
|
318
|
+
name = self.agent.config.get("name") or "Pandora"
|
|
319
|
+
self._call_app("write_agent_start", name)
|
|
320
|
+
|
|
321
|
+
def _on_assistant_step_start(self, event):
|
|
322
|
+
self._agent_buffer = ""
|
|
323
|
+
self._call_app("start_agent_step")
|
|
324
|
+
|
|
325
|
+
def _on_assistant_step_end(self, event):
|
|
326
|
+
self._call_app("end_agent_step")
|
|
327
|
+
self._call_app("refresh_status")
|
|
328
|
+
|
|
329
|
+
def _on_assistant_turn_end(self, event):
|
|
330
|
+
self._call_app("refresh_status")
|
|
331
|
+
|
|
332
|
+
def _on_content_delta(self, event):
|
|
333
|
+
self._call_app("write_agent_delta", event.delta)
|
|
334
|
+
|
|
335
|
+
def _on_tool_call_start(self, event):
|
|
336
|
+
tool_call = event.get("tool_call") or {}
|
|
337
|
+
name = tool_call.get("name") or "unknown"
|
|
338
|
+
call_id = tool_call.get("call_id") or name
|
|
339
|
+
self._print_tool_call(name, call_id)
|
|
340
|
+
|
|
341
|
+
def _on_server_tool_call(self, event):
|
|
342
|
+
name = event.get("name") or "unknown"
|
|
343
|
+
call_id = event.get("call_id") or name
|
|
344
|
+
status = event.get("status") or ""
|
|
345
|
+
self._print_tool_call(name, call_id, status=status)
|
|
346
|
+
|
|
347
|
+
def _print_tool_call(self, name, call_id, status=None):
|
|
348
|
+
if call_id in self._seen_tool_calls:
|
|
349
|
+
return
|
|
350
|
+
self._seen_tool_calls.add(call_id)
|
|
351
|
+
self._call_app("write_tool", name, call_id, status)
|
|
352
|
+
|
|
353
|
+
def _on_response_done(self, event):
|
|
354
|
+
# Tool call lines are flushed at assistant-turn end, not after each
|
|
355
|
+
# backend response, to keep agentic loops visually contiguous.
|
|
356
|
+
pass
|
|
@@ -16,6 +16,23 @@ class Event(modict):
|
|
|
16
16
|
self['type'] = snake_case_type(type(self))
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
class AssistantTurnStartEvent(Event):
|
|
20
|
+
message = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AssistantTurnEndEvent(Event):
|
|
24
|
+
message = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AssistantStepStartEvent(Event):
|
|
28
|
+
message = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AssistantStepEndEvent(Event):
|
|
32
|
+
message = None
|
|
33
|
+
called_tools = False
|
|
34
|
+
|
|
35
|
+
|
|
19
36
|
class ResponseStartEvent(Event):
|
|
20
37
|
message = None
|
|
21
38
|
|
{codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4/codex_agent_framework.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-agent-framework
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: A lightweight event-driven Codex agent runtime.
|
|
5
5
|
Author: Baptiste
|
|
6
6
|
License-Expression: MIT
|
|
@@ -26,7 +26,6 @@ Requires-Dist: openai
|
|
|
26
26
|
Requires-Dist: openpyxl
|
|
27
27
|
Requires-Dist: pathspec
|
|
28
28
|
Requires-Dist: pillow
|
|
29
|
-
Requires-Dist: prompt_toolkit
|
|
30
29
|
Requires-Dist: pydub
|
|
31
30
|
Requires-Dist: PyPDF2
|
|
32
31
|
Requires-Dist: pynteract
|
|
@@ -36,6 +35,7 @@ Requires-Dist: regex
|
|
|
36
35
|
Requires-Dist: requests
|
|
37
36
|
Requires-Dist: rich
|
|
38
37
|
Requires-Dist: selenium
|
|
38
|
+
Requires-Dist: textual
|
|
39
39
|
Requires-Dist: tiktoken
|
|
40
40
|
Requires-Dist: trafilatura
|
|
41
41
|
Provides-Extra: dev
|
|
@@ -342,6 +342,8 @@ python -m build
|
|
|
342
342
|
The distribution includes prompt text files and `codex_agent/get_text/default_gitignore` through package data and `MANIFEST.in`.
|
|
343
343
|
|
|
344
344
|
## Recent changes
|
|
345
|
+
- `0.1.4`: make the Textual REPL UI the default, add multiline input, assistant turn/step events, cleaner tool-call rendering, and `/voice` configuration.
|
|
346
|
+
- `0.1.3`: pass the current session id as `prompt_cache_key` to the Codex backend for prompt cache reuse.
|
|
345
347
|
- `0.1.2`: add `/clear` in the terminal chat to clear the screen without changing session history.
|
|
346
348
|
- `0.1.1`: image observations are now validated before session persistence, preventing broken `ImageMessage` entries from poisoning later turns.
|
|
347
349
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codex-agent-framework"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "A lightweight event-driven Codex agent runtime."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -34,7 +34,6 @@ dependencies = [
|
|
|
34
34
|
"openpyxl",
|
|
35
35
|
"pathspec",
|
|
36
36
|
"pillow",
|
|
37
|
-
"prompt_toolkit",
|
|
38
37
|
"pydub",
|
|
39
38
|
"PyPDF2",
|
|
40
39
|
"pynteract",
|
|
@@ -44,6 +43,7 @@ dependencies = [
|
|
|
44
43
|
"requests",
|
|
45
44
|
"rich",
|
|
46
45
|
"selenium",
|
|
46
|
+
"textual",
|
|
47
47
|
"tiktoken",
|
|
48
48
|
"trafilatura",
|
|
49
49
|
]
|
|
@@ -60,8 +60,12 @@ class FakeImageAI:
|
|
|
60
60
|
class FakeCodexClient:
|
|
61
61
|
def __init__(self, events):
|
|
62
62
|
self.events = events
|
|
63
|
+
self.args = None
|
|
64
|
+
self.kwargs = None
|
|
63
65
|
|
|
64
|
-
def stream(self, *
|
|
66
|
+
def stream(self, *args, **kwargs):
|
|
67
|
+
self.args = args
|
|
68
|
+
self.kwargs = kwargs
|
|
65
69
|
return iter(self.events)
|
|
66
70
|
|
|
67
71
|
|
|
@@ -290,6 +294,7 @@ def test_agent_get_response_emits_events_and_persists_assistant_message():
|
|
|
290
294
|
assert agent.session.messages[-1] is message
|
|
291
295
|
assert json.loads(json.dumps(message))["type"] == "assistant_message"
|
|
292
296
|
assert agent.ai.params["verbosity"] == agent.config.verbosity
|
|
297
|
+
assert agent.ai.params["prompt_cache_key"] == agent.current_session_id
|
|
293
298
|
assert any(tool["type"] == "custom" and tool["name"] == "python" for tool in agent.ai.params["tools"])
|
|
294
299
|
assert any(tool["type"] == "custom" and tool["name"] == "bash" for tool in agent.ai.params["tools"])
|
|
295
300
|
assert any(tool["type"] == "function" and tool["name"] == "read" for tool in agent.ai.params["tools"])
|
|
@@ -525,6 +530,7 @@ def test_agent_emits_server_tool_call_event_from_output_items():
|
|
|
525
530
|
)
|
|
526
531
|
|
|
527
532
|
assert seen == [("web_search", "ws_123", "completed")]
|
|
533
|
+
assert agent.ai._codex_client.kwargs["prompt_cache_key"] is None
|
|
528
534
|
|
|
529
535
|
|
|
530
536
|
def test_agent_persists_server_tool_output_items_as_messages():
|
|
@@ -1059,15 +1065,17 @@ def test_agent_can_enable_builtin_server_tools_from_config():
|
|
|
1059
1065
|
assert set(agent.server_tools) == {"web_search", "image_generation"}
|
|
1060
1066
|
|
|
1061
1067
|
|
|
1062
|
-
def
|
|
1068
|
+
def test_codex_stream_params_forward_verbosity_and_prompt_cache_key():
|
|
1063
1069
|
agent = Agent(session="new", voice_enabled=False, verbosity="low")
|
|
1064
1070
|
|
|
1065
1071
|
params = agent.ai._to_codex_stream_params({
|
|
1066
1072
|
"messages": agent.get_context(),
|
|
1067
1073
|
"verbosity": agent.config.verbosity,
|
|
1074
|
+
"prompt_cache_key": agent.current_session_id,
|
|
1068
1075
|
})
|
|
1069
1076
|
|
|
1070
1077
|
assert params["verbosity"] == "low"
|
|
1078
|
+
assert params["prompt_cache_key"] == agent.current_session_id
|
|
1071
1079
|
|
|
1072
1080
|
|
|
1073
1081
|
def test_agent_provider_messages_are_ephemeral():
|
|
@@ -1135,7 +1143,7 @@ def test_agent_command_supports_varargs_and_kwargs():
|
|
|
1135
1143
|
def test_agent_loads_builtin_commands():
|
|
1136
1144
|
agent = Agent(session="new", voice_enabled=False)
|
|
1137
1145
|
|
|
1138
|
-
assert {"help", "compact", "config", "model", "reasoning", "verbosity"}.issubset(agent.commands)
|
|
1146
|
+
assert {"help", "compact", "config", "model", "reasoning", "verbosity", "voice"}.issubset(agent.commands)
|
|
1139
1147
|
assert "/help" in agent("/help")
|
|
1140
1148
|
assert "/compact" in agent("/help")
|
|
1141
1149
|
|
|
@@ -1154,6 +1162,24 @@ def test_builtin_model_config_commands_update_and_persist(tmp_path, monkeypatch)
|
|
|
1154
1162
|
assert agent.config.verbosity == "high"
|
|
1155
1163
|
|
|
1156
1164
|
|
|
1165
|
+
def test_builtin_voice_command_shows_toggles_and_selects_voice(tmp_path, monkeypatch):
|
|
1166
|
+
monkeypatch.setattr(utils, "RUNTIME_DIR", str(tmp_path))
|
|
1167
|
+
agent = Agent(session="new", voice_enabled=False, voice="nova")
|
|
1168
|
+
|
|
1169
|
+
assert agent("/voice") == "voice: off; voice='nova'"
|
|
1170
|
+
assert agent("/voice on") == "Updated config: voice_enabled=True"
|
|
1171
|
+
assert agent.config.voice_enabled is True
|
|
1172
|
+
assert agent("/voice off") == "Updated config: voice_enabled=False"
|
|
1173
|
+
assert agent.config.voice_enabled is False
|
|
1174
|
+
assert agent("/voice shimmer") == "Updated config: voice='shimmer', voice_enabled=True"
|
|
1175
|
+
assert agent.config.voice == "shimmer"
|
|
1176
|
+
assert agent.config.voice_enabled is True
|
|
1177
|
+
|
|
1178
|
+
saved = json.loads((tmp_path / "agent_config.json").read_text())
|
|
1179
|
+
assert saved["voice"] == "shimmer"
|
|
1180
|
+
assert saved["voice_enabled"] is True
|
|
1181
|
+
|
|
1182
|
+
|
|
1157
1183
|
def test_builtin_config_command_shows_and_updates_multiple_model_fields(tmp_path, monkeypatch):
|
|
1158
1184
|
monkeypatch.setattr(utils, "RUNTIME_DIR", str(tmp_path))
|
|
1159
1185
|
agent = Agent(session="new", voice_enabled=False)
|
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
from rich.console import Console
|
|
2
|
-
|
|
3
1
|
from codex_agent import Agent
|
|
4
|
-
from codex_agent.chat import
|
|
2
|
+
from codex_agent.chat import Chat
|
|
5
3
|
from codex_agent.message import DeveloperMessage
|
|
6
4
|
|
|
7
5
|
|
|
8
|
-
class RecordingConsole(Console):
|
|
9
|
-
def __init__(self):
|
|
10
|
-
super().__init__(record=True, width=160)
|
|
11
|
-
self.clear_count = 0
|
|
12
|
-
|
|
13
|
-
def clear(self, *args, **kwargs):
|
|
14
|
-
self.clear_count += 1
|
|
15
|
-
return super().clear(*args, **kwargs)
|
|
16
|
-
|
|
17
|
-
|
|
18
6
|
def test_agent_context_status_is_lightweight_and_does_not_call_providers():
|
|
19
7
|
agent = Agent(session="new", voice_enabled=False, input_token_limit=10_000)
|
|
20
8
|
agent.session.messages.append(DeveloperMessage(content="hello context"))
|
|
@@ -36,28 +24,15 @@ def test_agent_context_status_is_lightweight_and_does_not_call_providers():
|
|
|
36
24
|
assert status.memory_entries == len(agent.memory.messages)
|
|
37
25
|
|
|
38
26
|
|
|
39
|
-
def
|
|
27
|
+
def test_chat_status_bar_formats_context_summary():
|
|
40
28
|
agent = Agent(session="new", voice_enabled=False, input_token_limit=10_000)
|
|
41
|
-
|
|
42
|
-
chat = TerminalChat(agent, console=console)
|
|
29
|
+
status = agent.context_status()
|
|
43
30
|
|
|
44
|
-
|
|
31
|
+
output = Chat.format_status(status).plain
|
|
45
32
|
|
|
46
|
-
output = console.export_text()
|
|
47
33
|
assert "ctx" in output
|
|
48
34
|
assert "%" in output
|
|
49
35
|
assert "tok" in output
|
|
50
36
|
assert "hist" in output
|
|
51
37
|
assert "msgs" in output
|
|
52
38
|
assert "mem" in output
|
|
53
|
-
|
|
54
|
-
def test_terminal_chat_clear_screen_does_not_touch_agent_session():
|
|
55
|
-
agent = Agent(session="new", voice_enabled=False)
|
|
56
|
-
initial_count = len(agent.session.messages)
|
|
57
|
-
console = RecordingConsole()
|
|
58
|
-
chat = TerminalChat(agent, console=console)
|
|
59
|
-
|
|
60
|
-
chat.clear_screen()
|
|
61
|
-
|
|
62
|
-
assert console.clear_count == 1
|
|
63
|
-
assert len(agent.session.messages) == initial_count
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from codex_agent import __main__
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FakeAgent:
|
|
5
|
+
instances = []
|
|
6
|
+
|
|
7
|
+
def __init__(self, **kwargs):
|
|
8
|
+
self.kwargs = kwargs
|
|
9
|
+
self.interacted = False
|
|
10
|
+
FakeAgent.instances.append(self)
|
|
11
|
+
|
|
12
|
+
def interact(self):
|
|
13
|
+
self.interacted = True
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_cli_starts_default_textual_chat(monkeypatch):
|
|
17
|
+
FakeAgent.instances.clear()
|
|
18
|
+
monkeypatch.setattr(__main__, "Agent", FakeAgent)
|
|
19
|
+
|
|
20
|
+
__main__.main([])
|
|
21
|
+
|
|
22
|
+
assert FakeAgent.instances[-1].interacted is True
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
from prompt_toolkit import PromptSession
|
|
2
|
-
from rich.console import Console
|
|
3
|
-
from rich.panel import Panel
|
|
4
|
-
from rich.rule import Rule
|
|
5
|
-
from rich.text import Text
|
|
6
|
-
from .event import ResponseContentDeltaEvent, ResponseDoneEvent, ResponseStartEvent, ServerToolCallEvent, ToolCallStartEvent
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TerminalChat:
|
|
10
|
-
"""Small terminal UI wired to agent events."""
|
|
11
|
-
|
|
12
|
-
def __init__(self, agent, console=None):
|
|
13
|
-
self.agent = agent
|
|
14
|
-
self.console = console or Console()
|
|
15
|
-
self.prompt_session = PromptSession(multiline=True)
|
|
16
|
-
self._in_response = False
|
|
17
|
-
self._has_content = False
|
|
18
|
-
self._seen_tool_calls = set()
|
|
19
|
-
|
|
20
|
-
def attach(self):
|
|
21
|
-
self.agent.on(ResponseStartEvent, self._on_response_start)
|
|
22
|
-
self.agent.on(ResponseContentDeltaEvent, self._on_content_delta)
|
|
23
|
-
self.agent.on(ToolCallStartEvent, self._on_tool_call_start)
|
|
24
|
-
self.agent.on(ServerToolCallEvent, self._on_server_tool_call)
|
|
25
|
-
self.agent.on(ResponseDoneEvent, self._on_response_done)
|
|
26
|
-
return self
|
|
27
|
-
|
|
28
|
-
def run(self):
|
|
29
|
-
self.attach()
|
|
30
|
-
self.console.print(Panel.fit(
|
|
31
|
-
"[bold]Codex Agent[/bold]\n[dim]Alt+Enter submits multiline input. Type exit or quit to stop.[/dim]",
|
|
32
|
-
border_style="cyan",
|
|
33
|
-
))
|
|
34
|
-
while True:
|
|
35
|
-
try:
|
|
36
|
-
self.print_status_bar()
|
|
37
|
-
prompt = self.prompt_session.prompt(
|
|
38
|
-
[("class:prompt", "You > ")],
|
|
39
|
-
prompt_continuation="... ",
|
|
40
|
-
)
|
|
41
|
-
command = prompt.strip().lower()
|
|
42
|
-
if command in ["exit", "quit", "/exit", "/quit"]:
|
|
43
|
-
self.console.print("[dim]Bye.[/dim]")
|
|
44
|
-
break
|
|
45
|
-
if command == "/clear":
|
|
46
|
-
self.clear_screen()
|
|
47
|
-
continue
|
|
48
|
-
if not prompt.strip():
|
|
49
|
-
continue
|
|
50
|
-
try:
|
|
51
|
-
result = self.agent(prompt)
|
|
52
|
-
if isinstance(result, str) and result:
|
|
53
|
-
self.console.print(result, style="dim")
|
|
54
|
-
except KeyboardInterrupt:
|
|
55
|
-
self.agent.interrupt("keyboard_interrupt")
|
|
56
|
-
self.console.print("\n[dim]Interrupted.[/dim]")
|
|
57
|
-
self.console.print()
|
|
58
|
-
except (KeyboardInterrupt, EOFError):
|
|
59
|
-
self.console.print("\n[dim]Bye.[/dim]")
|
|
60
|
-
break
|
|
61
|
-
|
|
62
|
-
def print_status_bar(self):
|
|
63
|
-
try:
|
|
64
|
-
status = self.agent.context_status()
|
|
65
|
-
except Exception as exc:
|
|
66
|
-
self.console.print(f"[dim red]status unavailable: {exc}[/dim red]")
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
percent = status.percent
|
|
70
|
-
style = "green"
|
|
71
|
-
if percent >= 90:
|
|
72
|
-
style = "bold red"
|
|
73
|
-
elif percent >= 75:
|
|
74
|
-
style = "yellow"
|
|
75
|
-
elif percent >= 50:
|
|
76
|
-
style = "cyan"
|
|
77
|
-
|
|
78
|
-
used = self._format_int(status.used_tokens)
|
|
79
|
-
limit = self._format_int(status.input_token_limit)
|
|
80
|
-
session = str(status.session_id or "-")[-15:]
|
|
81
|
-
compact_hint = " auto-compact:on" if status.auto_compact else " auto-compact:off"
|
|
82
|
-
if status.auto_compact and status.used_tokens >= status.auto_compact_threshold:
|
|
83
|
-
compact_hint = " auto-compact:due"
|
|
84
|
-
|
|
85
|
-
self.console.print(
|
|
86
|
-
Text.assemble(
|
|
87
|
-
("ctx ", "dim"),
|
|
88
|
-
(f"{percent:.1f}%", style),
|
|
89
|
-
(f" {used}/{limit} tok", "dim"),
|
|
90
|
-
(f" · hist {status.visible_history_messages}/{status.total_history_messages}", "dim"),
|
|
91
|
-
(f" · msgs {status.total_session_messages}", "dim"),
|
|
92
|
-
(f" · img {status.image_messages}", "dim"),
|
|
93
|
-
(f" · mem {status.memory_entries}", "dim"),
|
|
94
|
-
(f" · pending {status.pending_messages}", "dim"),
|
|
95
|
-
(f" · {session}", "dim"),
|
|
96
|
-
(compact_hint, "dim"),
|
|
97
|
-
)
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
def clear_screen(self):
|
|
101
|
-
self.console.clear()
|
|
102
|
-
|
|
103
|
-
@staticmethod
|
|
104
|
-
def _format_int(value):
|
|
105
|
-
try:
|
|
106
|
-
return f"{int(value):,}".replace(",", " ")
|
|
107
|
-
except Exception:
|
|
108
|
-
return str(value)
|
|
109
|
-
|
|
110
|
-
def _on_response_start(self, event):
|
|
111
|
-
if self._in_response:
|
|
112
|
-
self.console.print()
|
|
113
|
-
self._in_response = True
|
|
114
|
-
self._has_content = False
|
|
115
|
-
self._seen_tool_calls.clear()
|
|
116
|
-
name = event.message.get("name") if event.get("message") else None
|
|
117
|
-
self.console.print(Rule(Text(name or "Agent", style="bold cyan"), style="cyan"))
|
|
118
|
-
|
|
119
|
-
def _on_content_delta(self, event):
|
|
120
|
-
self._has_content = True
|
|
121
|
-
self.console.print(event.delta, end="", markup=False, highlight=False, soft_wrap=True)
|
|
122
|
-
|
|
123
|
-
def _on_tool_call_start(self, event):
|
|
124
|
-
tool_call = event.get("tool_call") or {}
|
|
125
|
-
name = tool_call.get("name") or "unknown"
|
|
126
|
-
call_id = tool_call.get("call_id") or name
|
|
127
|
-
self._print_tool_call(name, call_id)
|
|
128
|
-
|
|
129
|
-
def _on_server_tool_call(self, event):
|
|
130
|
-
name = event.get("name") or "unknown"
|
|
131
|
-
call_id = event.get("call_id") or name
|
|
132
|
-
self._print_tool_call(name, call_id)
|
|
133
|
-
|
|
134
|
-
def _print_tool_call(self, name, call_id):
|
|
135
|
-
if call_id in self._seen_tool_calls:
|
|
136
|
-
return
|
|
137
|
-
self._seen_tool_calls.add(call_id)
|
|
138
|
-
if self._has_content:
|
|
139
|
-
self.console.print()
|
|
140
|
-
self.console.print(f"tool: {name}", style="dim cyan")
|
|
141
|
-
self._has_content = False
|
|
142
|
-
|
|
143
|
-
def _on_response_done(self, event):
|
|
144
|
-
if self._has_content:
|
|
145
|
-
self.console.print()
|
|
146
|
-
self._in_response = False
|
|
File without changes
|
|
File without changes
|
{codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/builtin_providers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/get_text/__init__.py
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/get_text/default_gitignore
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/get_text/get_text.py
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/get_text/simpler_get_text.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_agent_framework-0.1.2 → codex_agent_framework-0.1.4}/codex_agent/prompts/system_prompt.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|