codex-agent-framework 0.1.14__tar.gz → 0.1.15__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 (103) hide show
  1. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/CHANGELOG.md +15 -0
  2. {codex_agent_framework-0.1.14/codex_agent_framework.egg-info → codex_agent_framework-0.1.15}/PKG-INFO +20 -6
  3. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/README.md +19 -5
  4. codex_agent_framework-0.1.15/codex_agent/__main__.py +7 -0
  5. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/agent_runtime.py +152 -3
  6. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/ai.py +60 -9
  7. codex_agent_framework-0.1.15/codex_agent/cli/__init__.py +1 -0
  8. codex_agent_framework-0.1.15/codex_agent/cli/headless.py +203 -0
  9. codex_agent_framework-0.1.14/codex_agent/__main__.py → codex_agent_framework-0.1.15/codex_agent/cli/main.py +46 -20
  10. codex_agent_framework-0.1.15/codex_agent/cli/output.py +31 -0
  11. codex_agent_framework-0.1.15/codex_agent/cli/root.py +169 -0
  12. codex_agent_framework-0.1.15/codex_agent/cli/runner.py +87 -0
  13. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/client.py +20 -0
  14. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/config.py +20 -0
  15. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/memory.py +13 -2
  16. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/plugin.py +2 -0
  17. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/registry.py +16 -6
  18. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/server.py +20 -26
  19. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/service.py +4 -3
  20. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15/codex_agent_framework.egg-info}/PKG-INFO +20 -6
  21. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent_framework.egg-info/SOURCES.txt +10 -0
  22. codex_agent_framework-0.1.15/codex_agent_framework.egg-info/entry_points.txt +2 -0
  23. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/pyproject.toml +2 -2
  24. codex_agent_framework-0.1.15/tests/test_agent_runtime.py +121 -0
  25. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_ai.py +49 -1
  26. codex_agent_framework-0.1.15/tests/test_builtin_config.py +33 -0
  27. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_cli.py +28 -46
  28. codex_agent_framework-0.1.15/tests/test_cli_headless.py +136 -0
  29. codex_agent_framework-0.1.15/tests/test_cli_root.py +106 -0
  30. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_memory.py +75 -0
  31. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_server.py +58 -47
  32. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_service.py +4 -2
  33. codex_agent_framework-0.1.14/codex_agent_framework.egg-info/entry_points.txt +0 -2
  34. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/LICENSE +0 -0
  35. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/MANIFEST.in +0 -0
  36. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/__init__.py +0 -0
  37. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/agent.py +0 -0
  38. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_commands.py +0 -0
  39. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/__init__.py +0 -0
  40. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  41. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/browser/controller.py +0 -0
  42. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  43. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/desktop/controller.py +0 -0
  44. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/desktop/screenshot.py +0 -0
  45. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  46. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  47. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  48. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_providers.py +0 -0
  49. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/__init__.py +0 -0
  50. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/files.py +0 -0
  51. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/server_tools.py +0 -0
  52. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/shell.py +0 -0
  53. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/system.py +0 -0
  54. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/vision.py +0 -0
  55. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/chat.py +0 -0
  56. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/command.py +0 -0
  57. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/context.py +0 -0
  58. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/event.py +0 -0
  59. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/get_text/__init__.py +0 -0
  60. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/get_text/default_gitignore +0 -0
  61. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/get_text/get_text.py +0 -0
  62. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/get_text/simpler_get_text.py +0 -0
  63. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/hooks.py +0 -0
  64. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/image.py +0 -0
  65. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/latex.py +0 -0
  66. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/mainloop.py +0 -0
  67. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/message.py +0 -0
  68. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
  69. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/prompts/system_prompt.txt +0 -0
  70. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/provider.py +0 -0
  71. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/runtime.py +0 -0
  72. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/scheduler.py +0 -0
  73. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  74. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/sessions.py +0 -0
  75. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/status.py +0 -0
  76. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/stream_utils.py +0 -0
  77. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/tool.py +0 -0
  78. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/tray.py +0 -0
  79. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/tui.py +0 -0
  80. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/utils.py +0 -0
  81. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/voice.py +0 -0
  82. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/worker.py +0 -0
  83. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  84. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent_framework.egg-info/requires.txt +0 -0
  85. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent_framework.egg-info/top_level.txt +0 -0
  86. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/dependencies.txt +0 -0
  87. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/setup.cfg +0 -0
  88. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_agent.py +0 -0
  89. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_browser.py +0 -0
  90. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_chat.py +0 -0
  91. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_client.py +0 -0
  92. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_events.py +0 -0
  93. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_get_text_browser.py +0 -0
  94. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_hooks.py +0 -0
  95. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_image_message.py +0 -0
  96. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_local_desktop.py +0 -0
  97. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_messages.py +0 -0
  98. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_planner.py +0 -0
  99. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_scheduler.py +0 -0
  100. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_tray.py +0 -0
  101. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_tui.py +0 -0
  102. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_utils.py +0 -0
  103. {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_worker.py +0 -0
@@ -4,6 +4,21 @@ 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.15] - 2026-05-08
8
+ ### Added
9
+ - Add `AgentRuntime` as the canonical external runtime contract for HTTP server, headless CLI, in-process, and process-backed integrations.
10
+ - Add structured runtime configuration updates through `PATCH /config`, `AgentClient.update_config()`, and `codex-agent config set`.
11
+ - Add direct built-in selection config for tools, providers, and plugins with `None` meaning all built-ins, an empty list meaning none, and explicit lists selecting named built-ins.
12
+
13
+ ### Changed
14
+ - Split the command line implementation into the `codex_agent.cli` package with root orchestration commands and headless runtime commands.
15
+ - Route server operations through `AgentRuntime` instead of depending on `Agent` internals directly.
16
+ - Update CLI service/interface commands to the explicit `start/stop/restart server|tray` and `open/close tui` forms.
17
+
18
+ ### Tests
19
+ - Add regression coverage for the runtime abstraction, headless CLI, root CLI dispatch, config updates, and built-in selection.
20
+ - Validate the targeted release-regression suite at 82 passing tests.
21
+
7
22
  ## [0.1.14] - 2026-05-07
8
23
  ### Added
9
24
  - 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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.14
3
+ Version: 0.1.15
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -123,19 +123,29 @@ codex-agent
123
123
  Run only the long-lived local server:
124
124
 
125
125
  ```bash
126
- codex-agent server
126
+ codex-agent start server
127
127
  ```
128
128
 
129
129
  Connect a TUI to an already running server:
130
130
 
131
131
  ```bash
132
- codex-agent chat
132
+ codex-agent open tui
133
133
  ```
134
134
 
135
135
  Start only the tray controller:
136
136
 
137
137
  ```bash
138
- codex-agent tray
138
+ codex-agent start tray
139
+ ```
140
+
141
+ Headless runtime examples:
142
+
143
+ ```bash
144
+ codex-agent status --json
145
+ codex-agent tools
146
+ codex-agent config get input_token_limit
147
+ codex-agent config set input_token_limit 128000
148
+ codex-agent run "summarize this repository"
139
149
  ```
140
150
 
141
151
  Install the user services for the server and tray controller:
@@ -259,6 +269,7 @@ Key endpoints:
259
269
  GET /health
260
270
  GET /status
261
271
  GET /config
272
+ PATCH /config
262
273
  GET /session
263
274
  GET /sessions
264
275
  GET /messages
@@ -380,13 +391,15 @@ agent = Agent(
380
391
  )
381
392
  ```
382
393
 
383
- Configuration is persisted to `agent_config.json` in the runtime directory when updated through agent helpers or slash commands.
394
+ Configuration is persisted to `agent_config.json` in the runtime directory when updated through agent helpers, slash commands, the HTTP API, or `codex-agent config set`.
384
395
 
385
396
  ## Project layout
386
397
 
387
398
  ```text
388
399
  codex_agent/ Python package
389
400
  codex_agent/agent.py Agent, AgentConfig, AgentSession
401
+ codex_agent/agent_runtime.py Runtime abstraction for server, CLI, and process adapters
402
+ codex_agent/cli/ Command line interface package
390
403
  codex_agent/plugin.py Stateful plugin base class
391
404
  codex_agent/builtin_tools/ Built-in local tools package
392
405
  codex_agent/builtin_commands.py Built-in slash commands
@@ -412,7 +425,7 @@ The tests isolate `AGENT_RUNTIME_DIR` automatically, so they should not create o
412
425
  Current baseline:
413
426
 
414
427
  ```text
415
- 412 passed
428
+ 82 targeted release-regression tests passed
416
429
  ```
417
430
 
418
431
  ## Packaging
@@ -427,6 +440,7 @@ python -m build
427
440
  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`.
428
441
 
429
442
  ## Recent changes
443
+ - `0.1.15`: add the `AgentRuntime` interface for server/CLI/process adapters, split the CLI into a package with headless runtime commands, add `codex-agent config set` plus `PATCH /config`, and make built-in tools/providers/plugins configurable.
430
444
  - `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.
431
445
  - `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.
432
446
  - `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.
@@ -74,19 +74,29 @@ codex-agent
74
74
  Run only the long-lived local server:
75
75
 
76
76
  ```bash
77
- codex-agent server
77
+ codex-agent start server
78
78
  ```
79
79
 
80
80
  Connect a TUI to an already running server:
81
81
 
82
82
  ```bash
83
- codex-agent chat
83
+ codex-agent open tui
84
84
  ```
85
85
 
86
86
  Start only the tray controller:
87
87
 
88
88
  ```bash
89
- codex-agent tray
89
+ codex-agent start tray
90
+ ```
91
+
92
+ Headless runtime examples:
93
+
94
+ ```bash
95
+ codex-agent status --json
96
+ codex-agent tools
97
+ codex-agent config get input_token_limit
98
+ codex-agent config set input_token_limit 128000
99
+ codex-agent run "summarize this repository"
90
100
  ```
91
101
 
92
102
  Install the user services for the server and tray controller:
@@ -210,6 +220,7 @@ Key endpoints:
210
220
  GET /health
211
221
  GET /status
212
222
  GET /config
223
+ PATCH /config
213
224
  GET /session
214
225
  GET /sessions
215
226
  GET /messages
@@ -331,13 +342,15 @@ agent = Agent(
331
342
  )
332
343
  ```
333
344
 
334
- Configuration is persisted to `agent_config.json` in the runtime directory when updated through agent helpers or slash commands.
345
+ Configuration is persisted to `agent_config.json` in the runtime directory when updated through agent helpers, slash commands, the HTTP API, or `codex-agent config set`.
335
346
 
336
347
  ## Project layout
337
348
 
338
349
  ```text
339
350
  codex_agent/ Python package
340
351
  codex_agent/agent.py Agent, AgentConfig, AgentSession
352
+ codex_agent/agent_runtime.py Runtime abstraction for server, CLI, and process adapters
353
+ codex_agent/cli/ Command line interface package
341
354
  codex_agent/plugin.py Stateful plugin base class
342
355
  codex_agent/builtin_tools/ Built-in local tools package
343
356
  codex_agent/builtin_commands.py Built-in slash commands
@@ -363,7 +376,7 @@ The tests isolate `AGENT_RUNTIME_DIR` automatically, so they should not create o
363
376
  Current baseline:
364
377
 
365
378
  ```text
366
- 412 passed
379
+ 82 targeted release-regression tests passed
367
380
  ```
368
381
 
369
382
  ## Packaging
@@ -378,6 +391,7 @@ python -m build
378
391
  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`.
379
392
 
380
393
  ## Recent changes
394
+ - `0.1.15`: add the `AgentRuntime` interface for server/CLI/process adapters, split the CLI into a package with headless runtime commands, add `codex-agent config set` plus `PATCH /config`, and make built-in tools/providers/plugins configurable.
381
395
  - `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.
382
396
  - `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.
383
397
  - `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.
@@ -0,0 +1,7 @@
1
+ """Compatibility entrypoint for ``python -m codex_agent``."""
2
+
3
+ from .cli.main import main
4
+
5
+
6
+ if __name__ == '__main__':
7
+ main()
@@ -3,12 +3,13 @@ import multiprocessing
3
3
  import queue
4
4
  import threading
5
5
  import uuid
6
+ from abc import ABC, abstractmethod
6
7
 
7
8
  from modict import modict
8
9
 
9
10
  from .agent import Agent
10
11
  from .event import Event
11
- from .message import message_from_data
12
+ from .message import UserMessage, message_from_data
12
13
 
13
14
 
14
15
  def jsonable(value):
@@ -19,7 +20,143 @@ class AgentRuntimeError(RuntimeError):
19
20
  pass
20
21
 
21
22
 
22
- class InProcessAgentRuntime:
23
+ class AgentRuntime(ABC):
24
+ """Canonical contract for exposing an agent to external applications.
25
+
26
+ This ABC is the source of truth for every public surface that drives or
27
+ introspects an Agent from outside the core object: HTTP server, headless CLI,
28
+ local process adapters, remote clients and future integrations should all
29
+ implement or target this runtime interface instead of depending on Agent
30
+ internals directly.
31
+ """
32
+
33
+ @abstractmethod
34
+ def start(self):
35
+ return self
36
+
37
+ @abstractmethod
38
+ def stop(self, timeout=None):
39
+ return self
40
+
41
+ @abstractmethod
42
+ def on_event(self, handler):
43
+ return handler
44
+
45
+ @property
46
+ @abstractmethod
47
+ def busy(self):
48
+ return False
49
+
50
+ @property
51
+ @abstractmethod
52
+ def current_turn_id(self):
53
+ return None
54
+
55
+ @property
56
+ @abstractmethod
57
+ def current_session_id(self):
58
+ return None
59
+
60
+ @abstractmethod
61
+ def get_status(self):
62
+ raise NotImplementedError
63
+
64
+ @abstractmethod
65
+ def config(self):
66
+ raise NotImplementedError
67
+
68
+ @abstractmethod
69
+ def update_config(self, values=None, save=True, **kwargs):
70
+ raise NotImplementedError
71
+
72
+ @abstractmethod
73
+ def session(self):
74
+ raise NotImplementedError
75
+
76
+ @abstractmethod
77
+ def sessions(self):
78
+ raise NotImplementedError
79
+
80
+ @abstractmethod
81
+ def messages(self):
82
+ raise NotImplementedError
83
+
84
+ @abstractmethod
85
+ def memory(self):
86
+ raise NotImplementedError
87
+
88
+ @abstractmethod
89
+ def tools(self):
90
+ raise NotImplementedError
91
+
92
+ @abstractmethod
93
+ def wakeups(self, include_done=True):
94
+ raise NotImplementedError
95
+
96
+ @abstractmethod
97
+ def start_new_session(self):
98
+ raise NotImplementedError
99
+
100
+ @abstractmethod
101
+ def load_session(self, session_id):
102
+ raise NotImplementedError
103
+
104
+ @abstractmethod
105
+ def delete_session(self, session_id):
106
+ raise NotImplementedError
107
+
108
+ @abstractmethod
109
+ def navigate_session(self, direction):
110
+ raise NotImplementedError
111
+
112
+ @abstractmethod
113
+ def interrupt(self, reason="api"):
114
+ raise NotImplementedError
115
+
116
+ @abstractmethod
117
+ def start_turn(self):
118
+ raise NotImplementedError
119
+
120
+ @abstractmethod
121
+ def submit_command(self, prompt):
122
+ raise NotImplementedError
123
+
124
+ @abstractmethod
125
+ def submit_message(self, message):
126
+ raise NotImplementedError
127
+
128
+ def submit_prompt(self, prompt):
129
+ if not prompt or not str(prompt).strip():
130
+ raise ValueError("prompt must not be empty")
131
+ if str(prompt).strip().startswith("/"):
132
+ if self.busy:
133
+ raise RuntimeError("agent is busy")
134
+ turn_id = self.submit_command(prompt)
135
+ if turn_id is None:
136
+ raise RuntimeError("agent is busy")
137
+ return modict(command=True, queued=False, turn_id=turn_id)
138
+ was_busy = self.busy
139
+ result = self.submit_message(UserMessage(content=prompt))
140
+ return modict(
141
+ queued=was_busy,
142
+ pending_message_id=result.message.id,
143
+ turn_id=result.get("turn_id") or self.current_turn_id,
144
+ )
145
+
146
+ @abstractmethod
147
+ def schedule_wakeup(self, **kwargs):
148
+ raise NotImplementedError
149
+
150
+ @abstractmethod
151
+ def cancel_wakeup(self, job_id):
152
+ raise NotImplementedError
153
+
154
+ @abstractmethod
155
+ def delete_wakeup(self, job_id):
156
+ raise NotImplementedError
157
+
158
+
159
+ class InProcessAgentRuntime(AgentRuntime):
23
160
  def __init__(self, agent: Agent):
24
161
  self.agent = agent
25
162
 
@@ -53,6 +190,9 @@ class InProcessAgentRuntime:
53
190
  def config(self):
54
191
  return self.agent.config
55
192
 
193
+ def update_config(self, values=None, save=True, **kwargs):
194
+ return self.agent.config_manager.update(values, save=save, **kwargs)
195
+
56
196
  def session(self):
57
197
  return self.agent.session
58
198
 
@@ -115,7 +255,7 @@ class InProcessAgentRuntime:
115
255
  return job
116
256
 
117
257
 
118
- class ProcessAgentRuntime:
258
+ class ProcessAgentRuntime(AgentRuntime):
119
259
  def __init__(self, agent_kwargs=None, *, command_timeout=5.0, status_timeout=0.5):
120
260
  self.agent_kwargs = dict(agent_kwargs or {})
121
261
  self.command_timeout = float(command_timeout)
@@ -294,6 +434,13 @@ class ProcessAgentRuntime:
294
434
  def config(self):
295
435
  return self._cached_call("config")
296
436
 
437
+ def update_config(self, values=None, save=True, **kwargs):
438
+ payload = dict(values or {})
439
+ payload.update(kwargs)
440
+ result = self.call("update_config", {"values": payload, "save": save}, timeout=self.command_timeout)
441
+ self.cache.config = result
442
+ return result
443
+
297
444
  def session(self):
298
445
  return self._cached_call("session")
299
446
 
@@ -452,6 +599,8 @@ def run_worker_command(agent, name, payload):
452
599
  return status
453
600
  if name == "config":
454
601
  return agent.config
602
+ if name == "update_config":
603
+ return agent.config_manager.update(payload.get("values") or {}, save=payload.get("save", True))
455
604
  if name == "session":
456
605
  return agent.session
457
606
  if name == "sessions":
@@ -456,6 +456,8 @@ class AIClient:
456
456
  if not isinstance(content, str):
457
457
  raise ValueError(f"Content must be a string, got {type(content)}")
458
458
  content = self.truncate_embedding_input(content, model=model)
459
+ if not content.strip():
460
+ raise ValueError("Content must not be empty")
459
461
 
460
462
  try:
461
463
  response = self.client.embeddings.create(
@@ -485,9 +487,13 @@ class AIClient:
485
487
  Note:
486
488
  OpenAI API handles batching automatically for better performance.
487
489
  """
490
+ if not contents:
491
+ return []
488
492
  if not all(isinstance(content, str) for content in contents):
489
493
  raise ValueError("All contents must be strings")
490
494
  contents = [self.truncate_embedding_input(content) for content in contents]
495
+ if any(not content.strip() for content in contents):
496
+ raise ValueError("Embedding contents must not contain empty strings")
491
497
 
492
498
  try:
493
499
  response = self.client.embeddings.create(
@@ -501,6 +507,14 @@ class AIClient:
501
507
 
502
508
  return embeddings
503
509
 
510
+ def message_embedding_content(self, msg):
511
+ if hasattr(msg, 'content') and isinstance(msg.content, str):
512
+ return msg.content.strip()
513
+ raise ValueError("Message must have text content")
514
+
515
+ def zero_embedding(self, dimensions=128):
516
+ return [0.0] * int(dimensions)
517
+
504
518
  def similarity(self, emb1, emb2):
505
519
  """
506
520
  Calculate shifted positive cosine similarity between two embeddings.
@@ -524,15 +538,52 @@ class AIClient:
524
538
  Note:
525
539
  Only text content is supported.
526
540
  """
527
- if hasattr(msg, 'content') and isinstance(msg.content, str):
528
- content = msg.content
529
- else:
530
- raise ValueError("Message must have text content")
531
-
532
- # Generate embedding
541
+ content = self.message_embedding_content(msg)
542
+ if not content:
543
+ embedding = self.zero_embedding(dimensions)
544
+ msg.embedding = embedding
545
+ return embedding
533
546
  embedding = self.embed_content(content, precision, dimensions, model)
534
-
535
- # Assign to msg.embedding
536
547
  msg.embedding = embedding
537
-
538
548
  return embedding
549
+
550
+ def embed_messages(self, messages, precision=5, dimensions=128, model="text-embedding-3-small"):
551
+ """
552
+ Embed multiple Message objects in one embeddings API request and assign
553
+ the resulting vectors to each message.
554
+ """
555
+ messages = list(messages)
556
+ if not messages:
557
+ return []
558
+
559
+ embeddings = [None] * len(messages)
560
+ batch = []
561
+ batch_indices = []
562
+ for index, msg in enumerate(messages):
563
+ content = self.message_embedding_content(msg)
564
+ if not content:
565
+ embedding = self.zero_embedding(dimensions)
566
+ msg.embedding = embedding
567
+ embeddings[index] = embedding
568
+ continue
569
+ batch.append(content)
570
+ batch_indices.append(index)
571
+
572
+ if not batch:
573
+ return embeddings
574
+
575
+ batch_embeddings = self.embed_contents(
576
+ batch,
577
+ precision=precision,
578
+ dimensions=dimensions,
579
+ model=model,
580
+ )
581
+ if len(batch_embeddings) != len(batch):
582
+ raise AIClientError(
583
+ f"Expected {len(batch)} embeddings, got {len(batch_embeddings)}"
584
+ )
585
+ for index, embedding in zip(batch_indices, batch_embeddings):
586
+ msg = messages[index]
587
+ msg.embedding = embedding
588
+ embeddings[index] = embedding
589
+ return embeddings
@@ -0,0 +1 @@
1
+ """Command-line interface package for codex-agent."""
@@ -0,0 +1,203 @@
1
+ import json
2
+ import sys
3
+ import threading
4
+
5
+ from .output import dump_ndjson, output_payload, key_value_text
6
+ from .runner import in_process_runner, process_runner, server_runner
7
+
8
+
9
+ TERMINAL_EVENTS = {"agent_turn_end_event", "agent_turn_error_event", "server_shutdown"}
10
+
11
+
12
+ def runner_from_args(args):
13
+ runtime = getattr(args, "runtime", "server") or "server"
14
+ if runtime == "server":
15
+ return server_runner(base_url=args.url or f"http://{args.host}:{args.port}")
16
+ if runtime == "process":
17
+ from .main import default_agent_kwargs
18
+ return process_runner(agent_kwargs=default_agent_kwargs())
19
+ if runtime == "in-process":
20
+ from .main import default_agent
21
+ return in_process_runner(default_agent())
22
+ raise SystemExit(f"unsupported runtime: {runtime}")
23
+
24
+
25
+ def should_stop_runner(args):
26
+ return (getattr(args, "runtime", "server") or "server") != "server"
27
+
28
+
29
+ def output_format(args):
30
+ if getattr(args, "json", False):
31
+ return "json"
32
+ return getattr(args, "format", None) or "text"
33
+
34
+
35
+ def read_prompt(args, extra_args):
36
+ if getattr(args, "stdin", False):
37
+ prompt = sys.stdin.read()
38
+ else:
39
+ prompt = " ".join(extra_args).strip()
40
+ if not prompt:
41
+ raise SystemExit("prompt is required; pass it as arguments or use --stdin")
42
+ return prompt
43
+
44
+
45
+ def with_runner(args, callback):
46
+ runner = runner_from_args(args).start()
47
+ try:
48
+ return callback(runner)
49
+ finally:
50
+ if should_stop_runner(args):
51
+ runner.stop()
52
+
53
+
54
+ def run_status(args, extra_args=None):
55
+ payload = with_runner(args, lambda runner: runner.get_status())
56
+ output_payload(payload, fmt=output_format(args), text_formatter=key_value_text)
57
+ return payload
58
+
59
+
60
+ def run_tools(args, extra_args=None):
61
+ payload = with_runner(args, lambda runner: runner.tools())
62
+
63
+ def format_tools(data):
64
+ if isinstance(data, dict):
65
+ names = sorted(data.keys())
66
+ else:
67
+ names = [str(item) for item in data or []]
68
+ return "\n".join(names)
69
+
70
+ output_payload(payload, fmt=output_format(args), text_formatter=format_tools)
71
+ return payload
72
+
73
+
74
+ def parse_config_value(value):
75
+ text = str(value)
76
+ lowered = text.lower()
77
+ if lowered == "true":
78
+ return True
79
+ if lowered == "false":
80
+ return False
81
+ if lowered == "null" or lowered == "none":
82
+ return None
83
+ try:
84
+ return json.loads(text)
85
+ except json.JSONDecodeError:
86
+ return value
87
+
88
+
89
+ def parse_config_set_args(extra_args):
90
+ args = list(extra_args or [])
91
+ if not args:
92
+ raise SystemExit("config set requires KEY VALUE or KEY=VALUE")
93
+ values = {}
94
+ if len(args) == 1 and "=" in args[0]:
95
+ key, value = args[0].split("=", 1)
96
+ values[key] = parse_config_value(value)
97
+ return values
98
+ if len(args) % 2 != 0:
99
+ raise SystemExit("config set requires KEY VALUE pairs or KEY=VALUE")
100
+ for index in range(0, len(args), 2):
101
+ values[args[index]] = parse_config_value(args[index + 1])
102
+ return values
103
+
104
+
105
+ def run_config(args, extra_args=None):
106
+ extra_args = list(extra_args or [])
107
+ command = extra_args[0] if extra_args else "get"
108
+ if command == "get":
109
+ payload = with_runner(args, lambda runner: runner.config())
110
+ if len(extra_args) >= 2:
111
+ key = extra_args[1]
112
+ payload = payload.get(key)
113
+ elif command == "set":
114
+ values = parse_config_set_args(extra_args[1:])
115
+ payload = with_runner(args, lambda runner: runner.update_config(values, save=not getattr(args, "no_save", False)))
116
+ else:
117
+ raise SystemExit(f"unsupported config command: {command}")
118
+ output_payload(payload, fmt=output_format(args), text_formatter=lambda data: key_value_text(data) if isinstance(data, dict) else str(data))
119
+ return payload
120
+
121
+
122
+ def run_sessions(args, extra_args=None):
123
+ extra_args = list(extra_args or [])
124
+ command = extra_args[0] if extra_args else "list"
125
+
126
+ def action(runner):
127
+ if command == "list":
128
+ return runner.sessions()
129
+ if command == "new":
130
+ return runner.start_new_session()
131
+ if command == "load":
132
+ if len(extra_args) < 2:
133
+ raise SystemExit("sessions load requires a session id")
134
+ return runner.load_session(extra_args[1])
135
+ if command == "delete":
136
+ if len(extra_args) < 2:
137
+ raise SystemExit("sessions delete requires a session id")
138
+ return runner.delete_session(extra_args[1])
139
+ raise SystemExit(f"unsupported sessions command: {command}")
140
+
141
+ payload = with_runner(args, action)
142
+
143
+ def format_sessions(data):
144
+ if command != "list" or not isinstance(data, dict):
145
+ return key_value_text(data) if isinstance(data, dict) else str(data)
146
+ current = data.get("current")
147
+ sessions = data.get("sessions") or []
148
+ lines = []
149
+ for session in sessions:
150
+ session_id = session.get("id") if isinstance(session, dict) else str(session)
151
+ prefix = "*" if session_id == current else " "
152
+ title = session.get("title") or session.get("name") or "" if isinstance(session, dict) else ""
153
+ lines.append(f"{prefix} {session_id} {title}".rstrip())
154
+ return "\n".join(lines)
155
+
156
+ output_payload(payload, fmt=output_format(args), text_formatter=format_sessions)
157
+ return payload
158
+
159
+
160
+ def run_interrupt(args, extra_args=None):
161
+ reason = " ".join(extra_args or []).strip() or "cli"
162
+ payload = with_runner(args, lambda runner: runner.interrupt(reason=reason))
163
+ output_payload(payload, fmt=output_format(args), text_formatter=key_value_text)
164
+ return payload
165
+
166
+
167
+ def run_prompt(args, extra_args=None):
168
+ prompt = read_prompt(args, extra_args or [])
169
+ fmt = output_format(args)
170
+ runner = runner_from_args(args).start()
171
+ done = threading.Event()
172
+ result = {"ok": True, "text": "", "events": [], "accepted": None, "error": None}
173
+
174
+ def handle_event(event):
175
+ event_type = event.get("type")
176
+ if fmt == "ndjson":
177
+ dump_ndjson(event)
178
+ elif fmt == "text" and event_type == "response_content_delta_event":
179
+ delta = event.get("delta") or ""
180
+ result["text"] += delta
181
+ print(delta, end="", flush=True)
182
+ result["events"].append(dict(event))
183
+ if event_type == "agent_turn_error_event":
184
+ result["ok"] = False
185
+ result["error"] = event.get("error") or "agent turn failed"
186
+ if event_type in TERMINAL_EVENTS:
187
+ done.set()
188
+
189
+ runner.on_event(handle_event)
190
+ runner.start_events()
191
+ try:
192
+ accepted = runner.submit_prompt(prompt)
193
+ result["accepted"] = dict(accepted)
194
+ timeout = getattr(args, "wait_timeout", None)
195
+ if timeout != 0:
196
+ done.wait(timeout)
197
+ if fmt == "text" and result["text"]:
198
+ print()
199
+ if fmt == "json":
200
+ output_payload(result, fmt="json")
201
+ return result
202
+ finally:
203
+ runner.stop()