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.
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/CHANGELOG.md +15 -0
- {codex_agent_framework-0.1.14/codex_agent_framework.egg-info → codex_agent_framework-0.1.15}/PKG-INFO +20 -6
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/README.md +19 -5
- codex_agent_framework-0.1.15/codex_agent/__main__.py +7 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/agent_runtime.py +152 -3
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/ai.py +60 -9
- codex_agent_framework-0.1.15/codex_agent/cli/__init__.py +1 -0
- codex_agent_framework-0.1.15/codex_agent/cli/headless.py +203 -0
- codex_agent_framework-0.1.14/codex_agent/__main__.py → codex_agent_framework-0.1.15/codex_agent/cli/main.py +46 -20
- codex_agent_framework-0.1.15/codex_agent/cli/output.py +31 -0
- codex_agent_framework-0.1.15/codex_agent/cli/root.py +169 -0
- codex_agent_framework-0.1.15/codex_agent/cli/runner.py +87 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/client.py +20 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/config.py +20 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/memory.py +13 -2
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/plugin.py +2 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/registry.py +16 -6
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/server.py +20 -26
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/service.py +4 -3
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15/codex_agent_framework.egg-info}/PKG-INFO +20 -6
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent_framework.egg-info/SOURCES.txt +10 -0
- codex_agent_framework-0.1.15/codex_agent_framework.egg-info/entry_points.txt +2 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/pyproject.toml +2 -2
- codex_agent_framework-0.1.15/tests/test_agent_runtime.py +121 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_ai.py +49 -1
- codex_agent_framework-0.1.15/tests/test_builtin_config.py +33 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_cli.py +28 -46
- codex_agent_framework-0.1.15/tests/test_cli_headless.py +136 -0
- codex_agent_framework-0.1.15/tests/test_cli_root.py +106 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_memory.py +75 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_server.py +58 -47
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_service.py +4 -2
- codex_agent_framework-0.1.14/codex_agent_framework.egg-info/entry_points.txt +0 -2
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/LICENSE +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/MANIFEST.in +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/__init__.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/agent.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_commands.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/__init__.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/browser/controller.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/desktop/controller.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/desktop/screenshot.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_providers.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/__init__.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/files.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/server_tools.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/shell.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/system.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/builtin_tools/vision.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/chat.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/command.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/context.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/event.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/get_text/__init__.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/get_text/default_gitignore +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/get_text/get_text.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/get_text/simpler_get_text.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/hooks.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/image.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/latex.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/mainloop.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/message.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/prompts/system_prompt.txt +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/provider.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/runtime.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/scheduler.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/scripts/install-system-dependencies.sh +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/sessions.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/status.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/stream_utils.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/tool.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/tray.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/tui.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/utils.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/voice.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent/worker.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent_framework.egg-info/requires.txt +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/codex_agent_framework.egg-info/top_level.txt +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/dependencies.txt +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/setup.cfg +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_agent.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_browser.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_chat.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_client.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_events.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_get_text_browser.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_hooks.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_image_message.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_local_desktop.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_messages.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_planner.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_scheduler.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_tray.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_tui.py +0 -0
- {codex_agent_framework-0.1.14 → codex_agent_framework-0.1.15}/tests/test_utils.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
@@ -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
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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()
|