codex-agent-framework 0.1.15__tar.gz → 0.1.17__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 (105) hide show
  1. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/CHANGELOG.md +20 -0
  2. {codex_agent_framework-0.1.15/codex_agent_framework.egg-info → codex_agent_framework-0.1.17}/PKG-INFO +5 -1
  3. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/README.md +4 -0
  4. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/agent.py +5 -3
  5. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/cli/main.py +1 -1
  6. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/config.py +8 -0
  7. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/mainloop.py +1 -1
  8. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/message.py +0 -1
  9. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/plugin.py +2 -0
  10. codex_agent_framework-0.1.17/codex_agent/server/__init__.py +29 -0
  11. codex_agent_framework-0.1.17/codex_agent/server/app.py +201 -0
  12. codex_agent_framework-0.1.15/codex_agent/server.py → codex_agent_framework-0.1.17/codex_agent/server/core.py +29 -208
  13. codex_agent_framework-0.1.17/codex_agent/tui/__init__.py +25 -0
  14. {codex_agent_framework-0.1.15/codex_agent → codex_agent_framework-0.1.17/codex_agent/tui}/chat.py +3 -3
  15. codex_agent_framework-0.1.15/codex_agent/tui.py → codex_agent_framework-0.1.17/codex_agent/tui/lifecycle.py +1 -1
  16. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17/codex_agent_framework.egg-info}/PKG-INFO +5 -1
  17. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent_framework.egg-info/SOURCES.txt +6 -3
  18. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/pyproject.toml +1 -1
  19. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_agent.py +23 -0
  20. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_chat.py +2 -2
  21. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_server.py +21 -21
  22. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_tui.py +1 -1
  23. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/LICENSE +0 -0
  24. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/MANIFEST.in +0 -0
  25. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/__init__.py +0 -0
  26. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/__main__.py +0 -0
  27. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/agent_runtime.py +0 -0
  28. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/ai.py +0 -0
  29. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_commands.py +0 -0
  30. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_plugins/__init__.py +0 -0
  31. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  32. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_plugins/browser/controller.py +0 -0
  33. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  34. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_plugins/desktop/controller.py +0 -0
  35. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_plugins/desktop/screenshot.py +0 -0
  36. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  37. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  38. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  39. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_providers.py +0 -0
  40. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_tools/__init__.py +0 -0
  41. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_tools/files.py +0 -0
  42. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_tools/server_tools.py +0 -0
  43. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_tools/shell.py +0 -0
  44. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_tools/system.py +0 -0
  45. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/builtin_tools/vision.py +0 -0
  46. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/cli/__init__.py +0 -0
  47. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/cli/headless.py +0 -0
  48. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/cli/output.py +0 -0
  49. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/cli/root.py +0 -0
  50. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/cli/runner.py +0 -0
  51. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/client.py +0 -0
  52. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/command.py +0 -0
  53. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/context.py +0 -0
  54. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/event.py +0 -0
  55. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/get_text/__init__.py +0 -0
  56. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/get_text/default_gitignore +0 -0
  57. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/get_text/get_text.py +0 -0
  58. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/get_text/simpler_get_text.py +0 -0
  59. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/hooks.py +0 -0
  60. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/image.py +0 -0
  61. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/latex.py +0 -0
  62. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/memory.py +0 -0
  63. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
  64. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/prompts/system_prompt.txt +0 -0
  65. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/provider.py +0 -0
  66. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/registry.py +0 -0
  67. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/runtime.py +0 -0
  68. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/scheduler.py +0 -0
  69. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  70. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/service.py +0 -0
  71. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/sessions.py +0 -0
  72. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/status.py +0 -0
  73. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/stream_utils.py +0 -0
  74. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/tool.py +0 -0
  75. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/tray.py +0 -0
  76. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/utils.py +0 -0
  77. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/voice.py +0 -0
  78. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent/worker.py +0 -0
  79. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  80. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  81. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent_framework.egg-info/requires.txt +0 -0
  82. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/codex_agent_framework.egg-info/top_level.txt +0 -0
  83. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/dependencies.txt +0 -0
  84. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/setup.cfg +0 -0
  85. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_agent_runtime.py +0 -0
  86. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_ai.py +0 -0
  87. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_browser.py +0 -0
  88. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_builtin_config.py +0 -0
  89. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_cli.py +0 -0
  90. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_cli_headless.py +0 -0
  91. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_cli_root.py +0 -0
  92. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_client.py +0 -0
  93. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_events.py +0 -0
  94. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_get_text_browser.py +0 -0
  95. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_hooks.py +0 -0
  96. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_image_message.py +0 -0
  97. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_local_desktop.py +0 -0
  98. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_memory.py +0 -0
  99. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_messages.py +0 -0
  100. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_planner.py +0 -0
  101. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_scheduler.py +0 -0
  102. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_service.py +0 -0
  103. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_tray.py +0 -0
  104. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/tests/test_utils.py +0 -0
  105. {codex_agent_framework-0.1.15 → codex_agent_framework-0.1.17}/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.17] - 2026-05-08
8
+ ### Changed
9
+ - Reorganize the Textual TUI into the `codex_agent.tui` package, splitting chat UI code from TUI lifecycle helpers.
10
+ - Reorganize the FastAPI bridge into the `codex_agent.server` package, separating server core/service logic from app and route creation.
11
+ - Remove the obsolete `Message.lasting` field now that message visibility is controlled by `turns_left`, reducing serialized payload noise.
12
+
13
+ ### Removed
14
+ - Remove the old flat `codex_agent.chat` and `codex_agent.tui` modules in favor of the new package layout.
15
+
16
+ ### Tests
17
+ - Validate the full suite at 442 passing tests.
18
+
19
+ ## [0.1.16] - 2026-05-08
20
+ ### Fixed
21
+ - Allow agent startup without an OpenAI API key by disabling OpenAI-dependent capabilities such as voice defaults and the built-in memory plugin instead of failing during initialization.
22
+ - Skip automatic memory archiving when the memory plugin is unavailable.
23
+
24
+ ### Tests
25
+ - Add regression coverage for agent initialization with and without an OpenAI API key.
26
+
7
27
  ## [0.1.15] - 2026-05-08
8
28
  ### Added
9
29
  - Add `AgentRuntime` as the canonical external runtime contract for HTTP server, headless CLI, in-process, and process-backed integrations.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.15
3
+ Version: 0.1.17
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -393,6 +393,8 @@ agent = Agent(
393
393
 
394
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`.
395
395
 
396
+ If no OpenAI API key is configured through `openai_api_key` or `OPENAI_API_KEY`, the agent can still start, but OpenAI-dependent capabilities such as voice defaults and the built-in memory plugin are disabled.
397
+
396
398
  ## Project layout
397
399
 
398
400
  ```text
@@ -440,6 +442,8 @@ python -m build
440
442
  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`.
441
443
 
442
444
  ## Recent changes
445
+ - `0.1.17`: reorganize the TUI and FastAPI server into dedicated `codex_agent.tui` and `codex_agent.server` packages, remove obsolete message payload fields, and clean unused imports.
446
+ - `0.1.16`: tolerate missing OpenAI API keys at startup by disabling OpenAI-dependent voice and memory features, and skip memory archiving when the memory plugin is unavailable.
443
447
  - `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.
444
448
  - `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.
445
449
  - `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.
@@ -344,6 +344,8 @@ agent = Agent(
344
344
 
345
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`.
346
346
 
347
+ If no OpenAI API key is configured through `openai_api_key` or `OPENAI_API_KEY`, the agent can still start, but OpenAI-dependent capabilities such as voice defaults and the built-in memory plugin are disabled.
348
+
347
349
  ## Project layout
348
350
 
349
351
  ```text
@@ -391,6 +393,8 @@ python -m build
391
393
  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`.
392
394
 
393
395
  ## Recent changes
396
+ - `0.1.17`: reorganize the TUI and FastAPI server into dedicated `codex_agent.tui` and `codex_agent.server` packages, remove obsolete message payload fields, and clean unused imports.
397
+ - `0.1.16`: tolerate missing OpenAI API keys at startup by disabling OpenAI-dependent voice and memory features, and skip memory archiving when the memory plugin is unavailable.
394
398
  - `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.
395
399
  - `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.
396
400
  - `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.
@@ -1,5 +1,4 @@
1
- from .utils import text_content, ensure_runtime_dir, upload_file_to_folder, project_python_env
2
- from modict import modict
1
+ from .utils import text_content, ensure_runtime_dir, upload_file_to_folder, project_python_env, runtime_join
3
2
  import os
4
3
  import sys
5
4
  from .message import DeveloperMessage, ServerToolResponseMessage, SystemMessage, ToolResultMessage, UserMessage, message_from_data
@@ -17,6 +16,7 @@ from .status import StatusManager
17
16
  from .tool import ImageGenerationTool, reset_current_agent, set_current_agent
18
17
  from .ai import AIBadRequestError, AIClient
19
18
  from .voice import VoiceProcessor
19
+ from .memory import RAGMemory
20
20
  from .latex import LaTeXProcessor
21
21
  from .event import AgentInterruptRequestedEvent, AgentInterrupted, AgentInterruptedEvent, AudioPlaybackEvent, EventBus
22
22
  from pynteract import Shell
@@ -39,6 +39,7 @@ class Agent:
39
39
  self.config=self.config_manager.config
40
40
  self.config_manager.load()
41
41
  self.config_manager.update(kwargs, save=False)
42
+ self.config_manager.apply_openai_capability_defaults()
42
43
  self.events=EventBus()
43
44
  self.registry=RegistryManager(self)
44
45
  self.tools=self.registry.tools
@@ -65,6 +66,7 @@ class Agent:
65
66
  self.registry.init_providers_folder()
66
67
  self.command_manager.init_folder()
67
68
  self.ai=AIClient(self)
69
+ self.memory=RAGMemory(file=runtime_join("memory.json"))
68
70
  self.voice=VoiceProcessor(self)
69
71
  self.latex=LaTeXProcessor()
70
72
  self.content_processors=[self.voice]
@@ -429,5 +431,5 @@ class Agent:
429
431
  import os
430
432
  if os.environ.get('AGENT_ROOT_PATH') is None:
431
433
  set_root_path(file=__file__)
432
- from .chat import Chat
434
+ from .tui import Chat
433
435
  Chat(self).run()
@@ -2,7 +2,7 @@ from pathlib import Path
2
2
  import subprocess
3
3
 
4
4
  from codex_agent import Agent
5
- from codex_agent.chat import Chat
5
+ from codex_agent.tui import Chat
6
6
  from codex_agent.server import create_app
7
7
  from codex_agent.service import install_user_service, uninstall_user_service
8
8
  from codex_agent.tray import run_tray
@@ -121,6 +121,14 @@ class ConfigManager:
121
121
  self.save()
122
122
  return self.config
123
123
 
124
+ def has_openai_api_key(self):
125
+ return bool(self.config.get("openai_api_key") or os.getenv("OPENAI_API_KEY"))
126
+
127
+ def apply_openai_capability_defaults(self):
128
+ if not self.has_openai_api_key():
129
+ self.config.voice_enabled = False
130
+ return self.config
131
+
124
132
  def system_message(self):
125
133
  return SystemMessage(content=self.system_prompt())
126
134
 
@@ -265,7 +265,7 @@ class MainLoopManager:
265
265
 
266
266
  if self.require_next_step and not self.stop_agentic_loop:
267
267
  continue
268
- if self.agent.config.get("auto_archive_memory", True):
268
+ if self.agent.config.get("auto_archive_memory", True) and self.agent.plugins.get("memory"):
269
269
  self.agent.plugins.memory.archive_last_turn_async()
270
270
  return True
271
271
  except AgentInterrupted:
@@ -32,7 +32,6 @@ class Message(modict):
32
32
  id = modict.factory(lambda: short_id())
33
33
  session_id = None
34
34
  timestamp = modict.factory(lambda: timestamp())
35
- lasting = 0
36
35
  turns_left = -1
37
36
  embedding = None
38
37
  estimated_token_count = modict.factory(dict)
@@ -62,6 +62,8 @@ class PluginsManager:
62
62
  continue
63
63
  if not self.agent.config_manager.builtin_enabled("plugins", module_info.name):
64
64
  continue
65
+ if module_info.name == "memory" and not self.agent.config_manager.has_openai_api_key():
66
+ continue
65
67
  module = importlib.import_module(f"{builtin_plugins.__name__}.{module_info.name}")
66
68
  self.add_builtin_from_module(module)
67
69
 
@@ -0,0 +1,29 @@
1
+ """FastAPI server package for codex-agent."""
2
+
3
+ from .app import app, create_app, main
4
+ from .core import (
5
+ AgentServer,
6
+ SERVER_CONFIG_FILE,
7
+ SERVER_EVENTS_FILE,
8
+ create_server_agent,
9
+ load_server_config,
10
+ remembered_server_session,
11
+ save_server_config,
12
+ server_config_file,
13
+ server_events_file,
14
+ )
15
+
16
+ __all__ = [
17
+ "AgentServer",
18
+ "SERVER_CONFIG_FILE",
19
+ "SERVER_EVENTS_FILE",
20
+ "app",
21
+ "create_app",
22
+ "create_server_agent",
23
+ "load_server_config",
24
+ "main",
25
+ "remembered_server_session",
26
+ "save_server_config",
27
+ "server_config_file",
28
+ "server_events_file",
29
+ ]
@@ -0,0 +1,201 @@
1
+ import json
2
+
3
+ from ..agent import Agent
4
+ from ..agent_runtime import InProcessAgentRuntime, ProcessAgentRuntime
5
+ from .core import AgentServer, create_server_agent, remembered_server_session
6
+
7
+ def create_app(agent: Agent | None = None, start_agent=False, process_agent=False, **agent_kwargs):
8
+ from contextlib import asynccontextmanager
9
+
10
+ from fastapi import FastAPI, HTTPException, Request
11
+ from fastapi.responses import StreamingResponse
12
+ from pydantic import BaseModel
13
+
14
+ if process_agent and agent is None:
15
+ if "session" not in agent_kwargs:
16
+ agent_kwargs["session"] = remembered_server_session() or "latest"
17
+ runtime = ProcessAgentRuntime(agent_kwargs=agent_kwargs)
18
+ else:
19
+ runtime = InProcessAgentRuntime(agent or create_server_agent(**agent_kwargs))
20
+ service = AgentServer(runtime=runtime)
21
+ if start_agent:
22
+ service.start()
23
+
24
+ @asynccontextmanager
25
+ async def lifespan(app):
26
+ try:
27
+ yield
28
+ finally:
29
+ service.stop(timeout=1.0)
30
+
31
+ app = FastAPI(title="Codex Agent", version="0.1.17", lifespan=lifespan)
32
+ app.state.agent_service = service
33
+
34
+ class TurnRequest(BaseModel):
35
+ prompt: str
36
+
37
+ class InterruptRequest(BaseModel):
38
+ reason: str = "api"
39
+
40
+ class ConfigUpdateRequest(BaseModel):
41
+ values: dict = {}
42
+ save: bool = True
43
+
44
+ class WakeupRequest(BaseModel):
45
+ prompt: str
46
+ run_at: str | None = None
47
+ delay_seconds: float | None = None
48
+ interval_seconds: float | None = None
49
+ title: str = ""
50
+
51
+ class RestartRequest(BaseModel):
52
+ prompt: str | None = None
53
+ title: str = "post-restart wakeup"
54
+
55
+ def jsonable(value):
56
+ return json.loads(json.dumps(value, ensure_ascii=False, default=str))
57
+
58
+ def busy_guard(call):
59
+ try:
60
+ return call()
61
+ except RuntimeError as exc:
62
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
63
+ except ValueError as exc:
64
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
65
+
66
+ @app.get("/health")
67
+ def health():
68
+ return {"ok": True, "busy": service.busy, "session_id": service.current_session_id}
69
+
70
+ @app.get("/status")
71
+ def status():
72
+ return jsonable(service.status())
73
+
74
+ @app.get("/config")
75
+ def config():
76
+ return jsonable(service.config())
77
+
78
+ @app.patch("/config")
79
+ def update_config(request: ConfigUpdateRequest):
80
+ return jsonable(busy_guard(lambda: service.update_config(request.values, save=request.save)))
81
+
82
+ @app.get("/session")
83
+ def session():
84
+ return jsonable(service.session())
85
+
86
+ @app.get("/sessions")
87
+ def sessions():
88
+ return jsonable(service.sessions())
89
+
90
+ @app.post("/sessions/new")
91
+ def start_new_session():
92
+ return jsonable(busy_guard(service.start_new_session))
93
+
94
+ @app.post("/sessions/{session_id}/load")
95
+ def load_session(session_id: str):
96
+ return jsonable(busy_guard(lambda: service.load_session(session_id)))
97
+
98
+ @app.delete("/sessions/{session_id}")
99
+ def delete_session(session_id: str):
100
+ return jsonable(busy_guard(lambda: service.delete_session(session_id)))
101
+
102
+ @app.post("/sessions/navigate/{direction}")
103
+ def navigate_session(direction: str):
104
+ return jsonable(busy_guard(lambda: service.navigate_session(direction)))
105
+
106
+ @app.get("/messages")
107
+ def messages():
108
+ return jsonable(service.messages())
109
+
110
+ @app.get("/memory")
111
+ def memory():
112
+ return jsonable(service.memory())
113
+
114
+ @app.get("/tools")
115
+ def tools():
116
+ return jsonable(service.tools())
117
+
118
+ @app.get("/tui")
119
+ def tui():
120
+ return jsonable(service.tui())
121
+
122
+ @app.post("/tui/open")
123
+ def open_tui(request: Request):
124
+ base_url = str(request.base_url).rstrip("/")
125
+ return jsonable(busy_guard(lambda: service.open_tui(base_url=base_url)))
126
+
127
+ @app.post("/tui/close")
128
+ def close_tui():
129
+ return jsonable(service.close_tui())
130
+
131
+ @app.post("/restart")
132
+ @app.post("/restart/")
133
+ def restart(request: RestartRequest):
134
+ return jsonable(busy_guard(lambda: service.restart(**request.model_dump())))
135
+
136
+ @app.get("/wakeups")
137
+ def wakeups(include_done: bool = True):
138
+ return jsonable(service.wakeups(include_done=include_done))
139
+
140
+ @app.post("/wakeups", status_code=201)
141
+ def schedule_wakeup(request: WakeupRequest):
142
+ return jsonable(busy_guard(lambda: service.schedule_wakeup(**request.model_dump())))
143
+
144
+ @app.post("/wakeups/{job_id}/cancel")
145
+ def cancel_wakeup(job_id: str):
146
+ return jsonable(busy_guard(lambda: service.cancel_wakeup(job_id)))
147
+
148
+ @app.delete("/wakeups/{job_id}")
149
+ def delete_wakeup(job_id: str):
150
+ return jsonable(busy_guard(lambda: service.delete_wakeup(job_id)))
151
+
152
+ @app.post("/turns", status_code=202)
153
+ def turns(request: TurnRequest):
154
+ result = busy_guard(lambda: service.submit_prompt(request.prompt))
155
+ return {"accepted": True, **jsonable(result)}
156
+
157
+ @app.post("/interrupt")
158
+ def interrupt(request: InterruptRequest):
159
+ return jsonable(service.interrupt(request.reason))
160
+
161
+ @app.get("/events")
162
+ def events(limit: int | None = None, after_sequence: int | None = None, client_pid: int | None = None):
163
+ try:
164
+ subscription = service.subscribe(after_sequence=after_sequence, client_pid=client_pid)
165
+ except RuntimeError as exc:
166
+ raise HTTPException(status_code=409, detail=str(exc)) from exc
167
+
168
+ def stream():
169
+ count = 0
170
+ for payload in subscription:
171
+ lines = []
172
+ if payload.get("id"):
173
+ lines.append(f"id: {payload.id}")
174
+ lines.append(f"event: {payload.get('type', 'message')}")
175
+ lines.append(f"data: {json.dumps(payload, ensure_ascii=False, default=str)}")
176
+ yield "\n".join(lines) + "\n\n"
177
+ count += 1
178
+ if limit is not None and count >= limit:
179
+ break
180
+ return StreamingResponse(stream(), media_type="text/event-stream")
181
+
182
+ @app.get("/events/replay")
183
+ def replay_events(before_sequence: int | None = None, session_id: str | None = None):
184
+ return jsonable(service.replay_events(before_sequence=before_sequence, session_id=session_id))
185
+
186
+ return app
187
+
188
+
189
+ app = None
190
+
191
+
192
+ def main():
193
+ import uvicorn
194
+
195
+ global app
196
+ app = create_app(start_agent=True, process_agent=True)
197
+ uvicorn.run(app, host="127.0.0.1", port=8765, timeout_graceful_shutdown=2)
198
+
199
+
200
+ if __name__ == "__main__":
201
+ main()
@@ -8,14 +8,14 @@ from collections import deque
8
8
 
9
9
  from modict import modict
10
10
 
11
- from .agent import Agent
12
- from .agent_runtime import AgentRuntime, InProcessAgentRuntime, ProcessAgentRuntime
13
- from .event import Event
14
- from .message import UserMessage
15
- from .sessions import AgentSession
16
- from .service import pop_restart_wakeup, restart_server_service, write_restart_wakeup
17
- from .utils import ensure_runtime_dir, runtime_join
18
- from .tui import clear_tui, current_tui, open_tui_terminal, terminate_tui
11
+ from ..agent import Agent
12
+ from ..agent_runtime import AgentRuntime
13
+ from ..event import Event
14
+ from ..message import UserMessage
15
+ from ..sessions import AgentSession
16
+ from ..service import pop_restart_wakeup, restart_server_service, write_restart_wakeup
17
+ from ..utils import ensure_runtime_dir, runtime_join
18
+ from ..tui import clear_tui, current_tui, open_tui_terminal, terminate_tui
19
19
 
20
20
 
21
21
 
@@ -266,6 +266,16 @@ class AgentServer:
266
266
  for subscriber, state in list(self.subscribers.items()):
267
267
  if not state.thread.is_alive():
268
268
  self.subscribers.pop(subscriber, None)
269
+ return len(self.subscribers)
270
+
271
+ def subscriber_count(self):
272
+ if not self.subscriber_lock.acquire(timeout=0.05):
273
+ return len(self.subscribers)
274
+ try:
275
+ self.prune_subscribers()
276
+ return len(self.subscribers)
277
+ finally:
278
+ self.subscriber_lock.release()
269
279
 
270
280
  def close_subscribers(self, reason="server shutdown"):
271
281
  self.shutdown_requested.set()
@@ -277,7 +287,9 @@ class AgentServer:
277
287
  def subscribe(self, after_sequence=None, client_pid=None):
278
288
  after_sequence = int(after_sequence) if after_sequence is not None else None
279
289
  client_pid = int(client_pid) if client_pid is not None else None
280
- with self.subscriber_lock:
290
+ if not self.subscriber_lock.acquire(timeout=1.0):
291
+ raise RuntimeError("event subscription manager is busy")
292
+ try:
281
293
  self.shutdown_requested.clear()
282
294
  self.prune_subscribers()
283
295
  tui = current_tui()
@@ -297,6 +309,8 @@ class AgentServer:
297
309
  and int(event.get("sequence") or 0) <= replay_before_sequence
298
310
  and self.event_belongs_to_session(event, session_id)
299
311
  ]
312
+ finally:
313
+ self.subscriber_lock.release()
300
314
  def stream():
301
315
  try:
302
316
  yield modict(
@@ -321,18 +335,20 @@ class AgentServer:
321
335
  if payload.get("type") == "server_shutdown":
322
336
  break
323
337
  finally:
324
- with self.subscriber_lock:
338
+ if not self.subscriber_lock.acquire(timeout=1.0):
339
+ return
340
+ try:
325
341
  self.subscribers.pop(subscriber, None)
342
+ finally:
343
+ self.subscriber_lock.release()
326
344
  return stream()
327
345
 
328
346
  def status(self):
329
- with self.subscriber_lock:
330
- self.prune_subscribers()
331
347
  status = self.runtime.get_status()
332
348
  status.update(
333
349
  busy=self.busy,
334
350
  current_turn_id=self.current_turn_id,
335
- subscribers=len(self.subscribers),
351
+ subscribers=self.subscriber_count(),
336
352
  event_backlog=len(self.events),
337
353
  )
338
354
  self.persist_current_session()
@@ -483,198 +499,3 @@ class AgentServer:
483
499
  raise RuntimeError("agent is busy")
484
500
 
485
501
 
486
- def create_app(agent: Agent | None = None, start_agent=False, process_agent=False, **agent_kwargs):
487
- from contextlib import asynccontextmanager
488
-
489
- from fastapi import FastAPI, HTTPException, Request
490
- from fastapi.responses import StreamingResponse
491
- from pydantic import BaseModel
492
-
493
- if process_agent and agent is None:
494
- if "session" not in agent_kwargs:
495
- agent_kwargs["session"] = remembered_server_session() or "latest"
496
- runtime = ProcessAgentRuntime(agent_kwargs=agent_kwargs)
497
- else:
498
- runtime = InProcessAgentRuntime(agent or create_server_agent(**agent_kwargs))
499
- service = AgentServer(runtime=runtime)
500
- if start_agent:
501
- service.start()
502
-
503
- @asynccontextmanager
504
- async def lifespan(app):
505
- try:
506
- yield
507
- finally:
508
- service.stop(timeout=1.0)
509
-
510
- app = FastAPI(title="Codex Agent", version="0.1.0", lifespan=lifespan)
511
- app.state.agent_service = service
512
-
513
- class TurnRequest(BaseModel):
514
- prompt: str
515
-
516
- class InterruptRequest(BaseModel):
517
- reason: str = "api"
518
-
519
- class ConfigUpdateRequest(BaseModel):
520
- values: dict = {}
521
- save: bool = True
522
-
523
- class WakeupRequest(BaseModel):
524
- prompt: str
525
- run_at: str | None = None
526
- delay_seconds: float | None = None
527
- interval_seconds: float | None = None
528
- title: str = ""
529
-
530
- class RestartRequest(BaseModel):
531
- prompt: str | None = None
532
- title: str = "post-restart wakeup"
533
-
534
- def jsonable(value):
535
- return json.loads(json.dumps(value, ensure_ascii=False, default=str))
536
-
537
- def busy_guard(call):
538
- try:
539
- return call()
540
- except RuntimeError as exc:
541
- raise HTTPException(status_code=409, detail=str(exc)) from exc
542
- except ValueError as exc:
543
- raise HTTPException(status_code=400, detail=str(exc)) from exc
544
-
545
- @app.get("/health")
546
- def health():
547
- return {"ok": True, "busy": service.busy, "session_id": service.current_session_id}
548
-
549
- @app.get("/status")
550
- def status():
551
- return jsonable(service.status())
552
-
553
- @app.get("/config")
554
- def config():
555
- return jsonable(service.config())
556
-
557
- @app.patch("/config")
558
- def update_config(request: ConfigUpdateRequest):
559
- return jsonable(busy_guard(lambda: service.update_config(request.values, save=request.save)))
560
-
561
- @app.get("/session")
562
- def session():
563
- return jsonable(service.session())
564
-
565
- @app.get("/sessions")
566
- def sessions():
567
- return jsonable(service.sessions())
568
-
569
- @app.post("/sessions/new")
570
- def start_new_session():
571
- return jsonable(busy_guard(service.start_new_session))
572
-
573
- @app.post("/sessions/{session_id}/load")
574
- def load_session(session_id: str):
575
- return jsonable(busy_guard(lambda: service.load_session(session_id)))
576
-
577
- @app.delete("/sessions/{session_id}")
578
- def delete_session(session_id: str):
579
- return jsonable(busy_guard(lambda: service.delete_session(session_id)))
580
-
581
- @app.post("/sessions/navigate/{direction}")
582
- def navigate_session(direction: str):
583
- return jsonable(busy_guard(lambda: service.navigate_session(direction)))
584
-
585
- @app.get("/messages")
586
- def messages():
587
- return jsonable(service.messages())
588
-
589
- @app.get("/memory")
590
- def memory():
591
- return jsonable(service.memory())
592
-
593
- @app.get("/tools")
594
- def tools():
595
- return jsonable(service.tools())
596
-
597
- @app.get("/tui")
598
- def tui():
599
- return jsonable(service.tui())
600
-
601
- @app.post("/tui/open")
602
- def open_tui(request: Request):
603
- base_url = str(request.base_url).rstrip("/")
604
- return jsonable(busy_guard(lambda: service.open_tui(base_url=base_url)))
605
-
606
- @app.post("/tui/close")
607
- def close_tui():
608
- return jsonable(service.close_tui())
609
-
610
- @app.post("/restart")
611
- @app.post("/restart/")
612
- def restart(request: RestartRequest):
613
- return jsonable(busy_guard(lambda: service.restart(**request.model_dump())))
614
-
615
- @app.get("/wakeups")
616
- def wakeups(include_done: bool = True):
617
- return jsonable(service.wakeups(include_done=include_done))
618
-
619
- @app.post("/wakeups", status_code=201)
620
- def schedule_wakeup(request: WakeupRequest):
621
- return jsonable(busy_guard(lambda: service.schedule_wakeup(**request.model_dump())))
622
-
623
- @app.post("/wakeups/{job_id}/cancel")
624
- def cancel_wakeup(job_id: str):
625
- return jsonable(busy_guard(lambda: service.cancel_wakeup(job_id)))
626
-
627
- @app.delete("/wakeups/{job_id}")
628
- def delete_wakeup(job_id: str):
629
- return jsonable(busy_guard(lambda: service.delete_wakeup(job_id)))
630
-
631
- @app.post("/turns", status_code=202)
632
- def turns(request: TurnRequest):
633
- result = busy_guard(lambda: service.submit_prompt(request.prompt))
634
- return {"accepted": True, **jsonable(result)}
635
-
636
- @app.post("/interrupt")
637
- def interrupt(request: InterruptRequest):
638
- return jsonable(service.interrupt(request.reason))
639
-
640
- @app.get("/events")
641
- def events(limit: int | None = None, after_sequence: int | None = None, client_pid: int | None = None):
642
- try:
643
- subscription = service.subscribe(after_sequence=after_sequence, client_pid=client_pid)
644
- except RuntimeError as exc:
645
- raise HTTPException(status_code=409, detail=str(exc)) from exc
646
-
647
- def stream():
648
- count = 0
649
- for payload in subscription:
650
- lines = []
651
- if payload.get("id"):
652
- lines.append(f"id: {payload.id}")
653
- lines.append(f"event: {payload.get('type', 'message')}")
654
- lines.append(f"data: {json.dumps(payload, ensure_ascii=False, default=str)}")
655
- yield "\n".join(lines) + "\n\n"
656
- count += 1
657
- if limit is not None and count >= limit:
658
- break
659
- return StreamingResponse(stream(), media_type="text/event-stream")
660
-
661
- @app.get("/events/replay")
662
- def replay_events(before_sequence: int | None = None, session_id: str | None = None):
663
- return jsonable(service.replay_events(before_sequence=before_sequence, session_id=session_id))
664
-
665
- return app
666
-
667
-
668
- app = None
669
-
670
-
671
- def main():
672
- import uvicorn
673
-
674
- global app
675
- app = create_app(start_agent=True, process_agent=True)
676
- uvicorn.run(app, host="127.0.0.1", port=8765, timeout_graceful_shutdown=2)
677
-
678
-
679
- if __name__ == "__main__":
680
- main()