codex-agent-framework 0.1.13__tar.gz → 0.1.14__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.
Files changed (93) hide show
  1. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/CHANGELOG.md +20 -0
  2. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/MANIFEST.in +2 -0
  3. {codex_agent_framework-0.1.13/codex_agent_framework.egg-info → codex_agent_framework-0.1.14}/PKG-INFO +29 -4
  4. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/README.md +28 -3
  5. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/__init__.py +2 -0
  6. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/__main__.py +53 -3
  7. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/agent.py +31 -23
  8. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/ai.py +22 -0
  9. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_commands.py +1 -0
  10. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_plugins/browser/__init__.py +29 -21
  11. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_plugins/browser/controller.py +48 -8
  12. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_plugins/desktop/__init__.py +113 -301
  13. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_plugins/desktop/controller.py +34 -11
  14. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_plugins/memory/__init__.py +33 -10
  15. codex_agent_framework-0.1.14/codex_agent/builtin_plugins/planner/__init__.py +266 -0
  16. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_plugins/scheduler/__init__.py +12 -4
  17. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_tools/files.py +23 -2
  18. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_tools/shell.py +8 -2
  19. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/chat.py +30 -5
  20. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/client.py +0 -2
  21. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/config.py +18 -8
  22. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/context.py +6 -1
  23. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/event.py +5 -0
  24. codex_agent_framework-0.1.14/codex_agent/hooks.py +71 -0
  25. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/mainloop.py +44 -18
  26. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/message.py +6 -0
  27. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/plugin.py +21 -1
  28. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/prompts/system_prompt.txt +1 -14
  29. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/registry.py +8 -1
  30. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/scheduler.py +7 -11
  31. codex_agent_framework-0.1.14/codex_agent/scripts/install-system-dependencies.sh +149 -0
  32. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/sessions.py +36 -0
  33. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/utils.py +21 -1
  34. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14/codex_agent_framework.egg-info}/PKG-INFO +29 -4
  35. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent_framework.egg-info/SOURCES.txt +6 -0
  36. codex_agent_framework-0.1.14/dependencies.txt +88 -0
  37. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/pyproject.toml +2 -2
  38. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_agent.py +314 -124
  39. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_browser.py +33 -0
  40. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_chat.py +14 -8
  41. codex_agent_framework-0.1.14/tests/test_cli.py +139 -0
  42. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_client.py +2 -2
  43. codex_agent_framework-0.1.14/tests/test_hooks.py +188 -0
  44. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_local_desktop.py +62 -0
  45. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_memory.py +50 -3
  46. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_messages.py +16 -0
  47. codex_agent_framework-0.1.14/tests/test_planner.py +133 -0
  48. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_utils.py +12 -2
  49. codex_agent_framework-0.1.13/tests/test_cli.py +0 -65
  50. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/LICENSE +0 -0
  51. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/agent_runtime.py +0 -0
  52. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_plugins/__init__.py +0 -0
  53. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_plugins/desktop/screenshot.py +0 -0
  54. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_providers.py +0 -0
  55. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_tools/__init__.py +0 -0
  56. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_tools/server_tools.py +0 -0
  57. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_tools/system.py +0 -0
  58. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/builtin_tools/vision.py +0 -0
  59. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/command.py +0 -0
  60. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/get_text/__init__.py +0 -0
  61. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/get_text/default_gitignore +0 -0
  62. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/get_text/get_text.py +0 -0
  63. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/get_text/simpler_get_text.py +0 -0
  64. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/image.py +0 -0
  65. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/latex.py +0 -0
  66. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/memory.py +0 -0
  67. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
  68. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/provider.py +0 -0
  69. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/runtime.py +0 -0
  70. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/server.py +0 -0
  71. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/service.py +0 -0
  72. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/status.py +0 -0
  73. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/stream_utils.py +0 -0
  74. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/tool.py +0 -0
  75. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/tray.py +0 -0
  76. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/tui.py +0 -0
  77. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/voice.py +0 -0
  78. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent/worker.py +0 -0
  79. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  80. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  81. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent_framework.egg-info/requires.txt +0 -0
  82. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/codex_agent_framework.egg-info/top_level.txt +0 -0
  83. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/setup.cfg +0 -0
  84. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_ai.py +0 -0
  85. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_events.py +0 -0
  86. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_get_text_browser.py +0 -0
  87. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_image_message.py +0 -0
  88. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_scheduler.py +0 -0
  89. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_server.py +0 -0
  90. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_service.py +0 -0
  91. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_tray.py +0 -0
  92. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_tui.py +0 -0
  93. {codex_agent_framework-0.1.13 → codex_agent_framework-0.1.14}/tests/test_worker.py +0 -0
@@ -4,6 +4,26 @@ 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
6
 
7
+ ## [0.1.14] - 2026-05-07
8
+ ### Added
9
+ - Add a general `HookManager` with compatibility helpers and hook sites around message submission, commands, context/provider calls, runtime module loading, tool calls, shell execution, and file write/edit operations.
10
+ - Add planner and scheduler built-in plugins with atomic JSON persistence safeguards.
11
+ - Add `dependencies.txt` documenting non-Python system dependencies for browser, desktop, tray, service, TUI terminal, and audio features.
12
+ - Add packaged system dependency installation via `codex-agent install-system-deps` and a higher-level `codex-agent bootstrap` command that can install system dependencies then install/start the user services.
13
+
14
+ ### Changed
15
+ - Improve TUI SSE replay/reconnect handling so replayed deltas are rendered as a compact catch-up while live deltas continue streaming normally.
16
+ - Keep the TUI reconnection status concise by reporting one offline message per outage and a connected message on recovery.
17
+
18
+ ### Fixed
19
+ - Fix SSE cursor tracking so client replay state advances only from real sequenced events, avoiding missed events after reconnects.
20
+ - Surface asynchronous memory archive failures through `MemoryArchiveErrorEvent` instead of swallowing them silently.
21
+ - Clarify scheduler wakeup trigger event data around `turn_id` while preserving backward compatibility.
22
+
23
+ ### Tests
24
+ - Expand regression coverage for hooks, planner schema/loading, scheduler persistence, TUI reconnect/replay behavior, CLI bootstrap/system dependency commands, and memory archive errors.
25
+ - Validate the full suite at 412 passing tests.
26
+
7
27
  ## [0.1.13] - 2026-05-07
8
28
  ### Changed
9
29
  - Reorganize built-in tools into a `codex_agent.builtin_tools` package with separate modules for file, shell, vision, system, and server-tool responsibilities.
@@ -3,5 +3,7 @@ include LICENSE
3
3
  include CHANGELOG.md
4
4
  recursive-include codex_agent/prompts *.txt
5
5
  include codex_agent/get_text/default_gitignore
6
+ recursive-include codex_agent/scripts *.sh
7
+ include dependencies.txt
6
8
  recursive-exclude * __pycache__
7
9
  recursive-exclude * *.py[co]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -74,7 +74,9 @@ It provides a reusable `Agent` abstraction with persistent sessions, local tools
74
74
  - Python 3.10 or newer.
75
75
  - A working model backend configuration compatible with `codex-backend-sdk` / OpenAI usage.
76
76
  - GTK 3 plus Ayatana AppIndicator or AppIndicator bindings are needed for the tray controller on Linux.
77
- - Playwright's managed Chromium browser is used for rendered web extraction when static fetching is insufficient. Install it with `python -m playwright install chromium` if needed.
77
+ - Desktop automation on Linux/X11 uses explicit system tools: `wmctrl`, `xdotool`, and `xclip`.
78
+ - Playwright's managed Chromium browser is used for rendered web extraction when static fetching is insufficient.
79
+ - See `dependencies.txt` for the full list of optional non-Python system dependencies.
78
80
 
79
81
  ## Installation
80
82
 
@@ -90,6 +92,26 @@ For development:
90
92
  python -m pip install -e '.[dev]'
91
93
  ```
92
94
 
95
+ Install optional Linux system dependencies for browser, desktop, tray, service, terminal, and audio features:
96
+
97
+ ```bash
98
+ codex-agent install-system-deps -- -y
99
+ ```
100
+
101
+ Or bootstrap a local desktop setup by installing system dependencies and then installing/starting the user services:
102
+
103
+ ```bash
104
+ codex-agent bootstrap -- -y
105
+ ```
106
+
107
+ Useful bootstrap variants:
108
+
109
+ ```bash
110
+ codex-agent bootstrap --no-system-deps
111
+ codex-agent bootstrap --no-start-service -- -y
112
+ codex-agent install-system-deps -- --dry-run --no-tray --no-audio
113
+ ```
114
+
93
115
  ## Quick start
94
116
 
95
117
  Run the local server plus terminal TUI:
@@ -122,6 +144,8 @@ Install the user services for the server and tray controller:
122
144
  codex-agent install-service
123
145
  ```
124
146
 
147
+ For a first-time Linux desktop setup, `codex-agent bootstrap -- -y` combines optional system dependency installation with service installation/startup. The command forwards arguments after `--` to the system dependency installer.
148
+
125
149
  Or use the agent from Python:
126
150
 
127
151
  ```python
@@ -388,7 +412,7 @@ The tests isolate `AGENT_RUNTIME_DIR` automatically, so they should not create o
388
412
  Current baseline:
389
413
 
390
414
  ```text
391
- 376 passed
415
+ 412 passed
392
416
  ```
393
417
 
394
418
  ## Packaging
@@ -400,9 +424,10 @@ python -m pip install build
400
424
  python -m build
401
425
  ```
402
426
 
403
- The distribution includes prompt text files and `codex_agent/get_text/default_gitignore` through package data and `MANIFEST.in`.
427
+ The distribution includes prompt text files, `codex_agent/get_text/default_gitignore`, and the packaged Linux system dependency installer through package data and `MANIFEST.in`.
404
428
 
405
429
  ## Recent changes
430
+ - `0.1.14`: add HookManager infrastructure, planner/scheduler robustness fixes, documented system dependencies, `codex-agent install-system-deps`, `codex-agent bootstrap`, and improved TUI SSE reconnect/replay handling.
406
431
  - `0.1.13`: reorganize built-in tools into a `codex_agent.builtin_tools` package, keeping the public import surface compatible while separating file, shell, vision, system, and server-tool modules.
407
432
  - `0.1.12`: add a persistent Playwright/Chromium browser controller with tab navigation, DOM/action snapshots, screenshots, form/click/key tools, and `browser_goto(url)` for active-tab navigation.
408
433
  - `0.1.11`: split strict line-numbered UTF-8 `read` from unnumbered extracted `view`, preserve blank lines in read snippets, and show persistent+temporary message counts in the TUI status bar.
@@ -25,7 +25,9 @@ It provides a reusable `Agent` abstraction with persistent sessions, local tools
25
25
  - Python 3.10 or newer.
26
26
  - A working model backend configuration compatible with `codex-backend-sdk` / OpenAI usage.
27
27
  - GTK 3 plus Ayatana AppIndicator or AppIndicator bindings are needed for the tray controller on Linux.
28
- - Playwright's managed Chromium browser is used for rendered web extraction when static fetching is insufficient. Install it with `python -m playwright install chromium` if needed.
28
+ - Desktop automation on Linux/X11 uses explicit system tools: `wmctrl`, `xdotool`, and `xclip`.
29
+ - Playwright's managed Chromium browser is used for rendered web extraction when static fetching is insufficient.
30
+ - See `dependencies.txt` for the full list of optional non-Python system dependencies.
29
31
 
30
32
  ## Installation
31
33
 
@@ -41,6 +43,26 @@ For development:
41
43
  python -m pip install -e '.[dev]'
42
44
  ```
43
45
 
46
+ Install optional Linux system dependencies for browser, desktop, tray, service, terminal, and audio features:
47
+
48
+ ```bash
49
+ codex-agent install-system-deps -- -y
50
+ ```
51
+
52
+ Or bootstrap a local desktop setup by installing system dependencies and then installing/starting the user services:
53
+
54
+ ```bash
55
+ codex-agent bootstrap -- -y
56
+ ```
57
+
58
+ Useful bootstrap variants:
59
+
60
+ ```bash
61
+ codex-agent bootstrap --no-system-deps
62
+ codex-agent bootstrap --no-start-service -- -y
63
+ codex-agent install-system-deps -- --dry-run --no-tray --no-audio
64
+ ```
65
+
44
66
  ## Quick start
45
67
 
46
68
  Run the local server plus terminal TUI:
@@ -73,6 +95,8 @@ Install the user services for the server and tray controller:
73
95
  codex-agent install-service
74
96
  ```
75
97
 
98
+ For a first-time Linux desktop setup, `codex-agent bootstrap -- -y` combines optional system dependency installation with service installation/startup. The command forwards arguments after `--` to the system dependency installer.
99
+
76
100
  Or use the agent from Python:
77
101
 
78
102
  ```python
@@ -339,7 +363,7 @@ The tests isolate `AGENT_RUNTIME_DIR` automatically, so they should not create o
339
363
  Current baseline:
340
364
 
341
365
  ```text
342
- 376 passed
366
+ 412 passed
343
367
  ```
344
368
 
345
369
  ## Packaging
@@ -351,9 +375,10 @@ python -m pip install build
351
375
  python -m build
352
376
  ```
353
377
 
354
- The distribution includes prompt text files and `codex_agent/get_text/default_gitignore` through package data and `MANIFEST.in`.
378
+ The distribution includes prompt text files, `codex_agent/get_text/default_gitignore`, and the packaged Linux system dependency installer through package data and `MANIFEST.in`.
355
379
 
356
380
  ## Recent changes
381
+ - `0.1.14`: add HookManager infrastructure, planner/scheduler robustness fixes, documented system dependencies, `codex-agent install-system-deps`, `codex-agent bootstrap`, and improved TUI SSE reconnect/replay handling.
357
382
  - `0.1.13`: reorganize built-in tools into a `codex_agent.builtin_tools` package, keeping the public import surface compatible while separating file, shell, vision, system, and server-tool modules.
358
383
  - `0.1.12`: add a persistent Playwright/Chromium browser controller with tab navigation, DOM/action snapshots, screenshots, form/click/key tools, and `browser_goto(url)` for active-tab navigation.
359
384
  - `0.1.11`: split strict line-numbered UTF-8 `read` from unnumbered extracted `view`, preserve blank lines in read snippets, and show persistent+temporary message counts in the TUI status bar.
@@ -37,6 +37,7 @@ from .message import (
37
37
  AssistantMessage,
38
38
  CompactionMessage,
39
39
  DeveloperMessage,
40
+ InterruptMessage,
40
41
  Message,
41
42
  MessageChunk,
42
43
  ProviderMessage,
@@ -98,6 +99,7 @@ __all__ = [
98
99
  "AssistantMessage",
99
100
  "CompactionMessage",
100
101
  "DeveloperMessage",
102
+ "InterruptMessage",
101
103
  "Message",
102
104
  "MessageChunk",
103
105
  "ProviderMessage",
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+ import subprocess
2
3
 
3
4
  from . import Agent
4
5
  from .chat import Chat
@@ -47,6 +48,33 @@ def run_tray_mode(base_url="http://127.0.0.1:8765"):
47
48
  run_tray(base_url=base_url)
48
49
 
49
50
 
51
+ def run_system_deps_installer(extra_args=None):
52
+ script = Path(__file__).parent / "scripts" / "install-system-dependencies.sh"
53
+ if not script.is_file():
54
+ raise RuntimeError(f"System dependency installer not found: {script}")
55
+ return subprocess.call([str(script), *(extra_args or [])])
56
+
57
+
58
+ def run_bootstrap(
59
+ host="127.0.0.1",
60
+ port=8765,
61
+ *,
62
+ system_deps_args=None,
63
+ install_system_deps=True,
64
+ install_service=True,
65
+ start_service=True,
66
+ ):
67
+ if install_system_deps:
68
+ code = run_system_deps_installer(system_deps_args or [])
69
+ if code:
70
+ return code
71
+ if install_service:
72
+ path = install_user_service(host=host, port=port, enable=True, start=start_service)
73
+ action = "Installed and started" if start_service else "Installed"
74
+ print(f"{action} user services: {path}")
75
+ return 0
76
+
77
+
50
78
  def run_local(host="127.0.0.1", port=8765):
51
79
  import threading
52
80
  import time
@@ -77,14 +105,19 @@ def main(argv=None):
77
105
  parser.add_argument(
78
106
  "mode",
79
107
  nargs="?",
80
- choices=["local", "server", "chat", "tray", "install-service", "uninstall-service"],
108
+ choices=["local", "server", "chat", "tray", "install-service", "uninstall-service", "install-system-deps", "bootstrap"],
81
109
  default="local",
82
- help="local starts an embedded server then the TUI client; server runs only the API; chat connects to an existing API; tray starts the desktop tray controller; install-service installs a user systemd service.",
110
+ help="local starts an embedded server then the TUI client; server runs only the API; chat connects to an existing API; tray starts the desktop tray controller; install-service installs a user systemd service; install-system-deps installs optional OS packages; bootstrap installs system deps then user services.",
83
111
  )
84
112
  parser.add_argument("--host", default="127.0.0.1")
85
113
  parser.add_argument("--port", type=int, default=8765)
86
114
  parser.add_argument("--url", default=None, help="Base URL for chat mode, e.g. http://127.0.0.1:8765")
87
- args = parser.parse_args(argv)
115
+ parser.add_argument("--no-system-deps", action="store_true", help="With bootstrap, skip OS dependency installation.")
116
+ parser.add_argument("--no-service", action="store_true", help="With bootstrap, skip user service installation.")
117
+ parser.add_argument("--no-start-service", action="store_true", help="With bootstrap, install services but do not start them.")
118
+ args, extra_args = parser.parse_known_args(argv)
119
+ if args.mode not in {"install-system-deps", "bootstrap"} and extra_args:
120
+ parser.error(f"unrecognized arguments: {' '.join(extra_args)}")
88
121
 
89
122
  if args.mode == "server":
90
123
  run_server(args.host, args.port)
@@ -98,6 +131,23 @@ def main(argv=None):
98
131
  elif args.mode == "uninstall-service":
99
132
  path = uninstall_user_service(disable=True, stop=True)
100
133
  print(f"Uninstalled user service: {path}")
134
+ elif args.mode == "install-system-deps":
135
+ installer_args = extra_args
136
+ if installer_args and installer_args[0] == "--":
137
+ installer_args = installer_args[1:]
138
+ raise SystemExit(run_system_deps_installer(installer_args))
139
+ elif args.mode == "bootstrap":
140
+ installer_args = extra_args
141
+ if installer_args and installer_args[0] == "--":
142
+ installer_args = installer_args[1:]
143
+ raise SystemExit(run_bootstrap(
144
+ host=args.host,
145
+ port=args.port,
146
+ system_deps_args=installer_args,
147
+ install_system_deps=not args.no_system_deps,
148
+ install_service=not args.no_service,
149
+ start_service=not args.no_start_service,
150
+ ))
101
151
  else:
102
152
  run_local(args.host, args.port)
103
153
 
@@ -7,6 +7,7 @@ from .image import ImageMessage
7
7
  from .command import CommandManager
8
8
  from .config import ConfigManager
9
9
  from .context import ContextManager
10
+ from .hooks import HookManager
10
11
  from .mainloop import MainLoopManager
11
12
  from .plugin import PluginsManager
12
13
  from .registry import RegistryManager
@@ -14,7 +15,7 @@ from .runtime import RuntimeManager
14
15
  from .sessions import SessionsManager
15
16
  from .status import StatusManager
16
17
  from .tool import ImageGenerationTool, reset_current_agent, set_current_agent
17
- from .ai import AIClient
18
+ from .ai import AIBadRequestError, AIClient
18
19
  from .voice import VoiceProcessor
19
20
  from .latex import LaTeXProcessor
20
21
  from .event import AgentInterruptRequestedEvent, AgentInterrupted, AgentInterruptedEvent, AudioPlaybackEvent, EventBus
@@ -49,7 +50,7 @@ class Agent:
49
50
  self.plugins=self.plugins_manager.plugins
50
51
  self.sessions_manager=SessionsManager(self)
51
52
  self.runtime=RuntimeManager(self)
52
- self.hooks=modict()
53
+ self.hooks=HookManager(self)
53
54
  self.on(AudioPlaybackEvent, self.handle_audio_playback)
54
55
  self.mainloop_manager=MainLoopManager(self)
55
56
  self._interrupt_requested=ThreadEvent()
@@ -126,7 +127,7 @@ class Agent:
126
127
  self.emit(AgentInterruptedEvent, reason=reason)
127
128
  self.emit(AgentInterruptRequestedEvent, reason=reason, interrupted=True, error=str(abort_error) if abort_error else "")
128
129
  return True
129
-
130
+
130
131
  def clear_interrupt(self):
131
132
  self._interrupt_requested.clear()
132
133
 
@@ -189,7 +190,6 @@ class Agent:
189
190
  if self.mainloop_manager.dumping_pending:
190
191
  return self.sessions_manager.append_message(msg)
191
192
  return self.mainloop_manager.add_pending_message(msg)
192
-
193
193
 
194
194
  def new_message(self,pending=False,**kwargs):
195
195
  """Helper to create and add a message based on provided keyword arguments.
@@ -203,7 +203,7 @@ class Agent:
203
203
  """
204
204
  msg=message_from_data(kwargs)
205
205
  return self.add_message(msg,pending=pending)
206
-
206
+
207
207
  def add_image(self,source,pending=False,**kwargs):
208
208
  if source:
209
209
  img=ImageMessage(
@@ -247,21 +247,24 @@ class Agent:
247
247
  if hasattr(self, "mainloop_manager") and self.mainloop_manager.should_queue_state_command():
248
248
  return self.submit("add_hook", key=key, func=func).wait()
249
249
 
250
- self.hooks.setdefault(key, []).append(func)
251
- return func
250
+ return self.hooks.add(key, func)
252
251
 
253
252
  def has_hook(self, key):
254
- return bool(self.hooks.get(key))
253
+ return self.hooks.has(key)
254
+
255
+ def run_hook(self, key, **payload):
256
+ agent_context = set_current_agent(self)
257
+ try:
258
+ return self.hooks.run(key, **payload)
259
+ finally:
260
+ reset_current_agent(agent_context)
255
261
 
256
262
  def run_hooks(self, key, *args):
257
- results = []
258
- for hook in list(self.hooks.get(key, ())):
259
- agent_context = set_current_agent(self)
260
- try:
261
- results.append(hook(*args))
262
- finally:
263
- reset_current_agent(agent_context)
264
- return results
263
+ agent_context = set_current_agent(self)
264
+ try:
265
+ return self.hooks.run_legacy(key, *args)
266
+ finally:
267
+ reset_current_agent(agent_context)
265
268
 
266
269
  def get_messages(self,filter=None):
267
270
  return self.sessions_manager.messages(filter=filter)
@@ -321,16 +324,21 @@ class Agent:
321
324
  )
322
325
  messages = self.context_manager.get_context()
323
326
 
324
- message=self.ai.run(
325
- messages=messages,
326
- tools=self.registry.get_response_tools(),
327
- prompt_cache_key=self.current_session_id,
328
- **self.config.extract('model','reasoning_effort','verbosity','parallel_tool_calls')
329
- )
327
+ try:
328
+ message=self.ai.run(
329
+ messages=messages,
330
+ tools=self.registry.get_response_tools(),
331
+ prompt_cache_key=self.current_session_id,
332
+ **self.config.extract('model','reasoning_effort','verbosity','parallel_tool_calls')
333
+ )
334
+ except AIBadRequestError:
335
+ self.sessions_manager.restore_api_success_backup()
336
+ raise
330
337
  self.sessions_manager.append_message(message)
331
338
  for item in message.output_items:
332
339
  if self.registry.server_tool_for_item(item):
333
340
  self.sessions_manager.append_message(ServerToolResponseMessage(item=item))
341
+ self.sessions_manager.save_api_success_backup()
334
342
  return message
335
343
 
336
344
  def speak(self,text,**kwargs):
@@ -381,7 +389,7 @@ class Agent:
381
389
  """
382
390
  if source is None:
383
391
  raise ValueError("An audio source must be provided")
384
-
392
+
385
393
  # Transcribe audio to text
386
394
  transcribed_text = self.ai.audio_to_text(
387
395
  source=source,
@@ -51,6 +51,12 @@ class AIClientError(Exception):
51
51
  """Base exception for AI client errors."""
52
52
  pass
53
53
 
54
+
55
+ class AIBadRequestError(AIClientError):
56
+ """Raised when the backend rejects the request as malformed."""
57
+ pass
58
+
59
+
54
60
  class APIAuthenticationError(AIClientError):
55
61
  """Exception raised for OpenAI API authentication failures."""
56
62
  pass
@@ -237,6 +243,8 @@ class AIClient:
237
243
  **stream_params
238
244
  )
239
245
  except Exception as e:
246
+ if self.is_bad_request_error(e):
247
+ raise AIBadRequestError(str(e)) from e
240
248
  import traceback
241
249
  import sys
242
250
  print("\n[ERROR in ai.run setup]:", file=sys.stderr)
@@ -262,8 +270,20 @@ class AIClient:
262
270
  except ResponseAborted:
263
271
  raise AgentInterrupted()
264
272
  except Exception as e:
273
+ if self.is_bad_request_error(e):
274
+ raise AIBadRequestError(str(e)) from e
265
275
  yield MessageChunk(content=f"Hmmm, sorry! There was an error while reading the Codex backend stream. Here is the error message I got:\n\n ```\n{str(e)}\n```")
266
276
 
277
+ def is_bad_request_error(self, exc):
278
+ response = getattr(exc, "response", None)
279
+ status_code = getattr(response, "status_code", None)
280
+ if status_code == 400:
281
+ return True
282
+ status_code = getattr(exc, "status_code", None)
283
+ if status_code == 400:
284
+ return True
285
+ return "400" in str(exc) and "Bad Request" in str(exc)
286
+
267
287
  def get_quota_usage(self):
268
288
  try:
269
289
  return self.codex_client.usage()
@@ -346,6 +366,8 @@ class AIClient:
346
366
  return MessageChunk(tool_calls=[tool_call])
347
367
  return MessageChunk(output_items=[item])
348
368
  if isinstance(event, ResponseFailed):
369
+ if getattr(event, "code", None) == 400 or "bad_request" in str(getattr(event, "code", "")).lower():
370
+ raise AIBadRequestError(str(getattr(event, "message", "") or event))
349
371
  return MessageChunk(content=f"Hmmm, sorry! The Codex backend returned an error:\n\n```\n[{event.code}] {event.message}\n```")
350
372
  return None
351
373
 
@@ -11,6 +11,7 @@ MODEL_CONFIG_KEYS = (
11
11
  "input_token_limit",
12
12
  "max_input_tokens",
13
13
  "auto_compact",
14
+ "auto_archive_memory",
14
15
  )
15
16
 
16
17
 
@@ -30,19 +30,27 @@ def _format_browser_status(status: dict) -> str:
30
30
  class BrowserPlugin(Plugin):
31
31
  name = "browser"
32
32
  description = "Persistent Chromium browser tools and active-tab context provider."
33
+ system_prompt = """
34
+ # Browser
35
+
36
+ Use browser tools for interactive web pages, logged-in sessions, and workflows that require navigation, clicks, forms, or keyboard input in Chromium.
37
+ When the browser is open, the browser_state provider gives the current active tab snapshot and screenshot; treat that provider context as the source of truth.
38
+ Use the element ids from the latest browser_state snapshot with browser_click, browser_fill, browser_select, and browser_press.
39
+ Prefer browser_goto to navigate the current tab, browser_open when you need to start the browser or open a new tab, and browser_select_tab when the user explicitly wants another tab.
40
+ """
33
41
 
34
42
  @property
35
43
  def controller(self):
36
44
  return get_browser_controller()
37
45
 
38
- def browser_tool_result(self, call):
46
+ def tool_result(self, call):
39
47
  try:
40
48
  return call()
41
49
  except BrowserError as exc:
42
50
  return f"Browser error: {exc}"
43
51
 
44
52
  @tool
45
- def browser_open(self, url: str = None, headless: bool = False, profile_name: str = "default", new_tab: bool = None):
53
+ def open(self, url: str = None, headless: bool = False, profile_name: str = "default", new_tab: bool = None):
46
54
  """
47
55
  description: |
48
56
  Open the persistent Playwright/Chromium browser. If the browser is closed, this starts it.
@@ -61,17 +69,17 @@ class BrowserPlugin(Plugin):
61
69
  description: Force opening a new tab. Defaults to true when browser is already open, false for a fresh session.
62
70
  type: boolean
63
71
  """
64
- return self.browser_tool_result(lambda: _format_browser_status(self.controller.open(url=url, headless=headless, profile_name=profile_name, new_tab=new_tab)))
72
+ return self.tool_result(lambda: _format_browser_status(self.controller.open(url=url, headless=headless, profile_name=profile_name, new_tab=new_tab)))
65
73
 
66
74
  @tool
67
- def browser_close(self):
75
+ def close(self):
68
76
  """
69
77
  description: Close the persistent Playwright/Chromium browser session.
70
78
  """
71
- return self.browser_tool_result(lambda: self.controller.close())
79
+ return self.tool_result(lambda: self.controller.close())
72
80
 
73
81
  @tool
74
- def browser_select_tab(self, index: int):
82
+ def select_tab(self, index: int):
75
83
  """
76
84
  description: Select an open browser tab by zero-based index.
77
85
  parameters:
@@ -81,17 +89,17 @@ class BrowserPlugin(Plugin):
81
89
  required:
82
90
  - index
83
91
  """
84
- return self.browser_tool_result(lambda: _format_browser_status(self.controller.select_tab(index)))
92
+ return self.tool_result(lambda: _format_browser_status(self.controller.select_tab(index)))
85
93
 
86
94
  @tool
87
- def browser_previous_page(self):
95
+ def previous_page(self):
88
96
  """
89
97
  description: Navigate the active browser tab back in history.
90
98
  """
91
- return self.browser_tool_result(lambda: self.controller.previous_page())
99
+ return self.tool_result(lambda: self.controller.previous_page())
92
100
 
93
101
  @tool
94
- def browser_goto(self, url: str):
102
+ def goto(self, url: str):
95
103
  """
96
104
  description: Navigate the active browser tab to a URL, without opening or switching tabs.
97
105
  parameters:
@@ -101,9 +109,9 @@ class BrowserPlugin(Plugin):
101
109
  required:
102
110
  - url
103
111
  """
104
- return self.browser_tool_result(lambda: self.controller.goto(url))
112
+ return self.tool_result(lambda: self.controller.goto(url))
105
113
 
106
- def browser_snapshot(self, max_elements: int = 120, include_json: bool = False):
114
+ def snapshot(self, max_elements: int = 120, include_json: bool = False):
107
115
  """
108
116
  description: |
109
117
  Return a compact, action-oriented snapshot of the active web browser page.
@@ -116,10 +124,10 @@ class BrowserPlugin(Plugin):
116
124
  description: Include structured JSON snapshot data after the readable snapshot.
117
125
  type: boolean
118
126
  """
119
- return self.browser_tool_result(lambda: self.controller.snapshot(max_elements=max_elements, include_json=include_json))
127
+ return self.tool_result(lambda: self.controller.snapshot(max_elements=max_elements, include_json=include_json))
120
128
 
121
129
  @tool
122
- def browser_click(self, element_id: str):
130
+ def click(self, element_id: str):
123
131
  """
124
132
  description: Click an element from the latest browser provider snapshot by ID.
125
133
  parameters:
@@ -129,10 +137,10 @@ class BrowserPlugin(Plugin):
129
137
  required:
130
138
  - element_id
131
139
  """
132
- return self.browser_tool_result(lambda: self.controller.click(element_id))
140
+ return self.tool_result(lambda: self.controller.click(element_id))
133
141
 
134
142
  @tool
135
- def browser_fill(self, element_id: str, text: str):
143
+ def fill(self, element_id: str, text: str):
136
144
  """
137
145
  description: Fill an input/textarea/contenteditable element from the latest browser provider snapshot by ID.
138
146
  parameters:
@@ -146,10 +154,10 @@ class BrowserPlugin(Plugin):
146
154
  - element_id
147
155
  - text
148
156
  """
149
- return self.browser_tool_result(lambda: self.controller.fill(element_id, text))
157
+ return self.tool_result(lambda: self.controller.fill(element_id, text))
150
158
 
151
159
  @tool
152
- def browser_select(self, element_id: str, value):
160
+ def select(self, element_id: str, value):
153
161
  """
154
162
  description: Select an option value on a select element from the latest browser provider snapshot by ID.
155
163
  parameters:
@@ -162,10 +170,10 @@ class BrowserPlugin(Plugin):
162
170
  - element_id
163
171
  - value
164
172
  """
165
- return self.browser_tool_result(lambda: self.controller.select(element_id, value))
173
+ return self.tool_result(lambda: self.controller.select(element_id, value))
166
174
 
167
175
  @tool
168
- def browser_press(self, key: str, element_id: str = None):
176
+ def press(self, key: str, element_id: str = None):
169
177
  """
170
178
  description: Press a keyboard key globally or on a browser provider snapshot element ID.
171
179
  parameters:
@@ -178,7 +186,7 @@ class BrowserPlugin(Plugin):
178
186
  required:
179
187
  - key
180
188
  """
181
- return self.browser_tool_result(lambda: self.controller.press(key, element_id=element_id))
189
+ return self.tool_result(lambda: self.controller.press(key, element_id=element_id))
182
190
 
183
191
  @provider
184
192
  def browser_state(self):