codex-agent-framework 0.1.20__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.20 → codex_agent_framework-0.1.22}/CHANGELOG.md +40 -0
  2. {codex_agent_framework-0.1.20/codex_agent_framework.egg-info → codex_agent_framework-0.1.22}/PKG-INFO +23 -8
  3. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/README.md +22 -7
  4. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/__init__.py +6 -1
  5. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/agent.py +29 -18
  6. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/agent_runtime.py +58 -45
  7. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/memory/__init__.py +2 -1
  8. codex_agent_framework-0.1.22/codex_agent/builtin_plugins/subagents/__init__.py +220 -0
  9. codex_agent_framework-0.1.22/codex_agent/builtin_plugins/subagents/profiles/explorer.json +12 -0
  10. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/client.py +21 -13
  11. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/command.py +108 -11
  12. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/config.py +3 -2
  13. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/context.py +20 -10
  14. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/event.py +49 -10
  15. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/mainloop.py +87 -19
  16. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/message.py +34 -6
  17. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/plugin.py +12 -3
  18. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/prompts/system_prompt.txt +11 -8
  19. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/provider.py +1 -1
  20. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/scheduler.py +2 -2
  21. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/server/app.py +2 -1
  22. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/server/core.py +96 -59
  23. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/sessions.py +312 -32
  24. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/stream_utils.py +63 -37
  25. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/tool.py +37 -4
  26. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/tui/chat.py +76 -9
  27. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/utils.py +9 -0
  28. codex_agent_framework-0.1.22/codex_agent/version.py +1 -0
  29. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22/codex_agent_framework.egg-info}/PKG-INFO +23 -8
  30. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent_framework.egg-info/SOURCES.txt +3 -0
  31. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/pyproject.toml +2 -2
  32. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_agent.py +726 -65
  33. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_chat.py +72 -1
  34. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_client.py +2 -2
  35. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_events.py +18 -0
  36. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_memory.py +10 -6
  37. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_messages.py +13 -1
  38. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_scheduler.py +5 -4
  39. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_server.py +79 -13
  40. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/LICENSE +0 -0
  41. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/MANIFEST.in +0 -0
  42. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/__main__.py +0 -0
  43. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/ai.py +0 -0
  44. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/browser.py +0 -0
  45. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_commands.py +0 -0
  46. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/__init__.py +0 -0
  47. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  48. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  49. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  50. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/content/system.py +0 -0
  51. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/content/vision.py +0 -0
  52. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  53. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  54. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  55. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/files/tools.py +0 -0
  56. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  57. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  58. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  59. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  60. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/cli/__init__.py +0 -0
  61. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/cli/headless.py +0 -0
  62. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/cli/main.py +0 -0
  63. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/cli/output.py +0 -0
  64. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/cli/root.py +0 -0
  65. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/cli/runner.py +0 -0
  66. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/desktop.py +0 -0
  67. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/get_text/__init__.py +0 -0
  68. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/get_text/default_gitignore +0 -0
  69. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/get_text/get_text.py +0 -0
  70. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/get_text/simpler_get_text.py +0 -0
  71. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/hooks.py +0 -0
  72. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/image.py +0 -0
  73. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/latex.py +0 -0
  74. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
  75. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/runtime.py +0 -0
  76. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  77. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/server/__init__.py +0 -0
  78. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/service.py +0 -0
  79. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/status.py +0 -0
  80. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/tray.py +0 -0
  81. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/tui/__init__.py +0 -0
  82. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/tui/lifecycle.py +0 -0
  83. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/voice.py +0 -0
  84. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent/worker.py +0 -0
  85. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  86. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  87. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent_framework.egg-info/requires.txt +0 -0
  88. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/codex_agent_framework.egg-info/top_level.txt +0 -0
  89. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/dependencies.txt +0 -0
  90. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/setup.cfg +0 -0
  91. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_agent_runtime.py +0 -0
  92. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_ai.py +0 -0
  93. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_browser.py +0 -0
  94. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_builtin_config.py +0 -0
  95. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_cli.py +0 -0
  96. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_cli_headless.py +0 -0
  97. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_cli_root.py +0 -0
  98. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_get_text_browser.py +0 -0
  99. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_hooks.py +0 -0
  100. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_image_message.py +0 -0
  101. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_local_desktop.py +0 -0
  102. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_planner.py +0 -0
  103. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_service.py +0 -0
  104. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_tray.py +0 -0
  105. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_tui.py +0 -0
  106. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_utils.py +0 -0
  107. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.22}/tests/test_worker.py +0 -0
@@ -4,6 +4,46 @@ 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
+
24
+ ## [0.1.21] - 2026-05-10
25
+ ### Added
26
+ - Add the structured `AgentTurn` API: `agent(...)` and `AgentFuture.wait()` now return a turn object with `result`, `completed`, `reason`, `messages`, and `events`.
27
+ - Add resumable non-terminal turns for `auto_proceed=False`; turns that need another model step pause with `reason="requires_next_step"` and continue through `agent.resume_turn(turn)`.
28
+ - Add `agent.set_turn_result(data)` for tools/plugins that need to explicitly finish the current turn with a structured result.
29
+ - Add the built-in `subagents` plugin with profile-based child agents, compact parent results, child-local `set_result`, `SubagentEvent` relay, and a packaged read-only `explorer` profile.
30
+ - Add hierarchical `types` to messages and events while keeping `type` as the concrete computed type.
31
+ - Add configurable `exclude_tools` matching by public or canonical plugin tool names.
32
+
33
+ ### Changed
34
+ - Treat scheduled and post-restart wakeups as internal `DeveloperMessage` entries instead of user-authored messages.
35
+ - Make `AssistantTurnEndEvent` expose a terminal `reason` rather than carrying a raw result payload.
36
+ - Capture turn-local events and messages on `AgentTurn` so embedded callers can inspect what happened without replaying the whole session.
37
+ - Include packaged subagent profile JSON files in distributions.
38
+ - Refresh the packaged default system prompt around agent identity, clarification, durable fixes, and concise final reporting.
39
+
40
+ ### Fixed
41
+ - Preserve coherent turn state across suspended/resumed turns, including stable turn IDs, non-terminal suspension without a turn-end event, and terminal completion on resume.
42
+
43
+ ### Tests
44
+ - Validate the full suite at 483 passing tests.
45
+ - Validate post-restart wakeups after a real server restart and smoke-test `auto_proceed=False` `AgentTurn` suspension/resume behavior.
46
+
7
47
  ## [0.1.20] - 2026-05-09
8
48
  ### Added
9
49
  - Make runtime plugins the single user-facing extension unit for tools, providers, commands, prompt sections, hooks, and events.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.20
3
+ Version: 0.1.22
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -286,10 +286,17 @@ agent = Agent(
286
286
  voice_enabled=False,
287
287
  )
288
288
 
289
- answer = agent("Summarize this repository in three practical bullet points.")
290
- print(answer)
289
+ turn = agent("Summarize this repository in three practical bullet points.")
290
+
291
+ print("Completed:", turn.completed)
292
+ print("Reason:", turn.reason)
293
+ print("Result:", turn.result)
294
+ for message in turn.messages:
295
+ print(message.as_string())
291
296
  ```
292
297
 
298
+ `agent(...)` returns an `AgentTurn`, not a raw string. `turn.result` is the useful final result, while `turn.messages` and `turn.events` contain the messages and events produced by that command. If `auto_proceed=False`, a tool-using turn can pause with `completed=False` and `reason="requires_next_step"`; call `agent.resume_turn(turn)` to continue the same turn object until it reaches a terminal reason such as `completed`, `result_set`, `interrupted`, or `error`.
299
+
293
300
  Resume the latest session interactively:
294
301
 
295
302
  ```python
@@ -398,7 +405,7 @@ def repo():
398
405
 
399
406
  agent = Agent(session="latest", voice_enabled=False)
400
407
  agent.add_command(repo)
401
- print(agent("/repo"))
408
+ print(agent("/repo").result)
402
409
  ```
403
410
 
404
411
  ### Add stateful plugins
@@ -593,8 +600,11 @@ The default agent exposes local tools and plugin tools to the model. Plugin tool
593
600
  | Scheduler plugin | `scheduler_schedule`, `scheduler_cancel`, `scheduler_list`, `scheduler_restart_and_wakeup` | Future turns, periodic wakeups, post-restart continuation. |
594
601
  | Browser plugin | `browser_open`, `browser_goto`, `browser_click`, `browser_fill`, `browser_press`, ... | Persistent Playwright/Chromium automation with active-tab context. |
595
602
  | Desktop plugin | `desktop_start_session`, `desktop_run_commands`, `desktop_stop_session` | Screenshot-backed Linux desktop automation. |
603
+ | Subagents plugin | `subagents_run` | Run an ephemeral child agent from a predefined profile and return a compact structured result. |
596
604
 
597
- Built-ins are intentionally packaged as plugins. Current built-in plugin modules include `files`, `content`, `bash`, `python`, `interface`, `environment`, `memory`, `planner`, `scheduler`, `browser`, and `desktop`.
605
+ Built-ins are intentionally packaged as plugins. Current built-in plugin modules include `files`, `content`, `bash`, `python`, `interface`, `environment`, `memory`, `planner`, `scheduler`, `browser`, `desktop`, and `subagents`.
606
+
607
+ The `subagents` plugin ships profile JSON files inside the package, starting with `explorer`, a read-only codebase inspection agent. Parent agents choose an existing profile and task; child agents receive their own config, tools, prompt, and temporary session instead of inheriting the parent's runtime implicitly.
598
608
 
599
609
  Built-in plugins are configurable. `None` means load all plugins, an empty list means load none, and an explicit list selects specific plugin module names:
600
610
 
@@ -703,8 +713,11 @@ Useful exported events include:
703
713
  - `ToolCallStartEvent` / `ToolCallDoneEvent`
704
714
  - `SessionLoadedEvent` / `SessionDeletedEvent`
705
715
  - `AssistantInterruptedEvent`
716
+ - `SubagentEvent`
706
717
  - `AudioPlaybackEvent`
707
718
 
719
+ Messages and events expose hierarchical `types` as well as the final computed `type`, so listeners can subscribe to broad classes such as all events or to precise concrete event types.
720
+
708
721
  Plugins can also register event and hook handlers with decorators:
709
722
 
710
723
  ```python
@@ -833,13 +846,13 @@ The tests isolate `AGENT_RUNTIME_DIR` automatically, so they should not create o
833
846
  The current tree validates at:
834
847
 
835
848
  ```text
836
- 456 passed
849
+ 489 passed
837
850
  ```
838
851
 
839
- The published `0.1.20` release was validated at:
852
+ The published `0.1.22` release was validated at:
840
853
 
841
854
  ```text
842
- 456 passed
855
+ 489 passed
843
856
  ```
844
857
 
845
858
  ## Packaging
@@ -857,6 +870,8 @@ The distribution includes prompt text files, `codex_agent/get_text/default_gitig
857
870
 
858
871
  ## Recent changes
859
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.
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.
860
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.
861
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.
862
877
  - `0.1.18`: make `start server/tray` detach by default, move durable memory fully into the memory plugin, keep wakeups as a core runtime primitive, expose RAG memory timestamps/sources, and move browser/desktop backend controllers into core modules with plugins as facades.
@@ -237,10 +237,17 @@ agent = Agent(
237
237
  voice_enabled=False,
238
238
  )
239
239
 
240
- answer = agent("Summarize this repository in three practical bullet points.")
241
- print(answer)
240
+ turn = agent("Summarize this repository in three practical bullet points.")
241
+
242
+ print("Completed:", turn.completed)
243
+ print("Reason:", turn.reason)
244
+ print("Result:", turn.result)
245
+ for message in turn.messages:
246
+ print(message.as_string())
242
247
  ```
243
248
 
249
+ `agent(...)` returns an `AgentTurn`, not a raw string. `turn.result` is the useful final result, while `turn.messages` and `turn.events` contain the messages and events produced by that command. If `auto_proceed=False`, a tool-using turn can pause with `completed=False` and `reason="requires_next_step"`; call `agent.resume_turn(turn)` to continue the same turn object until it reaches a terminal reason such as `completed`, `result_set`, `interrupted`, or `error`.
250
+
244
251
  Resume the latest session interactively:
245
252
 
246
253
  ```python
@@ -349,7 +356,7 @@ def repo():
349
356
 
350
357
  agent = Agent(session="latest", voice_enabled=False)
351
358
  agent.add_command(repo)
352
- print(agent("/repo"))
359
+ print(agent("/repo").result)
353
360
  ```
354
361
 
355
362
  ### Add stateful plugins
@@ -544,8 +551,11 @@ The default agent exposes local tools and plugin tools to the model. Plugin tool
544
551
  | Scheduler plugin | `scheduler_schedule`, `scheduler_cancel`, `scheduler_list`, `scheduler_restart_and_wakeup` | Future turns, periodic wakeups, post-restart continuation. |
545
552
  | Browser plugin | `browser_open`, `browser_goto`, `browser_click`, `browser_fill`, `browser_press`, ... | Persistent Playwright/Chromium automation with active-tab context. |
546
553
  | Desktop plugin | `desktop_start_session`, `desktop_run_commands`, `desktop_stop_session` | Screenshot-backed Linux desktop automation. |
554
+ | Subagents plugin | `subagents_run` | Run an ephemeral child agent from a predefined profile and return a compact structured result. |
547
555
 
548
- Built-ins are intentionally packaged as plugins. Current built-in plugin modules include `files`, `content`, `bash`, `python`, `interface`, `environment`, `memory`, `planner`, `scheduler`, `browser`, and `desktop`.
556
+ Built-ins are intentionally packaged as plugins. Current built-in plugin modules include `files`, `content`, `bash`, `python`, `interface`, `environment`, `memory`, `planner`, `scheduler`, `browser`, `desktop`, and `subagents`.
557
+
558
+ The `subagents` plugin ships profile JSON files inside the package, starting with `explorer`, a read-only codebase inspection agent. Parent agents choose an existing profile and task; child agents receive their own config, tools, prompt, and temporary session instead of inheriting the parent's runtime implicitly.
549
559
 
550
560
  Built-in plugins are configurable. `None` means load all plugins, an empty list means load none, and an explicit list selects specific plugin module names:
551
561
 
@@ -654,8 +664,11 @@ Useful exported events include:
654
664
  - `ToolCallStartEvent` / `ToolCallDoneEvent`
655
665
  - `SessionLoadedEvent` / `SessionDeletedEvent`
656
666
  - `AssistantInterruptedEvent`
667
+ - `SubagentEvent`
657
668
  - `AudioPlaybackEvent`
658
669
 
670
+ Messages and events expose hierarchical `types` as well as the final computed `type`, so listeners can subscribe to broad classes such as all events or to precise concrete event types.
671
+
659
672
  Plugins can also register event and hook handlers with decorators:
660
673
 
661
674
  ```python
@@ -784,13 +797,13 @@ The tests isolate `AGENT_RUNTIME_DIR` automatically, so they should not create o
784
797
  The current tree validates at:
785
798
 
786
799
  ```text
787
- 456 passed
800
+ 489 passed
788
801
  ```
789
802
 
790
- The published `0.1.20` release was validated at:
803
+ The published `0.1.22` release was validated at:
791
804
 
792
805
  ```text
793
- 456 passed
806
+ 489 passed
794
807
  ```
795
808
 
796
809
  ## Packaging
@@ -808,6 +821,8 @@ The distribution includes prompt text files, `codex_agent/get_text/default_gitig
808
821
 
809
822
  ## Recent changes
810
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.
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.
811
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.
812
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.
813
828
  - `0.1.18`: make `start server/tray` detach by default, move durable memory fully into the memory plugin, keep wakeups as a core runtime primitive, expose RAG memory timestamps/sources, and move browser/desktop backend controllers into core modules with plugins as facades.
@@ -27,6 +27,7 @@ from .event import (
27
27
  ResponseToolCallsDeltaEvent,
28
28
  ServerToolCallEvent,
29
29
  SessionDeletedEvent,
30
+ SubagentEvent,
30
31
  SessionLoadedEvent,
31
32
  ToolCallDoneEvent,
32
33
  ToolCallStartEvent,
@@ -48,13 +49,14 @@ from .message import (
48
49
  from .image import ImageMessage
49
50
  from .mainloop import MainLoopManager
50
51
  from .plugin import Plugin, PluginsManager, event_handler, hook_handler
51
- from .command import AgentCommand, AgentFuture, CommandManager, command
52
+ from .command import AgentCommand, AgentFuture, AgentTurn, CommandManager, command
52
53
  from .provider import ProvidersManager, provider
53
54
  from .runtime import RuntimeManager
54
55
  from .sessions import AgentSession, SessionsManager
55
56
  from .status import StatusManager
56
57
  from .tool import ImageGenerationTool, ServerTool, Tool, ToolsManager, WebSearchTool, get_agent, get_current_call_id, tool
57
58
  from .worker import SummarizerWorker, TurnSummaryWorker, Worker
59
+ from .version import __version__
58
60
 
59
61
  __all__ = [
60
62
  "Agent",
@@ -87,6 +89,7 @@ __all__ = [
87
89
  "ResponseToolCallsDeltaEvent",
88
90
  "ServerToolCallEvent",
89
91
  "SessionDeletedEvent",
92
+ "SubagentEvent",
90
93
  "SessionLoadedEvent",
91
94
  "ToolCallDoneEvent",
92
95
  "ToolCallStartEvent",
@@ -111,6 +114,7 @@ __all__ = [
111
114
  "ProvidersManager",
112
115
  "AgentCommand",
113
116
  "AgentFuture",
117
+ "AgentTurn",
114
118
  "CommandManager",
115
119
  "command",
116
120
  "provider",
@@ -129,4 +133,5 @@ __all__ = [
129
133
  "SummarizerWorker",
130
134
  "TurnSummaryWorker",
131
135
  "Worker",
136
+ "__version__",
132
137
  ]
@@ -14,7 +14,7 @@ from .scheduler import AgentScheduler
14
14
  from .sessions import SessionsManager
15
15
  from .status import StatusManager
16
16
  from .tool import ImageGenerationTool, ToolsManager, reset_current_agent, set_current_agent
17
- from .ai import AIBadRequestError, AIClient
17
+ from .ai import AIClient
18
18
  from .voice import VoiceProcessor
19
19
  from .latex import LaTeXProcessor
20
20
  from .event import AssistantInterruptRequestedEvent, AssistantInterrupted, AssistantInterruptedEvent, AudioPlaybackEvent, EventBus
@@ -71,6 +71,7 @@ class Agent:
71
71
  self.plugins_manager.init_runtime()
72
72
  self.command_manager.init_runtime()
73
73
  self.tools_manager.init_server_tools()
74
+ self.tools_manager.apply_config_exclusions()
74
75
  self.start_new_session() if session == "new" else self.load_session(session)
75
76
 
76
77
  def on(self, event_type, handler=None):
@@ -82,7 +83,11 @@ class Agent:
82
83
  return self.events.on(event_type, handler)
83
84
 
84
85
  def emit(self, event_type, **data):
85
- return self.events.emit(event_type, **data)
86
+ event = self.events.emit(event_type, **data)
87
+ mainloop_manager = getattr(self, "mainloop_manager", None)
88
+ if mainloop_manager is not None:
89
+ mainloop_manager.record_event(event)
90
+ return event
86
91
 
87
92
  @property
88
93
  def busy(self):
@@ -104,28 +109,35 @@ class Agent:
104
109
  def start_turn(self):
105
110
  return self.mainloop_manager.start_turn()
106
111
 
112
+ def resume_turn(self, turn):
113
+ future = self.mainloop_manager.resume_turn(turn)
114
+ return None if future is None else future.wait()
115
+
107
116
  def submit_command(self, prompt):
108
117
  return self.command_manager.submit(prompt)
109
118
 
110
119
  def submit_message(self, message):
111
120
  return self.mainloop_manager.submit_message(message)
112
121
 
122
+ def set_turn_result(self, data):
123
+ return self.mainloop_manager.set_turn_result(data)
124
+
113
125
  def list_wakeups(self, include_done=True):
114
126
  return self.scheduler.list(include_done=include_done)
115
127
 
116
128
  def schedule_wakeup(self, **kwargs):
117
129
  if self.mainloop_manager.should_queue_state_command():
118
- return self.submit("schedule_wakeup", **kwargs).wait()
130
+ return self.submit("schedule_wakeup", **kwargs).wait().result
119
131
  return self.scheduler.schedule(**kwargs)
120
132
 
121
133
  def cancel_wakeup(self, job_id):
122
134
  if self.mainloop_manager.should_queue_state_command():
123
- return self.submit("cancel_wakeup", job_id=job_id).wait()
135
+ return self.submit("cancel_wakeup", job_id=job_id).wait().result
124
136
  return self.scheduler.cancel(job_id)
125
137
 
126
138
  def delete_wakeup(self, job_id):
127
139
  if self.mainloop_manager.should_queue_state_command():
128
- return self.submit("delete_wakeup", job_id=job_id).wait()
140
+ return self.submit("delete_wakeup", job_id=job_id).wait().result
129
141
  return self.scheduler.delete(job_id)
130
142
 
131
143
  def interrupt(self, reason="user"):
@@ -256,7 +268,7 @@ class Agent:
256
268
  return decorator
257
269
 
258
270
  if hasattr(self, "mainloop_manager") and self.mainloop_manager.should_queue_state_command():
259
- return self.submit("add_hook", key=key, func=func).wait()
271
+ return self.submit("add_hook", key=key, func=func).wait().result
260
272
 
261
273
  return self.hooks.add(key, func)
262
274
 
@@ -327,6 +339,10 @@ class Agent:
327
339
  Returns:
328
340
  Message: The aggregated assistant message with content and/or tool calls.
329
341
  """
342
+ if self.session and not self.session.is_sane():
343
+ repair = self.session.repair()
344
+ if repair.repaired:
345
+ self.status_manager.invalidate_context_cache()
330
346
  messages = self.context_manager.get_context()
331
347
  if self.context_manager.should_auto_compact(messages):
332
348
  self.compact_session(
@@ -335,21 +351,16 @@ class Agent:
335
351
  )
336
352
  messages = self.context_manager.get_context()
337
353
 
338
- try:
339
- message=self.ai.run(
340
- messages=messages,
341
- tools=self.tools_manager.get_response_tools(),
342
- prompt_cache_key=self.current_session_id,
343
- **self.config.extract('model','reasoning_effort','verbosity','parallel_tool_calls')
344
- )
345
- except AIBadRequestError:
346
- self.sessions_manager.restore_api_success_backup()
347
- raise
354
+ message=self.ai.run(
355
+ messages=messages,
356
+ tools=self.tools_manager.get_response_tools(),
357
+ prompt_cache_key=self.current_session_id,
358
+ **self.config.extract('model','reasoning_effort','verbosity','parallel_tool_calls')
359
+ )
348
360
  self.sessions_manager.append_message(message)
349
361
  for item in message.output_items:
350
362
  if self.tools_manager.server_tool_for_item(item):
351
363
  self.sessions_manager.append_message(ServerToolResponseMessage(item=item))
352
- self.sessions_manager.save_api_success_backup()
353
364
  return message
354
365
 
355
366
  def speak(self,text,**kwargs):
@@ -431,7 +442,7 @@ class Agent:
431
442
 
432
443
  return dest_path
433
444
 
434
- def __call__(self,prompt=None)->bool:
445
+ def __call__(self, prompt=None):
435
446
  return self.mainloop_manager.call(prompt=prompt)
436
447
 
437
448
  def interact(self):
@@ -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)
@@ -429,7 +445,7 @@ class ProcessAgentRuntime(AgentRuntime):
429
445
  payload = dict(values or {})
430
446
  payload.update(kwargs)
431
447
  result = self.call("update_config", {"values": payload, "save": save}, timeout=self.command_timeout)
432
- self.cache.config = result
448
+ self.cache["config"] = result
433
449
  return result
434
450
 
435
451
  def session(self):
@@ -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):
@@ -370,6 +370,7 @@ You can manage memory explicitly with the memory tools:
370
370
  Use your memory tools judiciously and proactively to create durable records of important information that should persist beyond the current session.
371
371
  Do not over-memorize routine exchanges, transient implementation details, or information that is only useful within the current turn.
372
372
  Prefer durable facts, preferences, decisions, and important outcomes.
373
+ Memory entries should be rather short (one paragraph) and semantically well scoped for more efficient retrieval.
373
374
  Keep your memory tidy and up to date by editing or deleting outdated or irrelevant entries.
374
375
  When automatic memory archiving is enabled, completed turns are summarized into memory in the background when they contain something worth remembering.
375
376
  """
@@ -469,7 +470,7 @@ When automatic memory archiving is enabled, completed turns are summarized into
469
470
 
470
471
  @event_handler(AssistantTurnEndEvent)
471
472
  def archive_completed_turn(self, event):
472
- if event.get("result") is not True:
473
+ if event.get("reason") != "completed":
473
474
  return None
474
475
  if not self.config.auto_archive:
475
476
  return None