python-codex 0.2.0__tar.gz → 0.2.2__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.
- {python_codex-0.2.0 → python_codex-0.2.2}/PKG-INFO +1 -1
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/interactive_session.py +5 -1
- {python_codex-0.2.0 → python_codex-0.2.2}/pyproject.toml +1 -1
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_workspace_server.py +71 -4
- {python_codex-0.2.0 → python_codex-0.2.2}/workspace_server/app.py +51 -2
- {python_codex-0.2.0 → python_codex-0.2.2}/workspace_server/workspace.html +527 -98
- {python_codex-0.2.0 → python_codex-0.2.2}/.github/workflows/publish.yml +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/.github/workflows/test.yml +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/.gitignore +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/AGENTS.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/LICENSE +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/README.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/README_ZH.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/docs/ALIGNMENT.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/docs/CONTEXT.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/docs/responses_server/README.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/__init__.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/agent.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/cli.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/collaboration.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/compat.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/context.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/doctor.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/feishu_card.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/feishu_link.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/model.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/portable.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/portable_server.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/collaboration_default.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/collaboration_plan.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/default_base_instructions.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/models.json +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/permissions/approval_policy/never.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/permissions/approval_policy/on_failure.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/permissions/approval_policy/on_request.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/permissions/approval_policy/unless_trusted.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/permissions/sandbox_mode/read_only.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/prompts/permissions/sandbox_mode/workspace_write.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/protocol.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/runtime.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/runtime_services.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/__init__.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/agent_tool_schemas.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/apply_patch_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/base_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/close_agent_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/code_mode_manager.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/exec_command_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/exec_runtime.js +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/exec_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/grep_files_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/ipython_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/list_dir_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/read_file_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/request_permissions_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/request_user_input_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/resume_agent_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/send_input_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/shell_command_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/shell_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/spawn_agent_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/unified_exec_manager.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/update_plan_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/view_image_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/wait_agent_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/wait_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/web_search_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/tools/write_stdin_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/__init__.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/async_bridge.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/compactor.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/debug.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/dotenv.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/get_env.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/random_ids.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/session_persist.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/toolcall_visualize.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/truncation.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/pycodex/utils/visualize.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/__init__.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/__main__.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/app.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/config.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/messages_api.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/payload_processors.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/server.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/session_store.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/stream_router.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/tools/__init__.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/tools/custom_adapter.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/tools/web_search.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/responses_server/trajectory_dump.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/TESTS.md +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/__init__.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/compare_request_user_input_roundtrip.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/compare_steer_request_bodies.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/compare_tool_schemas.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/fake_responses_server.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/fakes.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/responses_server/fake_chat_completions_server.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/responses_server/test_server.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_agent.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_async_bridge.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_builtin_tools.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_cli.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_compactor.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_context.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_doctor.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_fake_responses_server.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_feishu_card.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_feishu_link.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_ipython_tool.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_model.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_portable.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_py36_syntax.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tests/test_visualize.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/tools/feishu_oauth.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/workspace_server/__init__.py +0 -0
- {python_codex-0.2.0 → python_codex-0.2.2}/workspace_server/__main__.py +0 -0
|
@@ -18,6 +18,7 @@ import typing
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
EXIT_COMMANDS = {"/exit", "/quit"}
|
|
21
|
+
HELP_COMMAND = "/help"
|
|
21
22
|
HISTORY_COMMAND = "/history"
|
|
22
23
|
TITLE_COMMAND = "/title"
|
|
23
24
|
MODEL_COMMAND = "/model"
|
|
@@ -27,7 +28,7 @@ COMPACT_COMMAND = "/compact"
|
|
|
27
28
|
LINK_COMMAND = "/link"
|
|
28
29
|
UNLINK_COMMAND = "/unlink"
|
|
29
30
|
EXTRA_COMMANDS_LINE = (
|
|
30
|
-
"Extra commands: /history, /title, /model, /resume, /compact, /link, /unlink"
|
|
31
|
+
"Extra commands: /help, /history, /title, /model, /resume, /compact, /link, /unlink"
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
|
|
@@ -232,6 +233,9 @@ async def run_interactive_session(
|
|
|
232
233
|
continue
|
|
233
234
|
if prompt_text in EXIT_COMMANDS:
|
|
234
235
|
break
|
|
236
|
+
if prompt_text == HELP_COMMAND:
|
|
237
|
+
view.write_line(EXTRA_COMMANDS_LINE)
|
|
238
|
+
continue
|
|
235
239
|
if prompt_text == HISTORY_COMMAND:
|
|
236
240
|
view.show_history()
|
|
237
241
|
continue
|
|
@@ -8,6 +8,7 @@ import pytest
|
|
|
8
8
|
|
|
9
9
|
from pycodex import (
|
|
10
10
|
Agent,
|
|
11
|
+
AgentEvent,
|
|
11
12
|
AssistantMessage,
|
|
12
13
|
BaseTool,
|
|
13
14
|
CliSubmissionQueue,
|
|
@@ -193,6 +194,7 @@ def test_workspace_app_missing_board_returns_pending_page(tmp_path) -> None:
|
|
|
193
194
|
app = create_app(lambda: link, board)
|
|
194
195
|
|
|
195
196
|
with TestClient(app) as client:
|
|
197
|
+
shell = client.get("/")
|
|
196
198
|
response = client.get("/board")
|
|
197
199
|
status = client.get("/api/board")
|
|
198
200
|
|
|
@@ -200,6 +202,9 @@ def test_workspace_app_missing_board_returns_pending_page(tmp_path) -> None:
|
|
|
200
202
|
assert "Board pending" in response.text
|
|
201
203
|
assert str(board) in response.text
|
|
202
204
|
assert status.json() == {"exists": False}
|
|
205
|
+
assert "let lastBoardSignature = null;" in shell.text
|
|
206
|
+
assert "if (lastBoardSignature === null)" in shell.text
|
|
207
|
+
assert "boardFrame.src = `board?t=${Date.now()}`;" in shell.text
|
|
203
208
|
|
|
204
209
|
|
|
205
210
|
def test_workspace_app_session_snapshot_uses_turns_not_raw_messages(tmp_path) -> None:
|
|
@@ -218,6 +223,9 @@ def test_workspace_app_session_snapshot_uses_turns_not_raw_messages(tmp_path) ->
|
|
|
218
223
|
assert snapshot["turns"][0]["response"] == "pong"
|
|
219
224
|
assert snapshot["turns"][0]["thinking"] == ""
|
|
220
225
|
|
|
226
|
+
sessions = response.json()["sessions"]
|
|
227
|
+
assert sessions[0]["last_assistant"] == "pong"
|
|
228
|
+
|
|
221
229
|
|
|
222
230
|
def test_workspace_app_session_endpoint_returns_spinner_and_turns(tmp_path) -> None:
|
|
223
231
|
board = tmp_path / "board.html"
|
|
@@ -236,6 +244,26 @@ def test_workspace_app_session_endpoint_returns_spinner_and_turns(tmp_path) -> N
|
|
|
236
244
|
assert snapshot["turns"][0]["response"] == "response"
|
|
237
245
|
|
|
238
246
|
|
|
247
|
+
def test_workspace_view_tracks_context_remaining_percent() -> None:
|
|
248
|
+
view = WebSessionView()
|
|
249
|
+
assert view.snapshot()["context_remaining_percent"] is None
|
|
250
|
+
|
|
251
|
+
view.set_context_window_tokens(100_000)
|
|
252
|
+
assert view.snapshot()["context_remaining_percent"] == 100
|
|
253
|
+
assert view.summary()["context_remaining_percent"] == 100
|
|
254
|
+
|
|
255
|
+
view.handle_event(
|
|
256
|
+
AgentEvent(
|
|
257
|
+
kind="token_count",
|
|
258
|
+
turn_id="turn_1",
|
|
259
|
+
payload={"usage": {"total_tokens": 20_800}},
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
assert view.snapshot()["context_remaining_percent"] == 90
|
|
264
|
+
assert view.summary()["context_remaining_percent"] == 90
|
|
265
|
+
|
|
266
|
+
|
|
239
267
|
def test_workspace_app_shell_uses_spinner_without_send_button(tmp_path) -> None:
|
|
240
268
|
board = tmp_path / "board.html"
|
|
241
269
|
board.write_text("<!doctype html><title>Board</title>", encoding="utf-8")
|
|
@@ -247,7 +275,30 @@ def test_workspace_app_shell_uses_spinner_without_send_button(tmp_path) -> None:
|
|
|
247
275
|
response = client.get("/")
|
|
248
276
|
|
|
249
277
|
assert response.status_code == 200
|
|
250
|
-
assert '
|
|
278
|
+
assert 'class="spinner-wrap"' in response.text
|
|
279
|
+
assert 'class="prompt-row"' in response.text
|
|
280
|
+
assert 'id="spinner" class="spinner" type="button" role="checkbox"' in response.text
|
|
281
|
+
assert 'id="toast" class="toast" role="status"' in response.text
|
|
282
|
+
assert '<textarea id="prompt" rows="1"' in response.text
|
|
283
|
+
assert ".spinner-wrap:not(:has(.spinner:not(:empty)))" in response.text
|
|
284
|
+
assert response.text.index('class="prompt-row"') < response.text.index('id="contextMeter"')
|
|
285
|
+
assert response.text.index('id="contextMeter"') < response.text.index('id="prompt"')
|
|
286
|
+
assert 'id="contextMeter" class="context-meter hidden" aria-hidden="true"' in response.text
|
|
287
|
+
assert 'id="contextMeterValue" class="context-meter-value"' in response.text
|
|
288
|
+
assert 'contextMeter.style.setProperty("--context-remaining", `${value}%`)' in response.text
|
|
289
|
+
assert "contextMeter.setAttribute(\"aria-valuenow\"" not in response.text
|
|
290
|
+
assert "contextMeterValue.textContent = String(value)" in response.text
|
|
291
|
+
assert "sessionId === activeSessionId && spinnerText" in response.text
|
|
292
|
+
assert "spinner.addEventListener(\"click\", toggleSpinnerNotification)" in response.text
|
|
293
|
+
assert "new Notification(`session: ${title}`" in response.text
|
|
294
|
+
assert "body: lastAssistant" in response.text
|
|
295
|
+
assert "notifySessionDone(sessionId, session)" in response.text
|
|
296
|
+
assert "notify-on-done enabled" in response.text
|
|
297
|
+
assert "notify-on-done canceled" in response.text
|
|
298
|
+
assert "browser notifications blocked" in response.text
|
|
299
|
+
assert "requestNotificationPermission" in response.text
|
|
300
|
+
assert 'document.addEventListener("visibilitychange"' in response.text
|
|
301
|
+
assert 'document.visibilityState !== "visible"' in response.text
|
|
251
302
|
assert ">Send<" not in response.text
|
|
252
303
|
assert "Enter sends. Shift+Enter adds a newline." not in response.text
|
|
253
304
|
assert "compositionstart" in response.text
|
|
@@ -281,7 +332,18 @@ def test_workspace_spinner_tool_call_preview_is_longer_than_title() -> None:
|
|
|
281
332
|
assert spinner.startswith("calling exec_command(")
|
|
282
333
|
|
|
283
334
|
|
|
284
|
-
|
|
335
|
+
@pytest.mark.parametrize(
|
|
336
|
+
("command", "expected_response"),
|
|
337
|
+
[
|
|
338
|
+
("/history", "No history yet."),
|
|
339
|
+
("/help", "/history"),
|
|
340
|
+
],
|
|
341
|
+
)
|
|
342
|
+
def test_workspace_app_message_uses_shared_interactive_commands(
|
|
343
|
+
tmp_path,
|
|
344
|
+
command: str,
|
|
345
|
+
expected_response: str,
|
|
346
|
+
) -> None:
|
|
285
347
|
board = tmp_path / "board.html"
|
|
286
348
|
board.write_text("<!doctype html><title>Board</title>", encoding="utf-8")
|
|
287
349
|
model = ScriptedModelClient(
|
|
@@ -295,10 +357,11 @@ def test_workspace_app_message_uses_shared_interactive_commands(tmp_path) -> Non
|
|
|
295
357
|
app = create_app(build_session, board)
|
|
296
358
|
|
|
297
359
|
with TestClient(app) as client:
|
|
298
|
-
response = client.post("/api/session/message", json={"prompt":
|
|
360
|
+
response = client.post("/api/session/message", json={"prompt": command})
|
|
299
361
|
snapshot = _wait_for_snapshot(
|
|
300
362
|
client,
|
|
301
|
-
lambda item: item["turns"]
|
|
363
|
+
lambda item: item["turns"]
|
|
364
|
+
and expected_response in item["turns"][-1]["response"],
|
|
302
365
|
)
|
|
303
366
|
|
|
304
367
|
assert response.status_code == 200
|
|
@@ -871,6 +934,8 @@ def test_workspace_app_session_list_uses_lightweight_summary(tmp_path) -> None:
|
|
|
871
934
|
"running": True,
|
|
872
935
|
"spinner": "thinking",
|
|
873
936
|
"turn_count": 123,
|
|
937
|
+
"last_assistant": "latest answer",
|
|
938
|
+
"context_remaining_percent": 48,
|
|
874
939
|
}
|
|
875
940
|
|
|
876
941
|
def snapshot(self):
|
|
@@ -886,6 +951,8 @@ def test_workspace_app_session_list_uses_lightweight_summary(tmp_path) -> None:
|
|
|
886
951
|
assert response.json()["sessions"][0]["running"] is True
|
|
887
952
|
assert response.json()["sessions"][0]["spinner"] == "thinking"
|
|
888
953
|
assert response.json()["sessions"][0]["turn_count"] == 123
|
|
954
|
+
assert response.json()["sessions"][0]["last_assistant"] == "latest answer"
|
|
955
|
+
assert response.json()["sessions"][0]["context_remaining_percent"] == 48
|
|
889
956
|
|
|
890
957
|
|
|
891
958
|
def test_workspace_app_websocket_ping(tmp_path) -> None:
|
|
@@ -25,7 +25,12 @@ from pycodex.utils.session_persist import (
|
|
|
25
25
|
load_resumed_session_path,
|
|
26
26
|
)
|
|
27
27
|
from pycodex.utils import uuid7_string
|
|
28
|
-
from pycodex.utils.visualize import
|
|
28
|
+
from pycodex.utils.visualize import (
|
|
29
|
+
IDLE_LISTENING_STATUS,
|
|
30
|
+
percent_of_context_window_remaining,
|
|
31
|
+
shorten_title,
|
|
32
|
+
tool_summary,
|
|
33
|
+
)
|
|
29
34
|
import typing
|
|
30
35
|
|
|
31
36
|
|
|
@@ -180,6 +185,8 @@ class WebSessionView:
|
|
|
180
185
|
self._title = ""
|
|
181
186
|
self._spinner_status = ""
|
|
182
187
|
self._stream_buffer = ""
|
|
188
|
+
self._context_window_tokens: "typing.Union[int, None]" = None
|
|
189
|
+
self._context_remaining_percent: "typing.Union[int, None]" = None
|
|
183
190
|
self._closed = False
|
|
184
191
|
self._server_loop: "typing.Union[asyncio.AbstractEventLoop, None]" = None
|
|
185
192
|
self._worker_loop: "typing.Union[asyncio.AbstractEventLoop, None]" = None
|
|
@@ -347,7 +354,11 @@ class WebSessionView:
|
|
|
347
354
|
self,
|
|
348
355
|
context_window_tokens: "typing.Union[int, None]",
|
|
349
356
|
) -> None:
|
|
350
|
-
|
|
357
|
+
with self._lock:
|
|
358
|
+
self._context_window_tokens = context_window_tokens
|
|
359
|
+
self._context_remaining_percent = (
|
|
360
|
+
100 if context_window_tokens is not None else None
|
|
361
|
+
)
|
|
351
362
|
|
|
352
363
|
def subscribe(self) -> "asyncio.Queue":
|
|
353
364
|
queue: "asyncio.Queue" = asyncio.Queue()
|
|
@@ -386,6 +397,7 @@ class WebSessionView:
|
|
|
386
397
|
"spinner": self._spinner_status,
|
|
387
398
|
"model": "pycodex",
|
|
388
399
|
"title": self._title,
|
|
400
|
+
"context_remaining_percent": self._context_remaining_percent,
|
|
389
401
|
"turns": [_public_turn(turn) for turn in self._turns[-80:]],
|
|
390
402
|
}
|
|
391
403
|
|
|
@@ -396,6 +408,8 @@ class WebSessionView:
|
|
|
396
408
|
"spinner": self._spinner_status,
|
|
397
409
|
"title": self._title,
|
|
398
410
|
"turn_count": len(self._turns),
|
|
411
|
+
"last_assistant": _last_assistant_text(self._turns),
|
|
412
|
+
"context_remaining_percent": self._context_remaining_percent,
|
|
399
413
|
}
|
|
400
414
|
|
|
401
415
|
def _apply_runtime_event(self, event: "AgentEvent") -> None:
|
|
@@ -403,6 +417,9 @@ class WebSessionView:
|
|
|
403
417
|
payload = getattr(event, "payload", {})
|
|
404
418
|
if not isinstance(payload, dict):
|
|
405
419
|
payload = {}
|
|
420
|
+
if kind == "token_count":
|
|
421
|
+
self._update_context_window(payload.get("usage"))
|
|
422
|
+
return
|
|
406
423
|
turn_id = str(payload.get("turn_id") or getattr(event, "turn_id", "") or "")
|
|
407
424
|
submission_id = str(payload.get("submission_id") or turn_id or "")
|
|
408
425
|
turn = self._turns_by_submission_id.get(submission_id)
|
|
@@ -475,6 +492,22 @@ class WebSessionView:
|
|
|
475
492
|
turn["status"] = "interrupted"
|
|
476
493
|
self._stream_buffer = ""
|
|
477
494
|
|
|
495
|
+
def _update_context_window(self, usage: "object") -> None:
|
|
496
|
+
if self._context_window_tokens is None:
|
|
497
|
+
return
|
|
498
|
+
if not isinstance(usage, dict):
|
|
499
|
+
self._context_remaining_percent = None
|
|
500
|
+
return
|
|
501
|
+
try:
|
|
502
|
+
total_tokens = int(usage["total_tokens"])
|
|
503
|
+
except (KeyError, TypeError, ValueError):
|
|
504
|
+
self._context_remaining_percent = None
|
|
505
|
+
return
|
|
506
|
+
self._context_remaining_percent = percent_of_context_window_remaining(
|
|
507
|
+
total_tokens,
|
|
508
|
+
self._context_window_tokens,
|
|
509
|
+
)
|
|
510
|
+
|
|
478
511
|
def _apply_spinner_event(
|
|
479
512
|
self,
|
|
480
513
|
kind: str,
|
|
@@ -974,6 +1007,10 @@ class WorkspaceSessionManager:
|
|
|
974
1007
|
"running": bool(summary.get("running")),
|
|
975
1008
|
"spinner": summary.get("spinner") or "",
|
|
976
1009
|
"turn_count": summary.get("turn_count") or 0,
|
|
1010
|
+
"last_assistant": summary.get("last_assistant") or "",
|
|
1011
|
+
"context_remaining_percent": summary.get(
|
|
1012
|
+
"context_remaining_percent"
|
|
1013
|
+
),
|
|
977
1014
|
}
|
|
978
1015
|
)
|
|
979
1016
|
return result
|
|
@@ -1178,9 +1215,21 @@ def _session_summary(session) -> "typing.Dict[str, object]":
|
|
|
1178
1215
|
"running": bool(snapshot.get("running")),
|
|
1179
1216
|
"spinner": snapshot.get("spinner") or "",
|
|
1180
1217
|
"turn_count": len(snapshot.get("turns") or []),
|
|
1218
|
+
"last_assistant": _last_assistant_text(snapshot.get("turns") or []),
|
|
1219
|
+
"context_remaining_percent": snapshot.get("context_remaining_percent"),
|
|
1181
1220
|
}
|
|
1182
1221
|
|
|
1183
1222
|
|
|
1223
|
+
def _last_assistant_text(turns: "typing.Iterable[typing.Dict[str, object]]") -> str:
|
|
1224
|
+
for turn in reversed(list(turns)):
|
|
1225
|
+
if str(turn.get("kind") or "assistant") == "control":
|
|
1226
|
+
continue
|
|
1227
|
+
response = str(turn.get("response") or "").strip()
|
|
1228
|
+
if response:
|
|
1229
|
+
return response
|
|
1230
|
+
return ""
|
|
1231
|
+
|
|
1232
|
+
|
|
1184
1233
|
def _public_turn(turn: "typing.Dict[str, object]") -> "typing.Dict[str, object]":
|
|
1185
1234
|
return typing.cast(
|
|
1186
1235
|
"typing.Dict[str, object]",
|