codex-agent-framework 0.1.21__tar.gz → 0.1.22__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 (107) hide show
  1. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/CHANGELOG.md +17 -0
  2. {codex_agent_framework-0.1.21/codex_agent_framework.egg-info → codex_agent_framework-0.1.22}/PKG-INFO +5 -4
  3. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/README.md +4 -3
  4. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/__init__.py +2 -0
  5. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/agent_runtime.py +57 -44
  6. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/subagents/__init__.py +9 -9
  7. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/client.py +5 -11
  8. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/command.py +21 -11
  9. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/context.py +20 -10
  10. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/event.py +1 -9
  11. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/mainloop.py +36 -25
  12. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/message.py +1 -9
  13. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/server/app.py +2 -1
  14. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/server/core.py +94 -57
  15. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/sessions.py +6 -16
  16. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/tool.py +13 -6
  17. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/tui/chat.py +76 -9
  18. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/utils.py +9 -0
  19. codex_agent_framework-0.1.22/codex_agent/version.py +1 -0
  20. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22/codex_agent_framework.egg-info}/PKG-INFO +5 -4
  21. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent_framework.egg-info/SOURCES.txt +1 -0
  22. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/pyproject.toml +1 -1
  23. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_chat.py +71 -0
  24. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_client.py +2 -2
  25. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_server.py +65 -1
  26. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/LICENSE +0 -0
  27. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/MANIFEST.in +0 -0
  28. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/__main__.py +0 -0
  29. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/agent.py +0 -0
  30. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/ai.py +0 -0
  31. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/browser.py +0 -0
  32. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_commands.py +0 -0
  33. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/__init__.py +0 -0
  34. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  35. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  36. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  37. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/content/system.py +0 -0
  38. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/content/vision.py +0 -0
  39. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  40. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  41. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  42. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/files/tools.py +0 -0
  43. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  44. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  45. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  46. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  47. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  48. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  49. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/cli/__init__.py +0 -0
  50. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/cli/headless.py +0 -0
  51. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/cli/main.py +0 -0
  52. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/cli/output.py +0 -0
  53. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/cli/root.py +0 -0
  54. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/cli/runner.py +0 -0
  55. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/config.py +0 -0
  56. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/desktop.py +0 -0
  57. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/get_text/__init__.py +0 -0
  58. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/get_text/default_gitignore +0 -0
  59. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/get_text/get_text.py +0 -0
  60. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/get_text/simpler_get_text.py +0 -0
  61. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/hooks.py +0 -0
  62. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/image.py +0 -0
  63. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/latex.py +0 -0
  64. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/plugin.py +0 -0
  65. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
  66. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/prompts/system_prompt.txt +0 -0
  67. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/provider.py +0 -0
  68. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/runtime.py +0 -0
  69. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/scheduler.py +0 -0
  70. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  71. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/server/__init__.py +0 -0
  72. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/service.py +0 -0
  73. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/status.py +0 -0
  74. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/stream_utils.py +0 -0
  75. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/tray.py +0 -0
  76. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/tui/__init__.py +0 -0
  77. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/tui/lifecycle.py +0 -0
  78. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/voice.py +0 -0
  79. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent/worker.py +0 -0
  80. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  81. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  82. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent_framework.egg-info/requires.txt +0 -0
  83. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/codex_agent_framework.egg-info/top_level.txt +0 -0
  84. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/dependencies.txt +0 -0
  85. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/setup.cfg +0 -0
  86. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_agent.py +0 -0
  87. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_agent_runtime.py +0 -0
  88. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_ai.py +0 -0
  89. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_browser.py +0 -0
  90. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_builtin_config.py +0 -0
  91. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_cli.py +0 -0
  92. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_cli_headless.py +0 -0
  93. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_cli_root.py +0 -0
  94. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_events.py +0 -0
  95. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_get_text_browser.py +0 -0
  96. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_hooks.py +0 -0
  97. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_image_message.py +0 -0
  98. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_local_desktop.py +0 -0
  99. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_memory.py +0 -0
  100. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_messages.py +0 -0
  101. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_planner.py +0 -0
  102. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_scheduler.py +0 -0
  103. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_service.py +0 -0
  104. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_tray.py +0 -0
  105. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_tui.py +0 -0
  106. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_utils.py +0 -0
  107. {codex_agent_framework-0.1.21 → codex_agent_framework-0.1.22}/tests/test_worker.py +0 -0
@@ -4,6 +4,23 @@ 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.22] - 2026-05-10
8
+ ### Changed
9
+ - Harden the FastAPI/SSE bridge and Textual TUI streaming path after a stabilization audit: finite SSE read timeouts, batched live deltas, synchronous replay delta flushes, and explicit stale-subscriber replacement shutdowns.
10
+ - Improve process-backed runtime robustness with guarded startup, isolated event handlers, and clearer cached status/turn-state updates.
11
+ - Consolidate shared helpers for type hierarchy, turn outcomes, atomic session writes, tool exclusion matching, context truncation, and subagent child creation while removing smaller duplicated or dead helper paths.
12
+ - Publish the package version from a single `codex_agent.version` module and use it for FastAPI metadata.
13
+
14
+ ### Fixed
15
+ - Prevent stale SSE subscribers from lingering silently when a replacement TUI stream connects.
16
+ - Avoid concurrent subscriber-table mutation during event publication and serialize persisted turn-event rewrite/append operations.
17
+ - Preserve replay ordering in the TUI so caught-up deltas render before newly batched live stream deltas.
18
+ - Keep one failing process-runtime event handler from stopping delivery to later handlers.
19
+
20
+ ### Tests
21
+ - Validate post-restart API/import/runtime smoke tests after a real server restart.
22
+ - Validate `pyflakes codex_agent tests`, targeted server/client/chat smoke tests, and the full suite at 489 passing tests.
23
+
7
24
  ## [0.1.21] - 2026-05-10
8
25
  ### Added
9
26
  - Add the structured `AgentTurn` API: `agent(...)` and `AgentFuture.wait()` now return a turn object with `result`, `completed`, `reason`, `messages`, and `events`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.21
3
+ Version: 0.1.22
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -846,13 +846,13 @@ The tests isolate `AGENT_RUNTIME_DIR` automatically, so they should not create o
846
846
  The current tree validates at:
847
847
 
848
848
  ```text
849
- 483 passed
849
+ 489 passed
850
850
  ```
851
851
 
852
- The published `0.1.21` release was validated at:
852
+ The published `0.1.22` release was validated at:
853
853
 
854
854
  ```text
855
- 483 passed
855
+ 489 passed
856
856
  ```
857
857
 
858
858
  ## Packaging
@@ -870,6 +870,7 @@ The distribution includes prompt text files, `codex_agent/get_text/default_gitig
870
870
 
871
871
  ## Recent changes
872
872
 
873
+ - `0.1.22`: harden FastAPI/SSE and TUI streaming after a stabilization audit, including finite SSE read timeouts, stale-subscriber shutdown, persisted turn-event locking, synchronous replay delta flushes, process-runtime startup guards, and isolated event handlers.
873
874
  - `0.1.21`: add structured `AgentTurn` returns with messages/events, resumable non-terminal turns for `auto_proceed=False`, profile-based subagents, hierarchical message/event types, and internal wakeups as developer messages.
874
875
  - `0.1.20`: make runtime plugins the single user-facing extension unit for tools/providers/commands, remove legacy direct runtime tool/provider loading, split tool/provider managers, and keep system-prompt templating on an explicit minimal namespace.
875
876
  - `0.1.19`: prune compacted raw session history after compaction, keep only the latest compaction anchor plus the new compaction and any unfinished remainder, and drop older redundant compaction summaries from active session files.
@@ -797,13 +797,13 @@ The tests isolate `AGENT_RUNTIME_DIR` automatically, so they should not create o
797
797
  The current tree validates at:
798
798
 
799
799
  ```text
800
- 483 passed
800
+ 489 passed
801
801
  ```
802
802
 
803
- The published `0.1.21` release was validated at:
803
+ The published `0.1.22` release was validated at:
804
804
 
805
805
  ```text
806
- 483 passed
806
+ 489 passed
807
807
  ```
808
808
 
809
809
  ## Packaging
@@ -821,6 +821,7 @@ The distribution includes prompt text files, `codex_agent/get_text/default_gitig
821
821
 
822
822
  ## Recent changes
823
823
 
824
+ - `0.1.22`: harden FastAPI/SSE and TUI streaming after a stabilization audit, including finite SSE read timeouts, stale-subscriber shutdown, persisted turn-event locking, synchronous replay delta flushes, process-runtime startup guards, and isolated event handlers.
824
825
  - `0.1.21`: add structured `AgentTurn` returns with messages/events, resumable non-terminal turns for `auto_proceed=False`, profile-based subagents, hierarchical message/event types, and internal wakeups as developer messages.
825
826
  - `0.1.20`: make runtime plugins the single user-facing extension unit for tools/providers/commands, remove legacy direct runtime tool/provider loading, split tool/provider managers, and keep system-prompt templating on an explicit minimal namespace.
826
827
  - `0.1.19`: prune compacted raw session history after compaction, keep only the latest compaction anchor plus the new compaction and any unfinished remainder, and drop older redundant compaction summaries from active session files.
@@ -56,6 +56,7 @@ from .sessions import AgentSession, SessionsManager
56
56
  from .status import StatusManager
57
57
  from .tool import ImageGenerationTool, ServerTool, Tool, ToolsManager, WebSearchTool, get_agent, get_current_call_id, tool
58
58
  from .worker import SummarizerWorker, TurnSummaryWorker, Worker
59
+ from .version import __version__
59
60
 
60
61
  __all__ = [
61
62
  "Agent",
@@ -132,4 +133,5 @@ __all__ = [
132
133
  "SummarizerWorker",
133
134
  "TurnSummaryWorker",
134
135
  "Worker",
136
+ "__version__",
135
137
  ]
@@ -262,6 +262,8 @@ class ProcessAgentRuntime(AgentRuntime):
262
262
  self.event_handlers = []
263
263
  self.pending = {}
264
264
  self.pending_lock = threading.Lock()
265
+ self.start_lock = threading.RLock()
266
+ self.starting = False
265
267
  self.running = threading.Event()
266
268
  self.cache = modict(
267
269
  busy=False,
@@ -277,24 +279,28 @@ class ProcessAgentRuntime(AgentRuntime):
277
279
  )
278
280
 
279
281
  def start(self):
280
- if self.process and self.process.is_alive():
282
+ with self.start_lock:
283
+ if self.process and self.process.is_alive():
284
+ return self
285
+ self.starting = True
286
+ self.running.set()
287
+ self.process = self.context.Process(
288
+ target=agent_worker_main,
289
+ args=(self.command_queue, self.response_queue, self.event_queue, self.agent_kwargs),
290
+ name="codex-agent-worker",
291
+ daemon=True,
292
+ )
293
+ self.process.start()
294
+ self.event_thread = threading.Thread(target=self._event_loop, name="agent-runtime-events", daemon=True)
295
+ self.event_thread.start()
296
+ try:
297
+ self.call("start", timeout=self.command_timeout, autostart=False)
298
+ self.refresh_cache()
299
+ except Exception:
300
+ pass
301
+ finally:
302
+ self.starting = False
281
303
  return self
282
- self.running.set()
283
- self.process = self.context.Process(
284
- target=agent_worker_main,
285
- args=(self.command_queue, self.response_queue, self.event_queue, self.agent_kwargs),
286
- name="codex-agent-worker",
287
- daemon=True,
288
- )
289
- self.process.start()
290
- self.event_thread = threading.Thread(target=self._event_loop, name="agent-runtime-events", daemon=True)
291
- self.event_thread.start()
292
- try:
293
- self.call("start", timeout=self.command_timeout)
294
- self.refresh_cache()
295
- except Exception:
296
- pass
297
- return self
298
304
 
299
305
  def stop(self, timeout=None):
300
306
  timeout = timeout or 2.0
@@ -332,14 +338,14 @@ class ProcessAgentRuntime(AgentRuntime):
332
338
  self.cache[key] = self.call(command, timeout=self.status_timeout)
333
339
  except Exception:
334
340
  pass
335
- status = modict(self.cache.get("status") or {})
336
- self.cache.busy = bool(status.get("busy", self.cache.busy))
337
- self.cache.current_turn_id = status.get("current_turn_id", self.cache.current_turn_id)
338
- self.cache.current_session_id = status.get("session_id", self.cache.current_session_id)
341
+ self.update_status_cache(self.cache.get("status") or {})
339
342
  return self.cache
340
343
 
341
- def call(self, name, payload=None, timeout=None):
342
- self.start() if not (self.process and self.process.is_alive()) else None
344
+ def call(self, name, payload=None, timeout=None, autostart=True):
345
+ if not (self.process and self.process.is_alive()):
346
+ if not autostart or self.starting:
347
+ raise AgentRuntimeError("agent worker process is not running")
348
+ self.start()
343
349
  command_id = str(uuid.uuid4())
344
350
  future = _RuntimeFuture(command_id)
345
351
  with self.pending_lock:
@@ -377,24 +383,39 @@ class ProcessAgentRuntime(AgentRuntime):
377
383
  else:
378
384
  future.set_error(AgentRuntimeError(response.get("error") or "agent worker command failed"))
379
385
 
386
+ def update_status_cache(self, status):
387
+ status = modict(status or {})
388
+ self.cache.status = status
389
+ self.cache.busy = bool(status.get("busy", self.cache.busy))
390
+ self.cache.current_turn_id = status.get("current_turn_id", self.cache.current_turn_id)
391
+ self.cache.current_session_id = status.get("session_id", self.cache.current_session_id)
392
+ return status
393
+
394
+ def mark_turn_started(self, turn_id=None):
395
+ self.cache.busy = True
396
+ if turn_id is not None:
397
+ self.cache.current_turn_id = turn_id
398
+
399
+ def mark_turn_finished(self):
400
+ self.cache.busy = False
401
+ self.cache.current_turn_id = None
402
+
380
403
  def _handle_event(self, event):
381
404
  event_type = event.get("type")
382
405
  if event_type == "assistant_turn_start_event":
383
- self.cache.busy = True
406
+ self.mark_turn_started(event.get("turn_id"))
384
407
  elif event_type in ("assistant_turn_end_event", "assistant_turn_error_event"):
385
- self.cache.busy = False
386
- self.cache.current_turn_id = None
408
+ self.mark_turn_finished()
387
409
  elif event_type == "session_loaded_event":
388
410
  self.cache.current_session_id = event.get("session_id")
389
411
  self.cache.session = event.get("session") or self.cache.session
390
412
  elif event_type == "compaction_completed_event" and event.get("status"):
391
- status = modict(event.status)
392
- self.cache.status = status
393
- self.cache.busy = bool(status.get("busy", self.cache.busy))
394
- self.cache.current_turn_id = status.get("current_turn_id", self.cache.current_turn_id)
395
- self.cache.current_session_id = status.get("session_id", self.cache.current_session_id)
413
+ self.update_status_cache(event.status)
396
414
  for handler in list(self.event_handlers):
397
- handler(event)
415
+ try:
416
+ handler(event)
417
+ except Exception:
418
+ continue
398
419
 
399
420
  @property
400
421
  def busy(self):
@@ -410,12 +431,7 @@ class ProcessAgentRuntime(AgentRuntime):
410
431
 
411
432
  def get_status(self):
412
433
  try:
413
- status = modict(self.call("get_status", timeout=self.status_timeout))
414
- self.cache.status = status
415
- self.cache.busy = bool(status.get("busy", self.cache.busy))
416
- self.cache.current_turn_id = status.get("current_turn_id", self.cache.current_turn_id)
417
- self.cache.current_session_id = status.get("session_id", self.cache.current_session_id)
418
- return status
434
+ return self.update_status_cache(self.call("get_status", timeout=self.status_timeout))
419
435
  except Exception:
420
436
  status = modict(self.cache.status or {})
421
437
  status.setdefault("busy", self.cache.busy)
@@ -478,8 +494,7 @@ class ProcessAgentRuntime(AgentRuntime):
478
494
  return None
479
495
  turn_id = self.call("start_turn", timeout=1.0)
480
496
  if turn_id is not None:
481
- self.cache.busy = True
482
- self.cache.current_turn_id = turn_id
497
+ self.mark_turn_started(turn_id)
483
498
  return turn_id
484
499
 
485
500
  def submit_command(self, prompt):
@@ -487,16 +502,14 @@ class ProcessAgentRuntime(AgentRuntime):
487
502
  return None
488
503
  turn_id = self.call("submit_command", {"prompt": prompt}, timeout=1.0)
489
504
  if turn_id is not None:
490
- self.cache.busy = True
491
- self.cache.current_turn_id = turn_id
505
+ self.mark_turn_started(turn_id)
492
506
  return turn_id
493
507
 
494
508
  def submit_message(self, message):
495
509
  result = self.call("submit_message", {"message": message}, timeout=1.0)
496
510
  turn_id = result.get("turn_id") if isinstance(result, dict) else None
497
511
  if turn_id is not None:
498
- self.cache.busy = True
499
- self.cache.current_turn_id = turn_id
512
+ self.mark_turn_started(turn_id)
500
513
  return result
501
514
 
502
515
  def schedule_wakeup(self, **kwargs):
@@ -125,15 +125,11 @@ Fundamentals:
125
125
  return config
126
126
 
127
127
  def relay_child_event(self, subagent, child, event):
128
- wrapped = SubagentEvent(
128
+ self.agent.emit(SubagentEvent(
129
129
  subagent=subagent,
130
130
  event=event,
131
131
  session_id=getattr(child, "current_session_id", ""),
132
- )
133
- self.agent.events.emit(wrapped)
134
- mainloop_manager = getattr(self.agent, "mainloop_manager", None)
135
- if mainloop_manager is not None:
136
- mainloop_manager.record_event(wrapped)
132
+ ))
137
133
  return event
138
134
 
139
135
  def install_child_tools(self, child):
@@ -163,16 +159,20 @@ Fundamentals:
163
159
  if path and os.path.exists(path):
164
160
  os.remove(path)
165
161
 
166
- def run_subagent(self, profile, task):
162
+ def create_child(self, profile_data):
167
163
  from ...agent import Agent
168
164
 
165
+ child = Agent(session="new", **self.child_config(profile_data))
166
+ self.install_child_tools(child)
167
+ return child
168
+
169
+ def run_subagent(self, profile, task):
169
170
  child = None
170
171
  profile_data = None
171
172
  try:
172
173
  profile_data = self.profile(profile)
173
- child = Agent(session="new", **self.child_config(profile_data))
174
+ child = self.create_child(profile_data)
174
175
  child.on("*", lambda event: self.relay_child_event(profile, child, event))
175
- self.install_child_tools(child)
176
176
  prompt = (
177
177
  f"{task}\n\n"
178
178
  "When the delegated task is complete, call the `set_result` tool with the structured result to return to the parent agent."
@@ -6,6 +6,8 @@ from urllib.parse import urlencode
6
6
  import requests
7
7
  from modict import modict
8
8
 
9
+ from .utils import snake_case_type
10
+
9
11
 
10
12
  class AgentClient:
11
13
  """Small HTTP/SSE client for a codex-agent server."""
@@ -13,6 +15,7 @@ class AgentClient:
13
15
  def __init__(self, base_url="http://127.0.0.1:8765", timeout=10):
14
16
  self.base_url = base_url.rstrip("/")
15
17
  self.timeout = timeout
18
+ self.event_read_timeout = max(2.0, min(float(timeout), 5.0))
16
19
  self.client_pid = os.getpid()
17
20
  self._event_handlers = {}
18
21
  self._event_thread = None
@@ -39,7 +42,7 @@ class AgentClient:
39
42
  if isinstance(event_type, str):
40
43
  return event_type
41
44
  if isinstance(event_type, type):
42
- return self.snake_case_type(event_type)
45
+ return snake_case_type(event_type)
43
46
  return str(event_type)
44
47
 
45
48
  def emit_local(self, event):
@@ -129,7 +132,7 @@ class AgentClient:
129
132
  with requests.get(
130
133
  self.events_url(),
131
134
  stream=True,
132
- timeout=(self.timeout, None),
135
+ timeout=(self.timeout, self.event_read_timeout),
133
136
  headers={"Accept": "text/event-stream"},
134
137
  ) as response:
135
138
  response.raise_for_status()
@@ -348,12 +351,3 @@ class AgentClient:
348
351
  def stop(self, timeout=None):
349
352
  return self.stop_events(timeout=timeout or 0.2)
350
353
 
351
- @staticmethod
352
- def snake_case_type(cls):
353
- name = cls.__name__
354
- chars = []
355
- for i, char in enumerate(name):
356
- if char.isupper() and i > 0:
357
- chars.append("_")
358
- chars.append(char.lower())
359
- return "".join(chars)
@@ -13,6 +13,12 @@ from .utils import runtime_join
13
13
 
14
14
 
15
15
  COMMAND_MARKER = "__codex_agent_command__"
16
+ TURN_OUTCOME_TYPE = "turn_outcome"
17
+ TURN_REASON_COMPLETED = "completed"
18
+ TURN_REASON_ERROR = "error"
19
+ TURN_REASON_INTERRUPTED = "interrupted"
20
+ TURN_REASON_REQUIRES_NEXT_STEP = "requires_next_step"
21
+ TURN_REASON_RESULT_SET = "result_set"
16
22
 
17
23
 
18
24
  def command(func=None, **options):
@@ -112,18 +118,22 @@ class AgentFuture(modict):
112
118
  return message.get("content", "")
113
119
  return result
114
120
 
121
+ def is_turn_outcome(self, result):
122
+ return isinstance(result, dict) and result.get("type") == TURN_OUTCOME_TYPE
123
+
124
+ def apply_turn_outcome(self, outcome):
125
+ if self.is_turn_outcome(outcome):
126
+ self.turn.completed = bool(outcome.get("completed"))
127
+ self.turn.reason = outcome.get("reason", "")
128
+ return outcome.get("result")
129
+ self.turn.completed = True
130
+ self.turn.reason = TURN_REASON_COMPLETED
131
+ return outcome
132
+
115
133
  def set_result(self, result):
116
134
  from .utils import timestamp
117
135
 
118
- outcome = result if isinstance(result, dict) and result.get("type") == "turn_outcome" else None
119
- if outcome:
120
- self.turn.completed = outcome.completed
121
- self.turn.reason = outcome.reason
122
- result = outcome.get("result")
123
- else:
124
- self.turn.completed = True
125
- self.turn.reason = "completed"
126
-
136
+ result = self.apply_turn_outcome(result)
127
137
  self.status = "done"
128
138
  self.turn.status = "done"
129
139
  if self.turn.completed:
@@ -143,7 +153,7 @@ class AgentFuture(modict):
143
153
  self.turn.error = str(error)
144
154
  self.turn.status = "error"
145
155
  self.turn.completed = True
146
- self.turn.reason = "error"
156
+ self.turn.reason = TURN_REASON_ERROR
147
157
  self.turn.completed_at = timestamp()
148
158
  self.finish_messages()
149
159
  self.set_attr("error_obj", error)
@@ -254,7 +264,7 @@ class CommandManager:
254
264
  self.agent.emit(AssistantTurnStartEvent, prompt=prompt)
255
265
  try:
256
266
  result = self.run(prompt)
257
- self.agent.emit(AssistantTurnEndEvent, prompt=prompt, reason="completed")
267
+ self.agent.emit(AssistantTurnEndEvent, prompt=prompt, reason=TURN_REASON_COMPLETED)
258
268
  return result
259
269
  except Exception as exc:
260
270
  self.agent.emit(AssistantTurnErrorEvent, prompt=prompt, error=str(exc))
@@ -82,17 +82,9 @@ class ContextManager:
82
82
  msg for msg in self.agent.session.context_messages()
83
83
  if self.is_message_visible_in_context(msg)
84
84
  )
85
- images = [] if not self.agent.config.get("vision_enabled", False) else [
86
- msg for msg in session_messages if isinstance(msg, ImageMessage)
87
- ][-max_images:]
85
+ images = self.select_context_images(session_messages, max_images=max_images)
88
86
  others = [msg for msg in session_messages if not isinstance(msg, ImageMessage)]
89
-
90
- truncated_others = []
91
- for msg in others:
92
- msg_copy = msg.copy()
93
- if msg_copy.get("content") and isinstance(msg_copy.content, str):
94
- msg_copy.content = truncate(msg_copy.content, max_tokens=max_input_tokens, model=self.agent.config.model)
95
- truncated_others.append(msg_copy)
87
+ truncated_others = self.truncate_context_messages(others, max_input_tokens=max_input_tokens)
96
88
 
97
89
  current_count = self.response_input_token_count(system + images + providers_msgs, response_tools=response_tools)
98
90
  history_start = len(truncated_others)
@@ -138,6 +130,24 @@ class ContextManager:
138
130
  hook = self.agent.run_hook("context.build.after", messages=formatted_context)
139
131
  return hook.payload.messages
140
132
 
133
+ def select_context_images(self, messages, max_images=10):
134
+ if not self.agent.config.get("vision_enabled", False):
135
+ return []
136
+ return [msg for msg in messages if isinstance(msg, ImageMessage)][-max_images:]
137
+
138
+ def truncate_context_messages(self, messages, max_input_tokens):
139
+ truncated = []
140
+ for msg in messages:
141
+ msg_copy = msg.copy()
142
+ if msg_copy.get("content") and isinstance(msg_copy.content, str):
143
+ msg_copy.content = truncate(
144
+ msg_copy.content,
145
+ max_tokens=max_input_tokens,
146
+ model=self.agent.config.model,
147
+ )
148
+ truncated.append(msg_copy)
149
+ return truncated
150
+
141
151
  def prune_orphaned_session_tool_outputs(self):
142
152
  if not self.agent.session:
143
153
  return []
@@ -1,13 +1,5 @@
1
1
  from modict import modict
2
- from .utils import snake_case_type, timestamp
3
-
4
-
5
- def type_hierarchy(cls, root):
6
- return [
7
- snake_case_type(base)
8
- for base in reversed(cls.mro())
9
- if isinstance(base, type) and issubclass(base, root) and base is not object
10
- ]
2
+ from .utils import timestamp, type_hierarchy
11
3
 
12
4
 
13
5
  class AssistantInterrupted(Exception):
@@ -4,7 +4,17 @@ from threading import Event as ThreadEvent, Lock, Thread as BackgroundThread, ge
4
4
 
5
5
  from modict import modict
6
6
 
7
- from .command import AgentCommand, AgentFuture, AgentTurn
7
+ from .command import (
8
+ TURN_OUTCOME_TYPE,
9
+ TURN_REASON_COMPLETED,
10
+ TURN_REASON_ERROR,
11
+ TURN_REASON_INTERRUPTED,
12
+ TURN_REASON_REQUIRES_NEXT_STEP,
13
+ TURN_REASON_RESULT_SET,
14
+ AgentCommand,
15
+ AgentFuture,
16
+ AgentTurn,
17
+ )
8
18
  from .event import (
9
19
  AssistantInterrupted,
10
20
  AssistantStepEndEvent,
@@ -76,30 +86,31 @@ class MainLoopManager:
76
86
 
77
87
  def submit(self, command, **payload):
78
88
  self.start()
79
- if isinstance(command, AgentCommand):
80
- agent_command = command
81
- else:
82
- agent_command = AgentCommand(name=command, payload=payload)
83
- message_start = len(self.agent.session.messages) if getattr(self.agent, "session", None) is not None else 0
84
- future = AgentFuture(command_id=agent_command.id, command=agent_command.name)
89
+ agent_command = command if isinstance(command, AgentCommand) else AgentCommand(name=command, payload=payload)
85
90
  turn = payload.pop("turn", None)
86
- future.turn = turn or AgentTurn(
87
- id=future.id,
88
- command_id=agent_command.id,
89
- command=agent_command.name,
90
- session_id=getattr(self.agent, "current_session_id", ""),
91
- message_start=message_start,
92
- message_end=message_start,
93
- )
94
- if turn is not None:
95
- future.id = turn.id
96
- future.turn.status = "pending"
97
- future.turn.command_id = agent_command.id
91
+ future = AgentFuture(command_id=agent_command.id, command=agent_command.name)
92
+ future.turn = self.prepare_turn(future, agent_command, turn=turn)
98
93
  future.set_manager(self)
99
94
  self.futures[future.id] = future
100
95
  self.command_queue.put((agent_command, future))
101
96
  return future
102
97
 
98
+ def prepare_turn(self, future, command, turn=None):
99
+ if turn is None:
100
+ message_start = len(self.agent.session.messages) if getattr(self.agent, "session", None) is not None else 0
101
+ return AgentTurn(
102
+ id=future.id,
103
+ command_id=command.id,
104
+ command=command.name,
105
+ session_id=getattr(self.agent, "current_session_id", ""),
106
+ message_start=message_start,
107
+ message_end=message_start,
108
+ )
109
+ future.id = turn.id
110
+ turn.status = "pending"
111
+ turn.command_id = command.id
112
+ return turn
113
+
103
114
  def start_turn(self):
104
115
  if self.busy:
105
116
  return None
@@ -270,7 +281,7 @@ class MainLoopManager:
270
281
  return self.process(resume=resume)
271
282
 
272
283
  def turn_outcome(self, completed, reason, result=None):
273
- return modict(type="turn_outcome", completed=completed, reason=reason, result=result)
284
+ return modict(type=TURN_OUTCOME_TYPE, completed=completed, reason=reason, result=result)
274
285
 
275
286
  def finish_turn(self, reason, message=None, result=None):
276
287
  self.agent.emit(AssistantTurnEndEvent, prompt=None, message=message, reason=reason)
@@ -311,19 +322,19 @@ class MainLoopManager:
311
322
  )
312
323
 
313
324
  if self.turn_result_set:
314
- return self.finish_turn("result_set", message=turn_message, result=self.turn_result)
325
+ return self.finish_turn(TURN_REASON_RESULT_SET, message=turn_message, result=self.turn_result)
315
326
  if self.require_next_step and not self.stop_agentic_loop:
316
327
  if self.agent.config.auto_proceed:
317
328
  continue
318
- return self.turn_outcome(completed=False, reason="requires_next_step")
319
- return self.finish_turn("completed", message=turn_message)
329
+ return self.turn_outcome(completed=False, reason=TURN_REASON_REQUIRES_NEXT_STEP)
330
+ return self.finish_turn(TURN_REASON_COMPLETED, message=turn_message)
320
331
  except AssistantInterrupted:
321
332
  self.dump_pending()
322
333
  self.agent.sessions_manager.append_message(InterruptMessage())
323
- return self.finish_turn("interrupted", message=turn_message)
334
+ return self.finish_turn(TURN_REASON_INTERRUPTED, message=turn_message)
324
335
  except Exception as exc:
325
336
  self.agent.emit(AssistantTurnErrorEvent, prompt=None, message=turn_message, error=str(exc))
326
- self.agent.emit(AssistantTurnEndEvent, prompt=None, message=turn_message, reason="error")
337
+ self.agent.emit(AssistantTurnEndEvent, prompt=None, message=turn_message, reason=TURN_REASON_ERROR)
327
338
  raise
328
339
 
329
340
  def call_tools(self, message):
@@ -1,5 +1,5 @@
1
1
  from modict import modict
2
- from .utils import short_id, snake_case_type, timestamp, format
2
+ from .utils import short_id, snake_case_type, timestamp, format, type_hierarchy
3
3
  import base64
4
4
  import hashlib
5
5
  import json
@@ -19,14 +19,6 @@ class MessageChunk(modict):
19
19
  output_items = None
20
20
 
21
21
 
22
- def type_hierarchy(cls, root):
23
- return [
24
- snake_case_type(base)
25
- for base in reversed(cls.mro())
26
- if isinstance(base, type) and issubclass(base, root) and base is not object
27
- ]
28
-
29
-
30
22
  class Message(modict):
31
23
  """Base persisted message.
32
24
 
@@ -1,6 +1,7 @@
1
1
  import json
2
2
 
3
3
  from ..agent import Agent
4
+ from ..version import __version__
4
5
  from ..agent_runtime import InProcessAgentRuntime, ProcessAgentRuntime
5
6
  from .core import AgentServer, create_server_agent, remembered_server_session
6
7
 
@@ -28,7 +29,7 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
28
29
  finally:
29
30
  service.stop(timeout=1.0)
30
31
 
31
- app = FastAPI(title="Codex Agent", version="0.1.20", lifespan=lifespan)
32
+ app = FastAPI(title="Codex Agent", version=__version__, lifespan=lifespan)
32
33
  app.state.agent_service = service
33
34
 
34
35
  class TurnRequest(BaseModel):