python-codex 0.1.2__py3-none-any.whl → 0.1.4__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/__init__.py +5 -1
- pycodex/agent.py +89 -51
- pycodex/cli.py +152 -45
- pycodex/collaboration.py +6 -7
- pycodex/compat.py +99 -0
- pycodex/context.py +110 -87
- pycodex/doctor.py +40 -40
- pycodex/model.py +429 -90
- pycodex/portable.py +33 -33
- pycodex/portable_server.py +22 -21
- pycodex/prompts/models.json +30 -0
- pycodex/protocol.py +84 -86
- pycodex/runtime.py +36 -35
- pycodex/runtime_services.py +69 -69
- pycodex/tools/agent_tool_schemas.py +0 -2
- pycodex/tools/apply_patch_tool.py +45 -46
- pycodex/tools/base_tool.py +35 -36
- pycodex/tools/close_agent_tool.py +2 -4
- pycodex/tools/code_mode_manager.py +61 -61
- pycodex/tools/exec_command_tool.py +5 -6
- pycodex/tools/exec_runtime.js +3 -3
- pycodex/tools/exec_tool.py +2 -4
- pycodex/tools/grep_files_tool.py +10 -11
- pycodex/tools/list_dir_tool.py +8 -9
- pycodex/tools/read_file_tool.py +13 -14
- pycodex/tools/request_permissions_tool.py +2 -4
- pycodex/tools/request_user_input_tool.py +13 -14
- pycodex/tools/resume_agent_tool.py +2 -4
- pycodex/tools/send_input_tool.py +8 -9
- pycodex/tools/shell_command_tool.py +5 -6
- pycodex/tools/shell_tool.py +5 -6
- pycodex/tools/spawn_agent_tool.py +4 -5
- pycodex/tools/unified_exec_manager.py +62 -61
- pycodex/tools/update_plan_tool.py +4 -5
- pycodex/tools/view_image_tool.py +4 -5
- pycodex/tools/wait_agent_tool.py +2 -4
- pycodex/tools/wait_tool.py +4 -5
- pycodex/tools/web_search_tool.py +1 -3
- pycodex/tools/write_stdin_tool.py +4 -5
- pycodex/utils/__init__.py +4 -0
- pycodex/utils/compactor.py +189 -0
- pycodex/utils/dotenv.py +6 -6
- pycodex/utils/get_env.py +37 -33
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/session_persist.py +483 -0
- pycodex/utils/visualize.py +197 -83
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/METADATA +32 -11
- python_codex-0.1.4.dist-info/RECORD +76 -0
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/WHEEL +1 -1
- responses_server/app.py +32 -20
- responses_server/config.py +17 -17
- responses_server/payload_processors.py +26 -17
- responses_server/server.py +11 -11
- responses_server/session_store.py +10 -10
- responses_server/stream_router.py +83 -64
- responses_server/tools/custom_adapter.py +12 -12
- responses_server/tools/web_search.py +33 -33
- python_codex-0.1.2.dist-info/RECORD +0 -73
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.2.dist-info → python_codex-0.1.4.dist-info}/licenses/LICENSE +0 -0
pycodex/utils/visualize.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
import asyncio
|
|
4
3
|
import json
|
|
@@ -12,6 +11,7 @@ from prompt_toolkit.patch_stdout import StdoutProxy
|
|
|
12
11
|
from prompt_toolkit.patch_stdout import patch_stdout
|
|
13
12
|
|
|
14
13
|
from ..protocol import AgentEvent, JSONDict, ToolCall, ToolResult
|
|
14
|
+
import typing
|
|
15
15
|
|
|
16
16
|
ANSI_RESET = "\x1b[0m"
|
|
17
17
|
ANSI_BOLD = "\x1b[1m"
|
|
@@ -23,16 +23,18 @@ ANSI_YELLOW = "\x1b[33m"
|
|
|
23
23
|
ANSI_MAGENTA = "\x1b[35m"
|
|
24
24
|
ANSI_RED = "\x1b[31m"
|
|
25
25
|
SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
|
|
26
|
+
PROMPT_CONTEXT_BASELINE_TOKENS = 12_000
|
|
27
|
+
DEFAULT_MAIN_PROMPT = "pycodex> "
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
def shorten_title(text: str, limit: int = 48) -> str:
|
|
30
|
+
def shorten_title(text: 'str', limit: 'int' = 48) -> 'str':
|
|
29
31
|
compact = " ".join(text.split())
|
|
30
32
|
if len(compact) <= limit:
|
|
31
33
|
return compact
|
|
32
34
|
return compact[: limit - 3].rstrip() + "..."
|
|
33
35
|
|
|
34
36
|
|
|
35
|
-
def cli_color_enabled() -> bool:
|
|
37
|
+
def cli_color_enabled() -> 'bool':
|
|
36
38
|
return os.environ.get("PYCODEX_NO_COLOR", "").strip().lower() not in {
|
|
37
39
|
"1",
|
|
38
40
|
"true",
|
|
@@ -41,7 +43,7 @@ def cli_color_enabled() -> bool:
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
|
|
44
|
-
def colorize_cli_message(text: str, kind: str, enabled: bool) -> str:
|
|
46
|
+
def colorize_cli_message(text: 'str', kind: 'str', enabled: 'bool') -> 'str':
|
|
45
47
|
if not enabled:
|
|
46
48
|
return text
|
|
47
49
|
palette = {
|
|
@@ -61,9 +63,9 @@ def colorize_cli_message(text: str, kind: str, enabled: bool) -> str:
|
|
|
61
63
|
|
|
62
64
|
|
|
63
65
|
def format_cli_plan_messages(
|
|
64
|
-
summary: str,
|
|
65
|
-
plan_items:
|
|
66
|
-
) ->
|
|
66
|
+
summary: 'str',
|
|
67
|
+
plan_items: 'typing.List[JSONDict]',
|
|
68
|
+
) -> 'typing.List[str]':
|
|
67
69
|
lines = [f"[plan] {summary}" if summary else "[plan] Plan updated"]
|
|
68
70
|
for item in plan_items:
|
|
69
71
|
step = str(item.get("step", "")).strip()
|
|
@@ -79,20 +81,37 @@ def format_cli_plan_messages(
|
|
|
79
81
|
return lines
|
|
80
82
|
|
|
81
83
|
|
|
82
|
-
def build_cli_spinner_frame(index: int, label: str) -> str:
|
|
84
|
+
def build_cli_spinner_frame(index: 'int', label: 'str') -> 'str':
|
|
83
85
|
suffix = f" {label}" if label else ""
|
|
84
86
|
return f"⏳{suffix} {SPINNER_FRAMES[index % len(SPINNER_FRAMES)]}"
|
|
85
87
|
|
|
86
88
|
|
|
89
|
+
def percent_of_context_window_remaining(
|
|
90
|
+
total_tokens: 'int',
|
|
91
|
+
context_window_tokens: 'int',
|
|
92
|
+
) -> 'int':
|
|
93
|
+
if context_window_tokens <= PROMPT_CONTEXT_BASELINE_TOKENS:
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
effective_window = context_window_tokens - PROMPT_CONTEXT_BASELINE_TOKENS
|
|
97
|
+
used = max(total_tokens - PROMPT_CONTEXT_BASELINE_TOKENS, 0)
|
|
98
|
+
remaining = max(effective_window - used, 0)
|
|
99
|
+
return int(
|
|
100
|
+
round(
|
|
101
|
+
max(0.0, min(100.0, (remaining / effective_window) * 100.0))
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
87
106
|
class Spinner:
|
|
88
107
|
def __init__(
|
|
89
108
|
self,
|
|
90
109
|
raw_write,
|
|
91
110
|
raw_flush,
|
|
92
|
-
terminal_lock: threading.RLock,
|
|
93
|
-
color_enabled: bool,
|
|
94
|
-
enabled: bool,
|
|
95
|
-
) -> None:
|
|
111
|
+
terminal_lock: 'threading.RLock',
|
|
112
|
+
color_enabled: 'bool',
|
|
113
|
+
enabled: 'bool',
|
|
114
|
+
) -> 'None':
|
|
96
115
|
self._raw_write = raw_write
|
|
97
116
|
self._raw_flush = raw_flush
|
|
98
117
|
self._terminal_lock = terminal_lock
|
|
@@ -104,7 +123,7 @@ class Spinner:
|
|
|
104
123
|
self._index = 0
|
|
105
124
|
self._label = "thinking"
|
|
106
125
|
self._stop = threading.Event()
|
|
107
|
-
self._thread: threading.Thread
|
|
126
|
+
self._thread: 'typing.Union[threading.Thread, None]' = None
|
|
108
127
|
if self._enabled:
|
|
109
128
|
self._thread = threading.Thread(
|
|
110
129
|
target=self._run,
|
|
@@ -113,32 +132,32 @@ class Spinner:
|
|
|
113
132
|
)
|
|
114
133
|
self._thread.start()
|
|
115
134
|
|
|
116
|
-
def start_turn(self, label: str = "thinking") -> None:
|
|
135
|
+
def start_turn(self, label: 'str' = "thinking") -> 'None':
|
|
117
136
|
with self._terminal_lock:
|
|
118
137
|
self._turn_active = True
|
|
119
138
|
self._paused = False
|
|
120
139
|
self._label = label
|
|
121
140
|
|
|
122
|
-
def set_label(self, label: str) -> None:
|
|
141
|
+
def set_label(self, label: 'str') -> 'None':
|
|
123
142
|
with self._terminal_lock:
|
|
124
143
|
self._label = label
|
|
125
144
|
|
|
126
|
-
def finish_turn(self) -> None:
|
|
145
|
+
def finish_turn(self) -> 'None':
|
|
127
146
|
with self._terminal_lock:
|
|
128
147
|
self._turn_active = False
|
|
129
148
|
self._paused = False
|
|
130
149
|
self.clear()
|
|
131
150
|
|
|
132
|
-
def pause(self) -> None:
|
|
151
|
+
def pause(self) -> 'None':
|
|
133
152
|
with self._terminal_lock:
|
|
134
153
|
self._paused = True
|
|
135
154
|
self.clear()
|
|
136
155
|
|
|
137
|
-
def resume(self) -> None:
|
|
156
|
+
def resume(self) -> 'None':
|
|
138
157
|
with self._terminal_lock:
|
|
139
158
|
self._paused = False
|
|
140
159
|
|
|
141
|
-
def clear(self) -> None:
|
|
160
|
+
def clear(self) -> 'None':
|
|
142
161
|
if not self._enabled or not self._visible:
|
|
143
162
|
return
|
|
144
163
|
with self._terminal_lock:
|
|
@@ -146,13 +165,13 @@ class Spinner:
|
|
|
146
165
|
self._raw_flush()
|
|
147
166
|
self._visible = False
|
|
148
167
|
|
|
149
|
-
def close(self) -> None:
|
|
168
|
+
def close(self) -> 'None':
|
|
150
169
|
self.finish_turn()
|
|
151
170
|
if self._thread is not None:
|
|
152
171
|
self._stop.set()
|
|
153
172
|
self._thread.join(timeout=0.5)
|
|
154
173
|
|
|
155
|
-
def prompt_line(self) -> str
|
|
174
|
+
def prompt_line(self) -> 'typing.Union[str, None]':
|
|
156
175
|
if not self._turn_active:
|
|
157
176
|
return None
|
|
158
177
|
with self._terminal_lock:
|
|
@@ -160,7 +179,7 @@ class Spinner:
|
|
|
160
179
|
frame_index = int(time.monotonic() / 0.12)
|
|
161
180
|
return build_cli_spinner_frame(frame_index, label)
|
|
162
181
|
|
|
163
|
-
def _run(self) -> None:
|
|
182
|
+
def _run(self) -> 'None':
|
|
164
183
|
while not self._stop.wait(0.12):
|
|
165
184
|
if not self._turn_active or self._paused:
|
|
166
185
|
continue
|
|
@@ -178,7 +197,7 @@ class Spinner:
|
|
|
178
197
|
self._visible = True
|
|
179
198
|
|
|
180
199
|
|
|
181
|
-
def format_cli_tool_call_message(tool_name: str, payload: JSONDict) -> str
|
|
200
|
+
def format_cli_tool_call_message(tool_name: 'str', payload: 'JSONDict') -> 'typing.Union[str, None]':
|
|
182
201
|
if tool_name != "web_search":
|
|
183
202
|
return None
|
|
184
203
|
|
|
@@ -207,7 +226,7 @@ def format_cli_tool_call_message(tool_name: str, payload: JSONDict) -> str | Non
|
|
|
207
226
|
return "[web] browsing"
|
|
208
227
|
|
|
209
228
|
|
|
210
|
-
def short_id(value: str, limit: int = 8) -> str:
|
|
229
|
+
def short_id(value: 'str', limit: 'int' = 8) -> 'str':
|
|
211
230
|
compact = value.strip()
|
|
212
231
|
if len(compact) <= limit + 4:
|
|
213
232
|
return compact
|
|
@@ -215,10 +234,10 @@ def short_id(value: str, limit: int = 8) -> str:
|
|
|
215
234
|
|
|
216
235
|
|
|
217
236
|
def format_cli_tool_message(
|
|
218
|
-
tool_name: str,
|
|
219
|
-
summary: str,
|
|
220
|
-
is_error: bool,
|
|
221
|
-
) -> str:
|
|
237
|
+
tool_name: 'str',
|
|
238
|
+
summary: 'str',
|
|
239
|
+
is_error: 'bool',
|
|
240
|
+
) -> 'str':
|
|
222
241
|
if tool_name == "update_plan":
|
|
223
242
|
if is_error:
|
|
224
243
|
return f"[error] plan failed: {summary}" if summary else "[error] plan failed"
|
|
@@ -290,13 +309,13 @@ def format_cli_tool_message(
|
|
|
290
309
|
return f"[tool] {tool_name}: {summary}" if summary else f"[tool] {tool_name}"
|
|
291
310
|
|
|
292
311
|
|
|
293
|
-
def extract_plan_items(arguments: object) ->
|
|
312
|
+
def extract_plan_items(arguments: 'object') -> 'typing.List[JSONDict]':
|
|
294
313
|
if not isinstance(arguments, dict):
|
|
295
314
|
return []
|
|
296
315
|
raw_plan = arguments.get("plan")
|
|
297
316
|
if not isinstance(raw_plan, list):
|
|
298
317
|
return []
|
|
299
|
-
plan_items:
|
|
318
|
+
plan_items: 'typing.List[JSONDict]' = []
|
|
300
319
|
for item in raw_plan:
|
|
301
320
|
if not isinstance(item, dict):
|
|
302
321
|
continue
|
|
@@ -309,7 +328,7 @@ def extract_plan_items(arguments: object) -> list[JSONDict]:
|
|
|
309
328
|
return plan_items
|
|
310
329
|
|
|
311
330
|
|
|
312
|
-
def summarize_tool_event(call: ToolCall, result: ToolResult) -> str
|
|
331
|
+
def summarize_tool_event(call: 'ToolCall', result: 'ToolResult') -> 'typing.Union[str, None]':
|
|
313
332
|
command_preview = _command_preview(call)
|
|
314
333
|
result_summary = _summarize_tool_result(result)
|
|
315
334
|
if call.name == "update_plan":
|
|
@@ -322,8 +341,8 @@ def summarize_tool_event(call: ToolCall, result: ToolResult) -> str | None:
|
|
|
322
341
|
|
|
323
342
|
|
|
324
343
|
def extract_tool_event_display(
|
|
325
|
-
payload:
|
|
326
|
-
) ->
|
|
344
|
+
payload: 'typing.Dict[str, object]',
|
|
345
|
+
) -> 'typing.Tuple[str, str, bool]':
|
|
327
346
|
tool_name = str(payload.get("tool_name", "")).strip()
|
|
328
347
|
is_error = bool(payload.get("is_error"))
|
|
329
348
|
call = payload.get("call")
|
|
@@ -334,7 +353,7 @@ def extract_tool_event_display(
|
|
|
334
353
|
return tool_name, summary, is_error
|
|
335
354
|
|
|
336
355
|
|
|
337
|
-
def extract_plan_event_items(payload:
|
|
356
|
+
def extract_plan_event_items(payload: 'typing.Dict[str, object]') -> 'typing.List[JSONDict]':
|
|
338
357
|
call = payload.get("call")
|
|
339
358
|
if isinstance(call, ToolCall):
|
|
340
359
|
return extract_plan_items(call.arguments)
|
|
@@ -344,14 +363,14 @@ def extract_plan_event_items(payload: dict[str, object]) -> list[JSONDict]:
|
|
|
344
363
|
return []
|
|
345
364
|
|
|
346
365
|
|
|
347
|
-
def _truncate_text(text: str, limit: int = 96) -> str:
|
|
366
|
+
def _truncate_text(text: 'str', limit: 'int' = 96) -> 'str':
|
|
348
367
|
compact = " ".join(text.split())
|
|
349
368
|
if len(compact) <= limit:
|
|
350
369
|
return compact
|
|
351
370
|
return compact[: limit - 3].rstrip() + "..."
|
|
352
371
|
|
|
353
372
|
|
|
354
|
-
def _extract_output_preview(text: str) -> str
|
|
373
|
+
def _extract_output_preview(text: 'str') -> 'typing.Union[str, None]':
|
|
355
374
|
lines = [line.strip() for line in text.splitlines()]
|
|
356
375
|
if "Output:" in lines:
|
|
357
376
|
output_index = lines.index("Output:")
|
|
@@ -368,7 +387,7 @@ def _extract_output_preview(text: str) -> str | None:
|
|
|
368
387
|
return None
|
|
369
388
|
|
|
370
389
|
|
|
371
|
-
def _summarize_agent_status(status: object) -> str:
|
|
390
|
+
def _summarize_agent_status(status: 'object') -> 'str':
|
|
372
391
|
if isinstance(status, str):
|
|
373
392
|
return status
|
|
374
393
|
if isinstance(status, dict):
|
|
@@ -382,7 +401,7 @@ def _summarize_agent_status(status: object) -> str:
|
|
|
382
401
|
return _truncate_text(json.dumps(status, ensure_ascii=False, separators=(",", ":")))
|
|
383
402
|
|
|
384
403
|
|
|
385
|
-
def _summarize_tool_result(result: ToolResult) -> str
|
|
404
|
+
def _summarize_tool_result(result: 'ToolResult') -> 'typing.Union[str, None]':
|
|
386
405
|
if result.name == "spawn_agent" and isinstance(result.output, dict):
|
|
387
406
|
agent_id = str(result.output.get("agent_id", "")).strip()
|
|
388
407
|
nickname = str(result.output.get("nickname", "")).strip()
|
|
@@ -399,7 +418,7 @@ def _summarize_tool_result(result: ToolResult) -> str | None:
|
|
|
399
418
|
return "timed out"
|
|
400
419
|
status = result.output.get("status")
|
|
401
420
|
if isinstance(status, dict):
|
|
402
|
-
parts:
|
|
421
|
+
parts: 'typing.List[str]' = []
|
|
403
422
|
for agent_id, agent_status in status.items():
|
|
404
423
|
if not isinstance(agent_id, str):
|
|
405
424
|
continue
|
|
@@ -425,7 +444,7 @@ def _summarize_tool_result(result: ToolResult) -> str | None:
|
|
|
425
444
|
return None
|
|
426
445
|
|
|
427
446
|
|
|
428
|
-
def _string_arg(arguments: object, key: str) -> str
|
|
447
|
+
def _string_arg(arguments: 'object', key: 'str') -> 'typing.Union[str, None]':
|
|
429
448
|
if not isinstance(arguments, dict):
|
|
430
449
|
return None
|
|
431
450
|
value = arguments.get(key)
|
|
@@ -434,7 +453,7 @@ def _string_arg(arguments: object, key: str) -> str | None:
|
|
|
434
453
|
return str(value)
|
|
435
454
|
|
|
436
455
|
|
|
437
|
-
def _int_arg(arguments: object, key: str) -> int
|
|
456
|
+
def _int_arg(arguments: 'object', key: 'str') -> 'typing.Union[int, None]':
|
|
438
457
|
if not isinstance(arguments, dict):
|
|
439
458
|
return None
|
|
440
459
|
value = arguments.get(key)
|
|
@@ -443,7 +462,7 @@ def _int_arg(arguments: object, key: str) -> int | None:
|
|
|
443
462
|
return int(value)
|
|
444
463
|
|
|
445
464
|
|
|
446
|
-
def _command_preview(call: ToolCall) -> str
|
|
465
|
+
def _command_preview(call: 'ToolCall') -> 'typing.Union[str, None]':
|
|
447
466
|
if call.name == "exec_command":
|
|
448
467
|
cmd = _string_arg(call.arguments, "cmd")
|
|
449
468
|
if cmd:
|
|
@@ -503,7 +522,7 @@ def _command_preview(call: ToolCall) -> str | None:
|
|
|
503
522
|
return None
|
|
504
523
|
|
|
505
524
|
|
|
506
|
-
def _plan_progress_summary(plan:
|
|
525
|
+
def _plan_progress_summary(plan: 'typing.List[object]') -> 'str':
|
|
507
526
|
total = len(plan)
|
|
508
527
|
completed = 0
|
|
509
528
|
in_progress = 0
|
|
@@ -541,7 +560,7 @@ class CliSessionView:
|
|
|
541
560
|
source has closed and the caller should end the session loop.
|
|
542
561
|
- `write_line(text)`, `finish_stream()`, `show_error(text)`: imperative output
|
|
543
562
|
helpers for CLI-side messages that do not come from `AgentEvent`.
|
|
544
|
-
- `show_history()`, `show_title()`, `show_steer_queued(...)`,
|
|
563
|
+
- `show_history()`, `show_title()`, `load_session_history(...)`, `show_steer_queued(...)`,
|
|
545
564
|
`schedule_steer_inserted(...)`: small session UI helpers used by the
|
|
546
565
|
interactive command loop.
|
|
547
566
|
- `close()`: release prompt/spinner resources at shutdown.
|
|
@@ -562,27 +581,34 @@ class CliSessionView:
|
|
|
562
581
|
normal terminal stream so the reply is not lost.
|
|
563
582
|
"""
|
|
564
583
|
|
|
565
|
-
def __init__(
|
|
584
|
+
def __init__(
|
|
585
|
+
self,
|
|
586
|
+
context_window_tokens: 'typing.Union[int, None]' = None,
|
|
587
|
+
) -> 'None':
|
|
566
588
|
import sys
|
|
567
589
|
|
|
568
590
|
self._line_output = print
|
|
569
591
|
self._raw_write = sys.stdout.write
|
|
570
592
|
self._raw_flush = sys.stdout.flush
|
|
571
593
|
self._terminal_lock = threading.RLock()
|
|
572
|
-
self._title: str
|
|
573
|
-
self._pending_user_prompts:
|
|
574
|
-
self._queued_steer_prompts:
|
|
575
|
-
self._inserted_steer_prompts:
|
|
576
|
-
self._history:
|
|
594
|
+
self._title: 'typing.Union[str, None]' = None
|
|
595
|
+
self._pending_user_prompts: 'typing.Dict[str, str]' = {}
|
|
596
|
+
self._queued_steer_prompts: 'typing.Dict[str, typing.List[str]]' = {}
|
|
597
|
+
self._inserted_steer_prompts: 'typing.Dict[str, typing.List[str]]' = {}
|
|
598
|
+
self._history: 'typing.List[typing.Tuple[str, str]]' = []
|
|
577
599
|
self._streaming = False
|
|
578
600
|
self._prompt_stream_buffer = ""
|
|
579
601
|
self._streaming_in_prompt = False
|
|
580
602
|
self._input_active = False
|
|
603
|
+
self._context_window_tokens = context_window_tokens
|
|
604
|
+
self._context_remaining_percent: 'typing.Union[int, None]' = (
|
|
605
|
+
100 if context_window_tokens is not None else None
|
|
606
|
+
)
|
|
581
607
|
self._color_enabled = cli_color_enabled() and sys.stdout.isatty()
|
|
582
|
-
self._agent_names:
|
|
583
|
-
self._prompt_session: PromptSession
|
|
584
|
-
self._prompt_task: asyncio.Task[str]
|
|
585
|
-
self._stdout_proxy: StdoutProxy
|
|
608
|
+
self._agent_names: 'typing.Dict[str, str]' = {}
|
|
609
|
+
self._prompt_session: 'typing.Union[PromptSession, None]' = None
|
|
610
|
+
self._prompt_task: 'typing.Union[asyncio.Task[str], None]' = None
|
|
611
|
+
self._stdout_proxy: 'typing.Union[StdoutProxy, None]' = None
|
|
586
612
|
self._spinner = Spinner(
|
|
587
613
|
self._raw_write,
|
|
588
614
|
self._raw_flush,
|
|
@@ -591,7 +617,7 @@ class CliSessionView:
|
|
|
591
617
|
False,
|
|
592
618
|
)
|
|
593
619
|
|
|
594
|
-
def handle_event(self, event: AgentEvent) -> None:
|
|
620
|
+
def handle_event(self, event: 'AgentEvent') -> 'None':
|
|
595
621
|
if event.kind == "turn_started":
|
|
596
622
|
submission_id = str(event.payload.get("submission_id", event.turn_id)).strip()
|
|
597
623
|
user_texts = event.payload.get("user_texts")
|
|
@@ -627,6 +653,8 @@ class CliSessionView:
|
|
|
627
653
|
self._color_enabled,
|
|
628
654
|
)
|
|
629
655
|
)
|
|
656
|
+
if user_text:
|
|
657
|
+
self._print_user_turn(user_text)
|
|
630
658
|
self._spinner.start_turn("thinking")
|
|
631
659
|
if self._input_active:
|
|
632
660
|
self._spinner.pause()
|
|
@@ -640,6 +668,27 @@ class CliSessionView:
|
|
|
640
668
|
self._spinner.set_label("waiting model")
|
|
641
669
|
return
|
|
642
670
|
|
|
671
|
+
if event.kind == "token_count":
|
|
672
|
+
self._update_context_window(event.payload.get("usage"))
|
|
673
|
+
return
|
|
674
|
+
|
|
675
|
+
if event.kind == "stream_error":
|
|
676
|
+
self._finish_stream()
|
|
677
|
+
message = str(event.payload.get("message", "")).strip() or "Reconnecting..."
|
|
678
|
+
self._print_line(
|
|
679
|
+
colorize_cli_message(
|
|
680
|
+
f"[status] {message}",
|
|
681
|
+
"status",
|
|
682
|
+
self._color_enabled,
|
|
683
|
+
)
|
|
684
|
+
)
|
|
685
|
+
if self._input_active:
|
|
686
|
+
self._spinner.pause()
|
|
687
|
+
else:
|
|
688
|
+
self._spinner.resume()
|
|
689
|
+
self._spinner.set_label("reconnecting")
|
|
690
|
+
return
|
|
691
|
+
|
|
643
692
|
if event.kind == "assistant_delta":
|
|
644
693
|
delta = str(event.payload.get("delta", ""))
|
|
645
694
|
if not delta:
|
|
@@ -676,16 +725,26 @@ class CliSessionView:
|
|
|
676
725
|
self._spinner.pause()
|
|
677
726
|
else:
|
|
678
727
|
self._spinner.resume()
|
|
679
|
-
self._spinner.set_label("running tools")
|
|
728
|
+
self._spinner.set_label("running provider tools")
|
|
680
729
|
return
|
|
681
730
|
|
|
682
731
|
if event.kind == "tool_started":
|
|
683
732
|
self._finish_stream()
|
|
733
|
+
tool_name = str(event.payload.get("tool_name", "")).strip()
|
|
734
|
+
call = event.payload.get("call")
|
|
735
|
+
args = None
|
|
736
|
+
if isinstance(call, ToolCall):
|
|
737
|
+
args = call.arguments
|
|
684
738
|
if self._input_active:
|
|
685
739
|
self._spinner.pause()
|
|
686
740
|
else:
|
|
687
741
|
self._spinner.resume()
|
|
688
|
-
|
|
742
|
+
if tool_name and args is not None:
|
|
743
|
+
self._spinner.set_label(f"running {tool_name}({args})")
|
|
744
|
+
elif tool_name:
|
|
745
|
+
self._spinner.set_label(f"running {tool_name}")
|
|
746
|
+
else:
|
|
747
|
+
self._spinner.set_label("running provider tools")
|
|
689
748
|
return
|
|
690
749
|
|
|
691
750
|
if event.kind == "tool_completed":
|
|
@@ -738,7 +797,7 @@ class CliSessionView:
|
|
|
738
797
|
self._history.append((pending_prompt, final_text))
|
|
739
798
|
return
|
|
740
799
|
|
|
741
|
-
def show_history(self) -> None:
|
|
800
|
+
def show_history(self) -> 'None':
|
|
742
801
|
self._finish_stream()
|
|
743
802
|
if not self._history:
|
|
744
803
|
self._print_line("No history yet.")
|
|
@@ -749,27 +808,40 @@ class CliSessionView:
|
|
|
749
808
|
self._print_line(f"[{index}] user> {user_text}")
|
|
750
809
|
self._print_line(f" assistant> {assistant_text}")
|
|
751
810
|
|
|
752
|
-
def show_title(self) -> None:
|
|
811
|
+
def show_title(self) -> 'None':
|
|
753
812
|
self._finish_stream()
|
|
754
813
|
self._print_line(f"Session: {self._title or 'untitled'}")
|
|
755
814
|
|
|
756
|
-
def
|
|
815
|
+
def load_session_history(
|
|
816
|
+
self,
|
|
817
|
+
title: 'typing.Union[str, None]',
|
|
818
|
+
history: 'typing.Tuple[typing.Tuple[str, str], ...]',
|
|
819
|
+
) -> 'None':
|
|
820
|
+
self._spinner.finish_turn()
|
|
821
|
+
self._finish_stream()
|
|
822
|
+
self._title = title or None
|
|
823
|
+
self._history = list(history)
|
|
824
|
+
self._pending_user_prompts.clear()
|
|
825
|
+
self._queued_steer_prompts.clear()
|
|
826
|
+
self._inserted_steer_prompts.clear()
|
|
827
|
+
|
|
828
|
+
def pause_spinner(self) -> 'None':
|
|
757
829
|
self._spinner.pause()
|
|
758
830
|
|
|
759
|
-
def resume_spinner(self) -> None:
|
|
831
|
+
def resume_spinner(self) -> 'None':
|
|
760
832
|
self._spinner.resume()
|
|
761
833
|
|
|
762
|
-
def set_input_active(self, active: bool, resume_spinner: bool = True) -> None:
|
|
834
|
+
def set_input_active(self, active: 'bool', resume_spinner: 'bool' = True) -> 'None':
|
|
763
835
|
self._input_active = active
|
|
764
836
|
if active:
|
|
765
837
|
self._spinner.pause()
|
|
766
838
|
elif resume_spinner:
|
|
767
839
|
self._spinner.resume()
|
|
768
840
|
|
|
769
|
-
def is_streaming_output(self) -> bool:
|
|
841
|
+
def is_streaming_output(self) -> 'bool':
|
|
770
842
|
return self._streaming
|
|
771
843
|
|
|
772
|
-
def handoff_prompt_stream_to_output(self) -> None:
|
|
844
|
+
def handoff_prompt_stream_to_output(self) -> 'None':
|
|
773
845
|
if not self._streaming or not self._streaming_in_prompt:
|
|
774
846
|
return
|
|
775
847
|
buffered = self._prompt_stream_buffer
|
|
@@ -782,7 +854,7 @@ class CliSessionView:
|
|
|
782
854
|
self._raw_write(buffered)
|
|
783
855
|
self._raw_flush()
|
|
784
856
|
|
|
785
|
-
async def poll_prompt(self, prompt: str) -> str
|
|
857
|
+
async def poll_prompt(self, prompt: 'str') -> 'typing.Union[str, None]':
|
|
786
858
|
if self._prompt_task is None:
|
|
787
859
|
if self.is_streaming_output():
|
|
788
860
|
return None
|
|
@@ -807,7 +879,8 @@ class CliSessionView:
|
|
|
807
879
|
finally:
|
|
808
880
|
self.set_input_active(False, resume_spinner=False)
|
|
809
881
|
|
|
810
|
-
def build_input_prompt(self, prompt: str) -> str:
|
|
882
|
+
def build_input_prompt(self, prompt: 'str') -> 'str':
|
|
883
|
+
prompt = self._format_main_prompt(prompt)
|
|
811
884
|
if not self._input_active:
|
|
812
885
|
return prompt
|
|
813
886
|
if self._streaming and self._streaming_in_prompt:
|
|
@@ -819,7 +892,30 @@ class CliSessionView:
|
|
|
819
892
|
return prompt
|
|
820
893
|
return f"{prompt_line}\n{prompt}"
|
|
821
894
|
|
|
822
|
-
def
|
|
895
|
+
def _update_context_window(self, usage: 'object') -> 'None':
|
|
896
|
+
if self._context_window_tokens is None:
|
|
897
|
+
return
|
|
898
|
+
if not isinstance(usage, dict):
|
|
899
|
+
self._context_remaining_percent = None
|
|
900
|
+
return
|
|
901
|
+
try:
|
|
902
|
+
total_tokens = int(usage["total_tokens"])
|
|
903
|
+
except (KeyError, TypeError, ValueError):
|
|
904
|
+
self._context_remaining_percent = None
|
|
905
|
+
return
|
|
906
|
+
self._context_remaining_percent = percent_of_context_window_remaining(
|
|
907
|
+
total_tokens,
|
|
908
|
+
self._context_window_tokens,
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
def _format_main_prompt(self, prompt: 'str') -> 'str':
|
|
912
|
+
if prompt != DEFAULT_MAIN_PROMPT:
|
|
913
|
+
return prompt
|
|
914
|
+
if self._context_remaining_percent is None:
|
|
915
|
+
return prompt
|
|
916
|
+
return f"pyco({self._context_remaining_percent}%)> "
|
|
917
|
+
|
|
918
|
+
def show_steer_queued(self, turn_id: 'str', prompt: 'str') -> 'None':
|
|
823
919
|
preview = shorten_title(prompt, limit=72)
|
|
824
920
|
self._queued_steer_prompts.setdefault(turn_id, []).append(preview)
|
|
825
921
|
self._print_line(
|
|
@@ -830,12 +926,12 @@ class CliSessionView:
|
|
|
830
926
|
)
|
|
831
927
|
)
|
|
832
928
|
|
|
833
|
-
def schedule_steer_inserted(self, turn_id: str, prompt: str) -> None:
|
|
929
|
+
def schedule_steer_inserted(self, turn_id: 'str', prompt: 'str') -> 'None':
|
|
834
930
|
self._inserted_steer_prompts.setdefault(turn_id, []).append(
|
|
835
931
|
shorten_title(prompt, limit=72)
|
|
836
932
|
)
|
|
837
933
|
|
|
838
|
-
def close(self) -> None:
|
|
934
|
+
def close(self) -> 'None':
|
|
839
935
|
if self._prompt_task is not None and not self._prompt_task.done():
|
|
840
936
|
self._prompt_task.cancel()
|
|
841
937
|
self._prompt_task = None
|
|
@@ -843,24 +939,39 @@ class CliSessionView:
|
|
|
843
939
|
if self._stdout_proxy is not None:
|
|
844
940
|
self._stdout_proxy.close()
|
|
845
941
|
|
|
846
|
-
def
|
|
942
|
+
def set_context_window_tokens(
|
|
943
|
+
self,
|
|
944
|
+
context_window_tokens: 'typing.Union[int, None]',
|
|
945
|
+
) -> 'None':
|
|
946
|
+
self._context_window_tokens = context_window_tokens
|
|
947
|
+
self._context_remaining_percent = (
|
|
948
|
+
100 if context_window_tokens is not None else None
|
|
949
|
+
)
|
|
950
|
+
|
|
951
|
+
def finish_stream(self) -> 'None':
|
|
847
952
|
self._finish_stream()
|
|
848
953
|
|
|
849
|
-
def write_line(self, text: str) -> None:
|
|
954
|
+
def write_line(self, text: 'str') -> 'None':
|
|
850
955
|
self._print_line(text)
|
|
851
956
|
|
|
852
|
-
def show_error(self, text: str) -> None:
|
|
957
|
+
def show_error(self, text: 'str') -> 'None':
|
|
853
958
|
self._spinner.finish_turn()
|
|
854
959
|
self._finish_stream()
|
|
960
|
+
lines = str(text).splitlines() or [""]
|
|
961
|
+
formatted = [f"Error: {lines[0]}"]
|
|
962
|
+
formatted.extend(
|
|
963
|
+
f" {line}" if line else ""
|
|
964
|
+
for line in lines[1:]
|
|
965
|
+
)
|
|
855
966
|
self._print_line(
|
|
856
967
|
colorize_cli_message(
|
|
857
|
-
|
|
968
|
+
"\n".join(formatted),
|
|
858
969
|
"error",
|
|
859
970
|
self._color_enabled,
|
|
860
971
|
)
|
|
861
972
|
)
|
|
862
973
|
|
|
863
|
-
def _finish_stream(self) -> None:
|
|
974
|
+
def _finish_stream(self) -> 'None':
|
|
864
975
|
with self._terminal_lock:
|
|
865
976
|
self._spinner.clear()
|
|
866
977
|
if self._streaming:
|
|
@@ -872,9 +983,9 @@ class CliSessionView:
|
|
|
872
983
|
|
|
873
984
|
def _finalize_turn_output(
|
|
874
985
|
self,
|
|
875
|
-
final_text: str,
|
|
876
|
-
allow_standalone_output: bool,
|
|
877
|
-
) -> None:
|
|
986
|
+
final_text: 'str',
|
|
987
|
+
allow_standalone_output: 'bool',
|
|
988
|
+
) -> 'None':
|
|
878
989
|
self._spinner.finish_turn()
|
|
879
990
|
if self._streaming and self._streaming_in_prompt:
|
|
880
991
|
streamed_text = self._prompt_stream_buffer
|
|
@@ -903,7 +1014,7 @@ class CliSessionView:
|
|
|
903
1014
|
)
|
|
904
1015
|
)
|
|
905
1016
|
|
|
906
|
-
def _colorize_formatted_tool_message(self, message: str) -> str:
|
|
1017
|
+
def _colorize_formatted_tool_message(self, message: 'str') -> 'str':
|
|
907
1018
|
if message.startswith("[plan]"):
|
|
908
1019
|
return colorize_cli_message(message, "plan", self._color_enabled)
|
|
909
1020
|
if message.startswith("[exec]"):
|
|
@@ -916,12 +1027,15 @@ class CliSessionView:
|
|
|
916
1027
|
return colorize_cli_message(message, "error", self._color_enabled)
|
|
917
1028
|
return colorize_cli_message(message, "tool", self._color_enabled)
|
|
918
1029
|
|
|
919
|
-
def _print_line(self, text: str) -> None:
|
|
1030
|
+
def _print_line(self, text: 'str') -> 'None':
|
|
920
1031
|
with self._terminal_lock:
|
|
921
1032
|
self._spinner.clear()
|
|
922
1033
|
self._line_output(text)
|
|
923
1034
|
|
|
924
|
-
def
|
|
1035
|
+
def _print_user_turn(self, text: 'str') -> 'None':
|
|
1036
|
+
self._print_line(f"user> {text}")
|
|
1037
|
+
|
|
1038
|
+
def _remember_agent_name(self, tool_name: 'str', summary: 'str') -> 'None':
|
|
925
1039
|
if tool_name != "spawn_agent":
|
|
926
1040
|
return
|
|
927
1041
|
if " (" not in summary or not summary.endswith(")"):
|
|
@@ -933,7 +1047,7 @@ class CliSessionView:
|
|
|
933
1047
|
return
|
|
934
1048
|
self._agent_names[agent_short_id] = nickname
|
|
935
1049
|
|
|
936
|
-
def _rewrite_agent_summary(self, tool_name: str, summary: str) -> str:
|
|
1050
|
+
def _rewrite_agent_summary(self, tool_name: 'str', summary: 'str') -> 'str':
|
|
937
1051
|
if tool_name not in {"wait_agent", "send_input", "resume_agent", "close_agent"}:
|
|
938
1052
|
return summary
|
|
939
1053
|
rewritten = summary
|
|
@@ -945,7 +1059,7 @@ class CliSessionView:
|
|
|
945
1059
|
rewritten = rewritten.replace(agent_short_id, nickname)
|
|
946
1060
|
return rewritten
|
|
947
1061
|
|
|
948
|
-
async def prompt_async(self, prompt: str) -> str:
|
|
1062
|
+
async def prompt_async(self, prompt: 'str') -> 'str':
|
|
949
1063
|
if self._prompt_session is None:
|
|
950
1064
|
self._prompt_session = PromptSession(
|
|
951
1065
|
erase_when_done=True,
|
|
@@ -966,7 +1080,7 @@ class CliSessionView:
|
|
|
966
1080
|
finally:
|
|
967
1081
|
self.set_input_active(False, resume_spinner=False)
|
|
968
1082
|
|
|
969
|
-
async def _handoff_prompt_task_to_output(self) -> None:
|
|
1083
|
+
async def _handoff_prompt_task_to_output(self) -> 'None':
|
|
970
1084
|
if self._prompt_task is None:
|
|
971
1085
|
return
|
|
972
1086
|
prompt_task = self._prompt_task
|