codex-agent-framework 0.1.3__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.3 → codex_agent_framework-0.1.4}/CHANGELOG.md +13 -0
- {codex_agent_framework-0.1.3/codex_agent_framework.egg-info → codex_agent_framework-0.1.4}/PKG-INFO +3 -2
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/README.md +1 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/__init__.py +4 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/__main__.py +1 -1
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/agent.py +20 -12
- {codex_agent_framework-0.1.3 → 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.3 → codex_agent_framework-0.1.4}/codex_agent/event.py +17 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4/codex_agent_framework.egg-info}/PKG-INFO +3 -2
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent_framework.egg-info/SOURCES.txt +1 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent_framework.egg-info/requires.txt +1 -1
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/pyproject.toml +2 -2
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/tests/test_agent.py +19 -1
- {codex_agent_framework-0.1.3 → 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.3/codex_agent/chat.py +0 -146
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/LICENSE +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/MANIFEST.in +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/ai.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/builtin_providers.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/builtin_tools.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/command.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/get_text/__init__.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/get_text/default_gitignore +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/get_text/get_text.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/get_text/simpler_get_text.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/get_webdriver.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/image.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/latex.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/memory.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/message.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/prompts/system_prompt.txt +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/provider.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/stream_utils.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/tool.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/utils.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/voice.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/worker.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent_framework.egg-info/entry_points.txt +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent_framework.egg-info/top_level.txt +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/setup.cfg +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/tests/test_ai.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/tests/test_events.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/tests/test_image_message.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/tests/test_memory.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/tests/test_messages.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/tests/test_utils.py +0 -0
- {codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/tests/test_worker.py +0 -0
|
@@ -3,6 +3,19 @@
|
|
|
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
|
+
|
|
6
19
|
## [0.1.3] - 2026-05-03
|
|
7
20
|
### Added
|
|
8
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.
|
{codex_agent_framework-0.1.3/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,7 @@ 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.
|
|
345
346
|
- `0.1.3`: pass the current session id as `prompt_cache_key` to the Codex backend for prompt cache reuse.
|
|
346
347
|
- `0.1.2`: add `/clear` in the terminal chat to clear the screen without changing session history.
|
|
347
348
|
- `0.1.1`: image observations are now validated before session persistence, preventing broken `ImageMessage` entries from poisoning later turns.
|
|
@@ -295,6 +295,7 @@ 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.
|
|
298
299
|
- `0.1.3`: pass the current session id as `prompt_cache_key` to the Codex backend for prompt cache reuse.
|
|
299
300
|
- `0.1.2`: add `/clear` in the terminal chat to clear the screen without changing session history.
|
|
300
301
|
- `0.1.1`: image observations are now validated before session persistence, preventing broken `ImageMessage` entries from poisoning later turns.
|
|
@@ -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
|
|
@@ -1056,15 +1056,24 @@ class Agent:
|
|
|
1056
1056
|
|
|
1057
1057
|
self.clear_interrupt()
|
|
1058
1058
|
self.age_context_messages()
|
|
1059
|
+
turn_message = None
|
|
1060
|
+
self.emit(AssistantTurnStartEvent)
|
|
1059
1061
|
try:
|
|
1060
1062
|
while True:
|
|
1061
1063
|
self.check_interrupt()
|
|
1062
1064
|
self.dump_pending()
|
|
1063
|
-
message=
|
|
1064
|
-
|
|
1065
|
-
self.
|
|
1066
|
-
|
|
1067
|
-
|
|
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)
|
|
1068
1077
|
|
|
1069
1078
|
if called_tools:
|
|
1070
1079
|
if self.config.get('auto_proceed',True):
|
|
@@ -1077,6 +1086,8 @@ class Agent:
|
|
|
1077
1086
|
self.dump_pending()
|
|
1078
1087
|
self.add_message(DeveloperMessage(content="The previous agent response was interrupted by the user."))
|
|
1079
1088
|
return False
|
|
1089
|
+
finally:
|
|
1090
|
+
self.emit(AssistantTurnEndEvent, message=turn_message)
|
|
1080
1091
|
|
|
1081
1092
|
def speak(self,text,**kwargs):
|
|
1082
1093
|
from .voice import silent_play
|
|
@@ -1168,13 +1179,10 @@ class Agent:
|
|
|
1168
1179
|
return self.process()
|
|
1169
1180
|
|
|
1170
1181
|
def interact(self):
|
|
1171
|
-
"""Starts
|
|
1172
|
-
|
|
1173
|
-
Uses Alt+Enter for multi-line input submission.
|
|
1174
|
-
"""
|
|
1182
|
+
"""Starts the interactive Textual chat UI."""
|
|
1175
1183
|
from .utils import set_root_path
|
|
1176
1184
|
import os
|
|
1177
1185
|
if os.environ.get('AGENT_ROOT_PATH') is None:
|
|
1178
1186
|
set_root_path(file=__file__)
|
|
1179
|
-
from .chat import
|
|
1180
|
-
|
|
1187
|
+
from .chat import Chat
|
|
1188
|
+
Chat(self).run()
|
|
@@ -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.3 → 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,7 @@ 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.
|
|
345
346
|
- `0.1.3`: pass the current session id as `prompt_cache_key` to the Codex backend for prompt cache reuse.
|
|
346
347
|
- `0.1.2`: add `/clear` in the terminal chat to clear the screen without changing session history.
|
|
347
348
|
- `0.1.1`: image observations are now validated before session persistence, preventing broken `ImageMessage` entries from poisoning later turns.
|
|
@@ -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
|
]
|
|
@@ -1143,7 +1143,7 @@ def test_agent_command_supports_varargs_and_kwargs():
|
|
|
1143
1143
|
def test_agent_loads_builtin_commands():
|
|
1144
1144
|
agent = Agent(session="new", voice_enabled=False)
|
|
1145
1145
|
|
|
1146
|
-
assert {"help", "compact", "config", "model", "reasoning", "verbosity"}.issubset(agent.commands)
|
|
1146
|
+
assert {"help", "compact", "config", "model", "reasoning", "verbosity", "voice"}.issubset(agent.commands)
|
|
1147
1147
|
assert "/help" in agent("/help")
|
|
1148
1148
|
assert "/compact" in agent("/help")
|
|
1149
1149
|
|
|
@@ -1162,6 +1162,24 @@ def test_builtin_model_config_commands_update_and_persist(tmp_path, monkeypatch)
|
|
|
1162
1162
|
assert agent.config.verbosity == "high"
|
|
1163
1163
|
|
|
1164
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
|
+
|
|
1165
1183
|
def test_builtin_config_command_shows_and_updates_multiple_model_fields(tmp_path, monkeypatch):
|
|
1166
1184
|
monkeypatch.setattr(utils, "RUNTIME_DIR", str(tmp_path))
|
|
1167
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
|
|
File without changes
|
{codex_agent_framework-0.1.3 → 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.3 → codex_agent_framework-0.1.4}/codex_agent/get_text/__init__.py
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/get_text/default_gitignore
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.3 → codex_agent_framework-0.1.4}/codex_agent/get_text/get_text.py
RENAMED
|
File without changes
|
{codex_agent_framework-0.1.3 → 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.3 → 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
|