codex-agent-framework 0.1.20__tar.gz → 0.1.21__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 (106) hide show
  1. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/CHANGELOG.md +23 -0
  2. {codex_agent_framework-0.1.20/codex_agent_framework.egg-info → codex_agent_framework-0.1.21}/PKG-INFO +22 -8
  3. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/README.md +21 -7
  4. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/__init__.py +4 -1
  5. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/agent.py +29 -18
  6. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/agent_runtime.py +1 -1
  7. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/memory/__init__.py +2 -1
  8. codex_agent_framework-0.1.21/codex_agent/builtin_plugins/subagents/__init__.py +220 -0
  9. codex_agent_framework-0.1.21/codex_agent/builtin_plugins/subagents/profiles/explorer.json +12 -0
  10. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/client.py +16 -2
  11. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/command.py +98 -11
  12. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/config.py +3 -2
  13. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/event.py +56 -9
  14. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/mainloop.py +72 -15
  15. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/message.py +41 -5
  16. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/plugin.py +12 -3
  17. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/prompts/system_prompt.txt +11 -8
  18. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/provider.py +1 -1
  19. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/scheduler.py +2 -2
  20. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/server/core.py +2 -2
  21. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/sessions.py +307 -17
  22. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/stream_utils.py +63 -37
  23. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/tool.py +30 -4
  24. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21/codex_agent_framework.egg-info}/PKG-INFO +22 -8
  25. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent_framework.egg-info/SOURCES.txt +2 -0
  26. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/pyproject.toml +2 -2
  27. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_agent.py +726 -65
  28. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_chat.py +1 -1
  29. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_events.py +18 -0
  30. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_memory.py +10 -6
  31. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_messages.py +13 -1
  32. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_scheduler.py +5 -4
  33. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_server.py +14 -12
  34. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/LICENSE +0 -0
  35. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/MANIFEST.in +0 -0
  36. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/__main__.py +0 -0
  37. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/ai.py +0 -0
  38. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/browser.py +0 -0
  39. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_commands.py +0 -0
  40. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/__init__.py +0 -0
  41. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  42. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  43. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  44. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/content/system.py +0 -0
  45. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/content/vision.py +0 -0
  46. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  47. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  48. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  49. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/files/tools.py +0 -0
  50. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  51. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  52. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  53. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  54. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/cli/__init__.py +0 -0
  55. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/cli/headless.py +0 -0
  56. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/cli/main.py +0 -0
  57. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/cli/output.py +0 -0
  58. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/cli/root.py +0 -0
  59. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/cli/runner.py +0 -0
  60. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/context.py +0 -0
  61. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/desktop.py +0 -0
  62. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/get_text/__init__.py +0 -0
  63. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/get_text/default_gitignore +0 -0
  64. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/get_text/get_text.py +0 -0
  65. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/get_text/simpler_get_text.py +0 -0
  66. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/hooks.py +0 -0
  67. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/image.py +0 -0
  68. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/latex.py +0 -0
  69. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/prompts/image_generation_system_prompt.txt +0 -0
  70. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/runtime.py +0 -0
  71. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  72. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/server/__init__.py +0 -0
  73. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/server/app.py +0 -0
  74. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/service.py +0 -0
  75. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/status.py +0 -0
  76. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/tray.py +0 -0
  77. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/tui/__init__.py +0 -0
  78. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/tui/chat.py +0 -0
  79. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/tui/lifecycle.py +0 -0
  80. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/utils.py +0 -0
  81. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/voice.py +0 -0
  82. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent/worker.py +0 -0
  83. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  84. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  85. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent_framework.egg-info/requires.txt +0 -0
  86. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/codex_agent_framework.egg-info/top_level.txt +0 -0
  87. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/dependencies.txt +0 -0
  88. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/setup.cfg +0 -0
  89. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_agent_runtime.py +0 -0
  90. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_ai.py +0 -0
  91. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_browser.py +0 -0
  92. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_builtin_config.py +0 -0
  93. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_cli.py +0 -0
  94. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_cli_headless.py +0 -0
  95. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_cli_root.py +0 -0
  96. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_client.py +0 -0
  97. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_get_text_browser.py +0 -0
  98. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_hooks.py +0 -0
  99. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_image_message.py +0 -0
  100. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_local_desktop.py +0 -0
  101. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_planner.py +0 -0
  102. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_service.py +0 -0
  103. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_tray.py +0 -0
  104. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_tui.py +0 -0
  105. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_utils.py +0 -0
  106. {codex_agent_framework-0.1.20 → codex_agent_framework-0.1.21}/tests/test_worker.py +0 -0
@@ -4,6 +4,29 @@ 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.21] - 2026-05-10
8
+ ### Added
9
+ - Add the structured `AgentTurn` API: `agent(...)` and `AgentFuture.wait()` now return a turn object with `result`, `completed`, `reason`, `messages`, and `events`.
10
+ - 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)`.
11
+ - Add `agent.set_turn_result(data)` for tools/plugins that need to explicitly finish the current turn with a structured result.
12
+ - 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.
13
+ - Add hierarchical `types` to messages and events while keeping `type` as the concrete computed type.
14
+ - Add configurable `exclude_tools` matching by public or canonical plugin tool names.
15
+
16
+ ### Changed
17
+ - Treat scheduled and post-restart wakeups as internal `DeveloperMessage` entries instead of user-authored messages.
18
+ - Make `AssistantTurnEndEvent` expose a terminal `reason` rather than carrying a raw result payload.
19
+ - Capture turn-local events and messages on `AgentTurn` so embedded callers can inspect what happened without replaying the whole session.
20
+ - Include packaged subagent profile JSON files in distributions.
21
+ - Refresh the packaged default system prompt around agent identity, clarification, durable fixes, and concise final reporting.
22
+
23
+ ### Fixed
24
+ - 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.
25
+
26
+ ### Tests
27
+ - Validate the full suite at 483 passing tests.
28
+ - Validate post-restart wakeups after a real server restart and smoke-test `auto_proceed=False` `AgentTurn` suspension/resume behavior.
29
+
7
30
  ## [0.1.20] - 2026-05-09
8
31
  ### Added
9
32
  - 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.21
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
+ 483 passed
837
850
  ```
838
851
 
839
- The published `0.1.20` release was validated at:
852
+ The published `0.1.21` release was validated at:
840
853
 
841
854
  ```text
842
- 456 passed
855
+ 483 passed
843
856
  ```
844
857
 
845
858
  ## Packaging
@@ -857,6 +870,7 @@ The distribution includes prompt text files, `codex_agent/get_text/default_gitig
857
870
 
858
871
  ## Recent changes
859
872
 
873
+ - `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
874
  - `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
875
  - `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
876
  - `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
+ 483 passed
788
801
  ```
789
802
 
790
- The published `0.1.20` release was validated at:
803
+ The published `0.1.21` release was validated at:
791
804
 
792
805
  ```text
793
- 456 passed
806
+ 483 passed
794
807
  ```
795
808
 
796
809
  ## Packaging
@@ -808,6 +821,7 @@ The distribution includes prompt text files, `codex_agent/get_text/default_gitig
808
821
 
809
822
  ## Recent changes
810
823
 
824
+ - `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
825
  - `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
826
  - `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
827
  - `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,7 +49,7 @@ 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
@@ -87,6 +88,7 @@ __all__ = [
87
88
  "ResponseToolCallsDeltaEvent",
88
89
  "ServerToolCallEvent",
89
90
  "SessionDeletedEvent",
91
+ "SubagentEvent",
90
92
  "SessionLoadedEvent",
91
93
  "ToolCallDoneEvent",
92
94
  "ToolCallStartEvent",
@@ -111,6 +113,7 @@ __all__ = [
111
113
  "ProvidersManager",
112
114
  "AgentCommand",
113
115
  "AgentFuture",
116
+ "AgentTurn",
114
117
  "CommandManager",
115
118
  "command",
116
119
  "provider",
@@ -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):
@@ -429,7 +429,7 @@ class ProcessAgentRuntime(AgentRuntime):
429
429
  payload = dict(values or {})
430
430
  payload.update(kwargs)
431
431
  result = self.call("update_config", {"values": payload, "save": save}, timeout=self.command_timeout)
432
- self.cache.config = result
432
+ self.cache["config"] = result
433
433
  return result
434
434
 
435
435
  def session(self):
@@ -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
@@ -0,0 +1,220 @@
1
+ """Subagents plugin: delegate focused tasks to ephemeral child agents."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from modict import modict
8
+
9
+ from ...event import SubagentEvent
10
+ from ...plugin import Plugin
11
+ from ...tool import tool
12
+
13
+
14
+ class SubagentResult(modict):
15
+ _config = modict.config(require_all="never")
16
+
17
+ type = "subagent_result"
18
+ profile = ""
19
+ name = ""
20
+ task = ""
21
+ status = ""
22
+ result = None
23
+ error = None
24
+
25
+
26
+ class SubagentsPlugin(Plugin):
27
+ name = "subagents"
28
+ description = "Delegate focused tasks to ephemeral child agents with their own config and plugins."
29
+ profiles_dir = Path(__file__).parent / "profiles"
30
+ system_prompt = """
31
+ # Subagents
32
+
33
+ Use `subagents_run` to delegate a focused task to a subagent profile with its own predefined config, tools and plugins.
34
+ Choose one of the available profiles and provide a focused task. The child agent receives its own `set_result` tool to provide a structured result. The parent receives that result and should report it back clearly.
35
+ """
36
+ child_system_prompt = """
37
+ You are a focused subagent running inside a parent agent.
38
+
39
+ Fundamentals:
40
+ - Work only on the delegated task.
41
+ - Use your available tools autonomously when useful.
42
+ - You may perform multiple reasoning/tool steps before finishing.
43
+ - Keep your work scoped to your role and available capabilities.
44
+ - When the task is complete, call the local `set_result` tool with the structured result to return to the parent agent.
45
+ - Do not ask the user for follow-up; report uncertainty or missing inputs in the structured result instead.
46
+ """
47
+
48
+ def __init__(self, agent):
49
+ super().__init__(agent)
50
+ self.profiles = modict()
51
+ self.load_profiles()
52
+
53
+ def load_profiles(self):
54
+ profiles_dir = Path(self.profiles_dir)
55
+ if not profiles_dir.is_dir():
56
+ return self.profiles
57
+ for path in sorted(profiles_dir.glob("*.json")):
58
+ self.load_profile_file(path)
59
+ return self.profiles
60
+
61
+ def load_profile_file(self, path):
62
+ with open(path, encoding="utf-8") as f:
63
+ profile = json.load(f)
64
+ if not isinstance(profile, dict):
65
+ raise ValueError(f"Invalid subagent profile {path}: expected JSON object")
66
+ profile_name = profile.pop("id", None) or path.stem
67
+ self.register_profile(profile_name, **profile)
68
+ return profile_name
69
+
70
+ def register_profile(self, profile_name, **profile):
71
+ self.profiles[profile_name] = modict(profile)
72
+ return self.profiles[profile_name]
73
+
74
+ def get_system_prompt_sections(self):
75
+ if not self.profiles:
76
+ return [self.system_prompt.strip() + "\n\nNo subagent profiles are currently available."]
77
+ lines = [self.system_prompt.strip(), "", "Available subagent profiles:"]
78
+ for name, profile in self.profiles.items():
79
+ description = profile.get("description", "No description provided.")
80
+ lines.append(f"- `{name}`: {description}")
81
+ return ["\n".join(lines)]
82
+
83
+ def profile(self, name):
84
+ if not name:
85
+ raise ValueError("A subagent profile is required")
86
+ if name not in self.profiles:
87
+ raise ValueError(f"Unknown subagent profile: {name}")
88
+ return modict(self.profiles[name])
89
+
90
+ def profile_system_prompt(self, profile):
91
+ sections = [self.child_system_prompt.strip()]
92
+ structured = []
93
+ for key, title in (
94
+ ("role", "Role"),
95
+ ("goal", "Goal"),
96
+ ("means", "Means"),
97
+ ("objective", "Objective"),
98
+ ):
99
+ value = profile.get(key)
100
+ if value:
101
+ structured.append(f"## {title}\n{value}")
102
+ if structured:
103
+ sections.append("\n\n".join(structured))
104
+ if profile.get("system_prompt"):
105
+ sections.append(str(profile.system_prompt).strip())
106
+ return "\n\n".join(section for section in sections if section)
107
+
108
+ def child_config(self, profile):
109
+ data = modict(profile)
110
+ config = modict(data.pop("config", {}) or {})
111
+
112
+ for key in ("name", "builtin_plugins", "custom_plugins", "exclude_tools", "model", "reasoning_effort", "verbosity"):
113
+ if key in data:
114
+ config[key] = data[key]
115
+
116
+ config.system = self.profile_system_prompt(data)
117
+ config.setdefault("name", data.get("name") or f"{self.agent.config.name} subagent")
118
+ config.setdefault("username", self.agent.config.get("username"))
119
+ config.setdefault("userage", self.agent.config.get("userage"))
120
+ config.setdefault("openai_api_key", self.agent.config.get("openai_api_key"))
121
+ config.setdefault("voice_enabled", False)
122
+ config.setdefault("builtin_plugins", [])
123
+ config.setdefault("custom_plugins", [])
124
+ config.setdefault("exclude_tools", [])
125
+ return config
126
+
127
+ def relay_child_event(self, subagent, child, event):
128
+ wrapped = SubagentEvent(
129
+ subagent=subagent,
130
+ event=event,
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)
137
+ return event
138
+
139
+ def install_child_tools(self, child):
140
+ def set_result(data):
141
+ """
142
+ description: |
143
+ Set some data as this turn's result and finish the turn after the current step.
144
+ The result will be accessible by the caller (user or other Agent) as `AgentTurn.result`
145
+ Use this when your task is complete and you need to return structured data to the caller.
146
+ parameters:
147
+ data:
148
+ description: Structured result data to return for the current subagent turn.
149
+ required:
150
+ - data
151
+ """
152
+ child.set_turn_result(data)
153
+ return "Subagent result set."
154
+
155
+ child.add_tool(set_result, name="set_result")
156
+ return child
157
+
158
+ def cleanup_session(self, child):
159
+ session = getattr(child, "session", None)
160
+ if session is None:
161
+ return
162
+ for path in (session.session_file, session.api_success_backup_file):
163
+ if path and os.path.exists(path):
164
+ os.remove(path)
165
+
166
+ def run_subagent(self, profile, task):
167
+ from ...agent import Agent
168
+
169
+ child = None
170
+ profile_data = None
171
+ try:
172
+ profile_data = self.profile(profile)
173
+ child = Agent(session="new", **self.child_config(profile_data))
174
+ child.on("*", lambda event: self.relay_child_event(profile, child, event))
175
+ self.install_child_tools(child)
176
+ prompt = (
177
+ f"{task}\n\n"
178
+ "When the delegated task is complete, call the `set_result` tool with the structured result to return to the parent agent."
179
+ )
180
+ turn = child(prompt)
181
+ return SubagentResult(
182
+ profile=profile,
183
+ name=child.config.name,
184
+ task=task,
185
+ status=turn.status,
186
+ result=turn.result,
187
+ )
188
+ except Exception as exc:
189
+ return SubagentResult(
190
+ profile=profile,
191
+ name=(profile_data or {}).get("name", profile),
192
+ task=task,
193
+ status="error",
194
+ error=str(exc),
195
+ )
196
+ finally:
197
+ if child is not None:
198
+ child.stop(timeout=1.0)
199
+ if not (profile_data or {}).get("keep_session", False):
200
+ self.cleanup_session(child)
201
+
202
+ @tool(name="run")
203
+ def run(self, profile: str, task: str):
204
+ """
205
+ description: |
206
+ Run a focused task in an ephemeral child agent selected from predefined profiles.
207
+ The calling agent must only choose an existing profile and provide the specific task.
208
+ The child agent receives a local `set_result` tool to return structured data to the parent.
209
+ parameters:
210
+ profile:
211
+ description: Registered subagent profile to use.
212
+ type: string
213
+ task:
214
+ description: Specific task prompt to give to the selected subagent profile.
215
+ type: string
216
+ required:
217
+ - profile
218
+ - task
219
+ """
220
+ return self.run_subagent(profile=profile, task=task)
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "explorer",
3
+ "name": "Codebase explorer",
4
+ "description": "Read-only project/codebase exploration subagent for mapping structure, inspecting files, and reporting findings.",
5
+ "role": "You are a read-only project and codebase exploration subagent.",
6
+ "goal": "Understand the relevant structure, files, code paths, and implementation details needed for the delegated task, then return a clear structured report.",
7
+ "means": "Use read-only inspection tools such as file reading and content extraction. Do not modify files, run destructive actions, or change project state.",
8
+ "objective": "Call `set_result` with a structured exploration report including key findings, relevant files/symbols, confidence, and any remaining uncertainties.",
9
+ "builtin_plugins": ["files", "content", "environment"],
10
+ "exclude_tools": ["files.write", "files.edit"],
11
+ "voice_enabled": false
12
+ }
@@ -45,8 +45,22 @@ class AgentClient:
45
45
  def emit_local(self, event):
46
46
  event = modict(event)
47
47
  event_type = event.get("type")
48
- handlers = list(self._event_handlers.get(event_type, ()))
49
- handlers.extend(self._event_handlers.get("*", ()))
48
+ event_types = list(event.get("types") or [event_type])
49
+ if event_type and event_type not in event_types:
50
+ event_types.append(event_type)
51
+ handlers = []
52
+ seen = set()
53
+ for current_type in event_types:
54
+ for handler in self._event_handlers.get(current_type, ()):
55
+ if handler in seen:
56
+ continue
57
+ seen.add(handler)
58
+ handlers.append(handler)
59
+ for handler in self._event_handlers.get("*", ()):
60
+ if handler in seen:
61
+ continue
62
+ seen.add(handler)
63
+ handlers.append(handler)
50
64
  for handler in handlers:
51
65
  try:
52
66
  handler(event)