codex-agent-framework 0.1.53__tar.gz → 0.1.54__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 (153) hide show
  1. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/CHANGELOG.md +12 -1
  2. {codex_agent_framework-0.1.53/codex_agent_framework.egg-info → codex_agent_framework-0.1.54}/PKG-INFO +1 -1
  3. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/agent.py +0 -1
  4. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/agent_runtime.py +16 -6
  5. codex_agent_framework-0.1.54/codex_agent/builtin_commands.py +196 -0
  6. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/client.py +2 -2
  7. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/config.py +154 -67
  8. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/mainloop.py +4 -2
  9. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/server/app.py +2 -1
  10. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/server/core.py +5 -1
  11. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/sessions.py +37 -4
  12. codex_agent_framework-0.1.54/codex_agent/version.py +1 -0
  13. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54/codex_agent_framework.egg-info}/PKG-INFO +1 -1
  14. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/pyproject.toml +1 -1
  15. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_audio_hooks.py +7 -7
  16. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_commands.py +56 -13
  17. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_desktop.py +2 -2
  18. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_environment.py +3 -3
  19. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_observe.py +1 -1
  20. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_plugins.py +1 -1
  21. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_providers_config.py +23 -7
  22. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_runtime.py +24 -2
  23. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_tools.py +9 -7
  24. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_cli_headless.py +2 -1
  25. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_hooks.py +1 -1
  26. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_server.py +38 -3
  27. codex_agent_framework-0.1.53/codex_agent/builtin_commands.py +0 -152
  28. codex_agent_framework-0.1.53/codex_agent/version.py +0 -1
  29. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/LICENSE +0 -0
  30. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/MANIFEST.in +0 -0
  31. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/README.md +0 -0
  32. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/__init__.py +0 -0
  33. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/__main__.py +0 -0
  34. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/ai.py +0 -0
  35. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/browser.py +0 -0
  36. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/__init__.py +0 -0
  37. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  38. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
  39. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  40. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
  41. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  42. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
  43. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/content/system.py +0 -0
  44. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/content/vision.py +0 -0
  45. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/context/__init__.py +0 -0
  46. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  47. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
  48. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  49. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
  50. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  51. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
  52. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/files/tools.py +0 -0
  53. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
  54. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
  55. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
  56. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  57. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
  58. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  59. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
  60. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  61. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
  62. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  63. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
  64. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
  65. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
  66. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
  67. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
  68. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
  69. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/realtime/session.py +0 -0
  70. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  71. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
  72. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
  73. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  74. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
  75. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
  76. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
  77. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/builtin_plugins/web_search/__init__.py +0 -0
  78. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/cli/__init__.py +0 -0
  79. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/cli/headless.py +0 -0
  80. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/cli/main.py +0 -0
  81. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/cli/output.py +0 -0
  82. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/cli/root.py +0 -0
  83. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/cli/runner.py +0 -0
  84. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/command.py +0 -0
  85. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/context.py +0 -0
  86. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/desktop.py +0 -0
  87. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/event.py +0 -0
  88. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/get_text/__init__.py +0 -0
  89. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/get_text/default_gitignore +0 -0
  90. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/get_text/get_text.py +0 -0
  91. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/get_text/simpler_get_text.py +0 -0
  92. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/hooks.py +0 -0
  93. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/image.py +0 -0
  94. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/latex.py +0 -0
  95. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/message.py +0 -0
  96. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/plugin.py +0 -0
  97. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/prompts/system_prompt.txt +0 -0
  98. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/provider.py +0 -0
  99. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/runtime.py +0 -0
  100. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/scheduler.py +0 -0
  101. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  102. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/server/__init__.py +0 -0
  103. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/service.py +0 -0
  104. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/status.py +0 -0
  105. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/stream.py +0 -0
  106. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/stream_utils.py +0 -0
  107. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tool.py +0 -0
  108. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tray.py +0 -0
  109. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tui/__init__.py +0 -0
  110. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tui/app.py +0 -0
  111. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tui/chat.py +0 -0
  112. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tui/lifecycle.py +0 -0
  113. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tui/log.py +0 -0
  114. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tui/state.py +0 -0
  115. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tui/status_bar.py +0 -0
  116. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tui/style.py +0 -0
  117. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/tui/transcript.py +0 -0
  118. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/utils.py +0 -0
  119. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent/worker.py +0 -0
  120. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent_framework.egg-info/SOURCES.txt +0 -0
  121. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  122. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  123. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent_framework.egg-info/requires.txt +0 -0
  124. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/codex_agent_framework.egg-info/top_level.txt +0 -0
  125. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/dependencies.txt +0 -0
  126. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/setup.cfg +0 -0
  127. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent.py +0 -0
  128. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_context.py +0 -0
  129. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_image_generation.py +0 -0
  130. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_realtime.py +0 -0
  131. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_response.py +0 -0
  132. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_sessions.py +0 -0
  133. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_subagents.py +0 -0
  134. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_agent_turns.py +0 -0
  135. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_ai.py +0 -0
  136. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_browser.py +0 -0
  137. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_builtin_config.py +0 -0
  138. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_chat.py +0 -0
  139. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_cli.py +0 -0
  140. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_cli_root.py +0 -0
  141. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_client.py +0 -0
  142. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_events.py +0 -0
  143. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_get_text_browser.py +0 -0
  144. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_image_message.py +0 -0
  145. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_local_desktop.py +0 -0
  146. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_memory.py +0 -0
  147. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_messages.py +0 -0
  148. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_planner.py +0 -0
  149. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_scheduler.py +0 -0
  150. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_service.py +0 -0
  151. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_tui.py +0 -0
  152. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_utils.py +0 -0
  153. {codex_agent_framework-0.1.53 → codex_agent_framework-0.1.54}/tests/test_worker.py +0 -0
@@ -4,10 +4,21 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning where practical.
6
6
 
7
+ ## [0.1.54] - 2026-05-17
8
+ ### Changed
9
+ - Add a generic namespaced `/config` command surface so agent and plugin configs can be read, updated, and saved through a uniform dot-notation interface such as `/config agent.input_token_limit 200000`, `/config tts.voice nova`, `/config agent save`, and `/config save`.
10
+ - Keep config persistence explicit for SDK-created agents while still materializing `agent.config.json` for long-lived server sessions at server bootstrap.
11
+ - Simplify config loading by relying on direct `Config.load(...)` with ignored unknown extras instead of carrying legacy config shims.
12
+ - Hide empty plugin config namespaces from `/config` output so only agent config and plugins with actual exposed config values are shown.
13
+ - Align runtime/server/client paths and tests with the namespaced config model and the minimal default plugin surface.
14
+
15
+ ### Tests
16
+ - Validate with targeted config/server/runtime coverage and the full suite at 611 passed.
17
+
7
18
  ## [0.1.53] - 2026-05-16
8
19
  ### Changed
9
20
  - Default new agents to a smaller built-in plugin surface: `files`, `environment`, `memory`, `planner`, `scheduler`, and `context`.
10
- - Create `~/.codex-agent/agent.config.json` automatically on agent initialization instead of waiting for an explicit config update.
21
+ - Persist `~/.codex-agent/agent.config.json` only on explicit save/update actions instead of automatically on agent initialization.
11
22
  - Make `web_search` and `image_generation` plugin-first server tools: loading the plugin now controls whether the corresponding Responses API tool is exposed.
12
23
  - Remove the redundant `web_search_enabled` and `image_generation_enabled` config flags from the public agent config surface.
13
24
  - Update docs and tests to reflect the minimal default plugin set and plugin-driven server tool activation.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.53
3
+ Version: 0.1.54
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -54,7 +54,6 @@ class Agent:
54
54
  self.config_manager.update(kwargs, save=False)
55
55
  self.init_working_directories(root_dir=explicit_root_dir or launch_cwd, cwd=explicit_cwd)
56
56
  self.config_manager.apply_openai_capability_defaults()
57
- self.config_manager.save()
58
57
  self.events = EventBus()
59
58
  self.tools_manager = ToolsManager(self)
60
59
  self.providers_manager = ProvidersManager(self)
@@ -189,8 +189,8 @@ class InProcessAgentRuntime(AgentRuntime):
189
189
  def config(self):
190
190
  return self.agent.config
191
191
 
192
- def update_config(self, values=None, save=True, **kwargs):
193
- return self.agent.config_manager.update(values, save=save, **kwargs)
192
+ def update_config(self, values=None, save=True, namespace="agent", **kwargs):
193
+ return self.agent.config_manager.update_config(namespace, values, save=save, **kwargs)
194
194
 
195
195
  def session(self):
196
196
  return self.agent.session
@@ -588,11 +588,16 @@ class ProcessAgentRuntime(AgentRuntime):
588
588
  def config(self):
589
589
  return self._cached_call("config")
590
590
 
591
- def update_config(self, values=None, save=True, **kwargs):
591
+ def update_config(self, values=None, save=True, namespace="agent", **kwargs):
592
592
  payload = dict(values or {})
593
593
  payload.update(kwargs)
594
- result = self.call("update_config", {"values": payload, "save": save}, timeout=self.command_timeout)
595
- self.cache["config"] = result
594
+ result = self.call(
595
+ "update_config",
596
+ {"namespace": namespace, "values": payload, "save": save},
597
+ timeout=self.command_timeout,
598
+ )
599
+ if namespace == "agent":
600
+ self.cache["config"] = result
596
601
  return result
597
602
 
598
603
  def session(self):
@@ -703,6 +708,7 @@ class _RuntimeFuture:
703
708
 
704
709
  def agent_worker_main(command_queue, response_queue, event_queue, agent_kwargs):
705
710
  agent = Agent(**dict(agent_kwargs or {}))
711
+ agent.config_manager.save()
706
712
 
707
713
  def emit_event(event):
708
714
  event_queue.put(event_payload(event))
@@ -749,7 +755,11 @@ def run_worker_command(agent, name, payload):
749
755
  if name == "config":
750
756
  return agent.config
751
757
  if name == "update_config":
752
- return agent.config_manager.update(payload.get("values") or {}, save=payload.get("save", True))
758
+ return agent.config_manager.update_config(
759
+ payload.get("namespace", "agent"),
760
+ payload.get("values") or {},
761
+ save=payload.get("save", True),
762
+ )
753
763
  if name == "session":
754
764
  return agent.session
755
765
  if name == "sessions":
@@ -0,0 +1,196 @@
1
+ """Built-in slash commands."""
2
+
3
+ from modict import modict
4
+
5
+ from .command import command
6
+ from .config import parse_config_value
7
+ from .sessions import AgentSession
8
+ from .tool import get_agent
9
+
10
+
11
+ def _format_lines(value, prefix=""):
12
+ if isinstance(value, dict):
13
+ lines = []
14
+ for key, child in value.items():
15
+ full_key = f"{prefix}.{key}" if prefix else str(key)
16
+ if isinstance(child, dict):
17
+ lines.append(f"{full_key}:")
18
+ lines.extend(_format_lines(child, full_key))
19
+ else:
20
+ lines.append(f"{full_key}: {child!r}")
21
+ return lines
22
+ return [f"{prefix}: {value!r}" if prefix else repr(value)]
23
+
24
+
25
+ def format_config_output(value, prefix=""):
26
+ return "\n".join(_format_lines(value, prefix=prefix)) if isinstance(value, dict) else repr(value)
27
+
28
+
29
+ @command
30
+ def help():
31
+ agent = get_agent()
32
+ names = ", ".join(f"/{name}" for name in sorted(agent.commands))
33
+ return f"Available commands: {names}"
34
+
35
+
36
+ @command
37
+ def compact(max_compacted_turns=None):
38
+ agent = get_agent()
39
+ max_compacted_turns = None if max_compacted_turns in (None, "", "all") else int(max_compacted_turns)
40
+ message = agent.compact_session(
41
+ model=agent.config.model,
42
+ instructions=agent.config_manager.system_message().content,
43
+ max_compacted_turns=max_compacted_turns,
44
+ )
45
+ if message is None:
46
+ return "Nothing to compact."
47
+ if message.get("status") == "failed":
48
+ return f"Compaction failed: {message.error}"
49
+ suffix = "" if max_compacted_turns is None else f" from {max_compacted_turns} compacted turn(s)"
50
+ return f"Compacted {message.compacted_message_count} messages{suffix}."
51
+
52
+
53
+ def _message_count_text(count):
54
+ return f"{count} message" if count == 1 else f"{count} messages"
55
+
56
+
57
+ def _session_infos(agent):
58
+ infos = []
59
+ for index, info in enumerate(AgentSession.list_session_infos(), start=1):
60
+ infos.append(modict(
61
+ index=index,
62
+ session_id=info.session_id,
63
+ title=info.title or info.session_id,
64
+ message_count=info.message_count,
65
+ current=info.session_id == agent.current_session_id,
66
+ ))
67
+ return infos
68
+
69
+
70
+ def _format_session_list(agent):
71
+ infos = _session_infos(agent)
72
+ if not infos:
73
+ return "No saved sessions."
74
+ lines = ["Sessions:"]
75
+ for info in infos:
76
+ marker = "*" if info.current else " "
77
+ lines.append(
78
+ f"{marker} [{info.index}] {info.title} "
79
+ f"({info.session_id}, {_message_count_text(info.message_count)})"
80
+ )
81
+ return "\n".join(lines)
82
+
83
+
84
+ @command
85
+ def session(action=None, *parts):
86
+ agent = get_agent()
87
+ if action is None:
88
+ return _format_session_list(agent)
89
+
90
+ action_text = str(action).strip()
91
+ action_lower = action_text.lower()
92
+ if action_lower == "new":
93
+ created = agent.start_new_session()
94
+ return f"Created session: {created.session_id}"
95
+
96
+ if action_lower == "rename":
97
+ title = " ".join(str(part) for part in parts).strip()
98
+ if not title:
99
+ raise ValueError("/session rename requires a title")
100
+ if agent.session is None:
101
+ raise ValueError("No active session to rename")
102
+ agent.session.title = title
103
+ agent.save_session()
104
+ return f"Renamed session: {title}"
105
+
106
+ if parts:
107
+ raise ValueError("Usage: /session, /session <index>, /session new, /session rename <title>")
108
+
109
+ try:
110
+ index = int(action_text)
111
+ except ValueError as exc:
112
+ raise ValueError(f"Unknown /session action: {action_text}") from exc
113
+
114
+ infos = _session_infos(agent)
115
+ if index < 1 or index > len(infos):
116
+ raise ValueError(f"Session index out of range: {index}")
117
+ loaded = agent.load_session(infos[index - 1].session_id)
118
+ return f"Loaded session: {loaded.session_id}"
119
+
120
+
121
+ @command
122
+ def config(path=None, value=None, *extra, **values):
123
+ agent = get_agent()
124
+ manager = agent.config_manager
125
+
126
+ if values:
127
+ unknown = sorted(key for key in values if key not in agent.config)
128
+ if unknown:
129
+ raise ValueError(f"Unknown model config keys: {', '.join(unknown)}")
130
+ parsed = {key: parse_config_value(raw) for key, raw in values.items()}
131
+ manager.update_config("agent", config=parsed, save=True)
132
+ changed = ", ".join(f"{key}={agent.config.get(key)!r}" for key in parsed)
133
+ return f"Updated config: {changed}" if changed else format_config_output(manager.export_tree())
134
+
135
+ if path is None:
136
+ if value is not None or extra:
137
+ raise ValueError("/config with no path does not accept extra arguments")
138
+ return format_config_output(manager.export_tree())
139
+
140
+ if path == "save":
141
+ manager.save_all()
142
+ return "Saved all config namespaces."
143
+
144
+ if value is None and not extra:
145
+ displayed = manager.get_path(path)
146
+ return format_config_output(displayed, prefix=path)
147
+
148
+ if value == "save" and not extra:
149
+ namespace, remainder = manager.namespace_path(path)
150
+ if remainder:
151
+ raise ValueError("Save targets a config namespace, not a nested key")
152
+ manager.save_config(namespace)
153
+ return f"Saved config namespace: {namespace}"
154
+
155
+ raw_value = " ".join(str(part) for part in ((value,) + extra))
156
+ updated = manager.set_path(path, raw_value, save=False)
157
+ return f"Updated config: {path}={updated!r}"
158
+
159
+
160
+ @command
161
+ def model(value=None):
162
+ if value is None:
163
+ return f"model: {get_agent().config.model!r}"
164
+ get_agent().config_manager.set_path("agent.model", value, save=True)
165
+ return f"Updated config: model={get_agent().config.model!r}"
166
+
167
+
168
+ @command
169
+ def reasoning(value=None):
170
+ agent = get_agent()
171
+ if value is None:
172
+ return f"reasoning: {agent.config.get('reasoning')!r}"
173
+ current = dict(agent.config.get("reasoning") or {})
174
+ current["effort"] = value
175
+ current.setdefault("summary", "auto")
176
+ agent.config_manager.update_config("agent", reasoning=current, save=True)
177
+ return f"Updated config: reasoning={agent.config.get('reasoning')!r}"
178
+
179
+
180
+ @command
181
+ def verbosity(value=None):
182
+ agent = get_agent()
183
+ if value is None:
184
+ return f"text: {agent.config.get('text')!r}"
185
+ current = dict(agent.config.get("text") or {})
186
+ current["verbosity"] = value
187
+ agent.config_manager.update_config("agent", text=current, save=True)
188
+ return f"Updated config: text={agent.config.get('text')!r}"
189
+
190
+
191
+ @command
192
+ def voice(value=None):
193
+ agent = get_agent()
194
+ if "tts" not in agent.plugins:
195
+ agent.load_builtin_plugin("tts")
196
+ return agent.plugins.tts.voice(value)
@@ -327,10 +327,10 @@ class AgentClient:
327
327
  def get_config(self):
328
328
  return self.get("/config")
329
329
 
330
- def update_config(self, values=None, save=True, **kwargs):
330
+ def update_config(self, values=None, save=True, namespace="agent", **kwargs):
331
331
  payload = dict(values or {})
332
332
  payload.update(kwargs)
333
- return self.request("PATCH", "/config", json={"values": payload, "save": save})
333
+ return self.request("PATCH", "/config", json={"values": payload, "save": save, "namespace": namespace})
334
334
 
335
335
  def tools(self):
336
336
  return self.get("/tools")
@@ -1,7 +1,9 @@
1
+ import json
1
2
  import os
2
3
  import platform
3
4
 
4
5
  from modict import modict
6
+ from modict.collections_utils import get_nested
5
7
 
6
8
  from .event import RuntimeStateChangedEvent
7
9
  from .message import SystemMessage
@@ -12,8 +14,71 @@ DEFAULT_SYSTEM_PROMPT_PATH = os.path.join(os.path.dirname(__file__), "prompts",
12
14
  DEFAULT_SYSTEM_PROMPT = text_content(DEFAULT_SYSTEM_PROMPT_PATH)
13
15
 
14
16
 
15
- class AgentConfig(modict):
16
- _config = modict.config(enforce_json=True)
17
+ def parse_config_value(value):
18
+ if isinstance(value, bool):
19
+ return value
20
+ if not isinstance(value, str):
21
+ return value
22
+
23
+ lowered = value.lower()
24
+ if lowered in ("true", "yes", "on", "1"):
25
+ return True
26
+ if lowered in ("false", "no", "off", "0"):
27
+ return False
28
+ if lowered in ("none", "null"):
29
+ return None
30
+ if value.isdigit() or (value.startswith("-") and value[1:].isdigit()):
31
+ return int(value)
32
+ try:
33
+ if any(char in value for char in (".", "e", "E")):
34
+ return float(value)
35
+ except ValueError:
36
+ pass
37
+ if (value.startswith("{") and value.endswith("}")) or (value.startswith("[") and value.endswith("]")):
38
+ return json.loads(value)
39
+ return value
40
+
41
+
42
+ class Config(modict):
43
+ _config = modict.config(enforce_json=True, extra="ignore", validate_default=True)
44
+
45
+ def save(self, file_path):
46
+ target = str(file_path)
47
+ if not target:
48
+ raise ValueError(f"No file path configured for {type(self).__name__}")
49
+ parent = os.path.dirname(target)
50
+ if parent:
51
+ os.makedirs(parent, exist_ok=True)
52
+ self.dump(target, indent=2, ensure_ascii=False)
53
+ return self
54
+
55
+ def set_path(self, path, raw_value):
56
+ path = str(path or "").strip()
57
+ value = parse_config_value(raw_value)
58
+ if not path:
59
+ if not isinstance(value, dict):
60
+ raise ValueError("Root config assignment expects a mapping value")
61
+ self.clear()
62
+ self.update(value)
63
+ return self
64
+ if "." not in path:
65
+ self[path] = value
66
+ return self[path]
67
+ parent_path, _, leaf = path.rpartition(".")
68
+ parent = get_nested(self, parent_path)
69
+ if not isinstance(parent, dict):
70
+ raise TypeError(f"Config path {path!r} does not resolve to a mapping parent")
71
+ parent[leaf] = value
72
+ return get_nested(self, path)
73
+
74
+ def get_path(self, path=None, default=None):
75
+ path = str(path or "").strip()
76
+ if not path:
77
+ return self
78
+ return get_nested(self, path, default=default)
79
+
80
+
81
+ class AgentConfig(Config):
17
82
 
18
83
  model = "gpt-5.4"
19
84
  system = DEFAULT_SYSTEM_PROMPT
@@ -65,8 +130,9 @@ class AgentConfig(modict):
65
130
  )
66
131
 
67
132
 
68
- class PluginConfig(modict):
69
- _config = modict.config(enforce_json=True)
133
+
134
+ class PluginConfig(Config):
135
+ _config = modict.config(enforce_json=True, extra="allow", validate_default=True)
70
136
 
71
137
 
72
138
  def _as_name_set(value):
@@ -92,10 +158,6 @@ class ConfigManager:
92
158
  def file(self):
93
159
  return runtime_join("agent.config.json")
94
160
 
95
- @property
96
- def legacy_file(self):
97
- return runtime_join("agent_config.json")
98
-
99
161
  def config_file(self, namespace="agent", file=None):
100
162
  if file is not None:
101
163
  return str(file)
@@ -106,13 +168,22 @@ class ConfigManager:
106
168
  def save(self):
107
169
  return self.save_config("agent")
108
170
 
171
+ def save_all(self):
172
+ if self.should_queue_state_command():
173
+ return self.agent.submit("save_config", namespace=None).wait().result
174
+ saved = modict()
175
+ for namespace in list(self.configs):
176
+ saved[namespace] = self.save_config(namespace)
177
+ return saved
178
+
109
179
  def save_config(self, namespace="agent"):
110
- if namespace == "agent" and self.should_queue_state_command():
111
- return self.agent.submit("save_config").wait().result
112
- config = self.configs.get(namespace)
113
- if config is None:
114
- raise KeyError(f"Unknown config namespace: {namespace}")
180
+ if self.should_queue_state_command():
181
+ return self.agent.submit("save_config", namespace=namespace).wait().result
182
+ config = self.get_config(namespace)
115
183
  file = self.config_files.get(namespace) or self.config_file(namespace)
184
+ self.config_files[namespace] = file
185
+ if isinstance(config, Config):
186
+ return config.save(file)
116
187
  parent = os.path.dirname(file)
117
188
  if parent:
118
189
  os.makedirs(parent, exist_ok=True)
@@ -120,9 +191,8 @@ class ConfigManager:
120
191
  return config
121
192
 
122
193
  def load(self):
123
- source = self.file if os.path.isfile(self.file) else self.legacy_file
124
- if os.path.isfile(source):
125
- self.config = AgentConfig.load(source)
194
+ if os.path.isfile(self.file):
195
+ self.config = AgentConfig.load(self.file)
126
196
  self.agent.config = self.config
127
197
  self.configs.agent = self.config
128
198
  self.config_files.agent = self.file
@@ -142,38 +212,24 @@ class ConfigManager:
142
212
  return config
143
213
 
144
214
  def update(self, config=None, save=True, **kwargs):
215
+ return self.update_config("agent", config=config, save=save, **kwargs)
216
+
217
+ def update_config(self, namespace="agent", config=None, save=True, **kwargs):
145
218
  if self.should_queue_state_command():
146
219
  return self.agent.submit(
147
220
  "update_config",
221
+ namespace=namespace,
148
222
  config=config,
149
223
  save=save,
150
224
  kwargs=kwargs,
151
225
  ).wait().result
152
- data = modict(config or {})
153
- data.update(kwargs)
154
- if data:
155
- self.config.update(data)
156
- self.agent.config = self.config
157
- if save:
158
- self.save()
159
- self.emit_state_changed(
160
- reason="config_updated",
161
- namespace="config",
162
- name="agent",
163
- details=data,
164
- )
165
- return self.config
166
-
167
- def update_config(self, namespace="agent", config=None, save=True, **kwargs):
168
- if namespace == "agent":
169
- return self.update(config, save=save, **kwargs)
170
- target = self.configs.get(namespace)
171
- if target is None:
172
- raise KeyError(f"Unknown config namespace: {namespace}")
226
+ target = self.get_config(namespace)
173
227
  data = modict(config or {})
174
228
  data.update(kwargs)
175
229
  if data:
176
230
  target.update(data)
231
+ if namespace == "agent":
232
+ self.agent.config = target
177
233
  if save:
178
234
  self.save_config(namespace)
179
235
  self.emit_state_changed(
@@ -184,6 +240,67 @@ class ConfigManager:
184
240
  )
185
241
  return target
186
242
 
243
+ def get_config(self, namespace="agent"):
244
+ target = self.configs.get(str(namespace))
245
+ if target is None:
246
+ raise KeyError(f"Unknown config namespace: {namespace}")
247
+ return target
248
+
249
+ def namespace_path(self, path=None):
250
+ text = str(path or "").strip()
251
+ if not text:
252
+ return None, ""
253
+ namespace, _, remainder = text.partition(".")
254
+ if namespace not in self.configs:
255
+ raise KeyError(f"Unknown config namespace: {namespace}")
256
+ return namespace, remainder
257
+
258
+ def get_path(self, path=None, default=None):
259
+ if not path:
260
+ return modict({namespace: config for namespace, config in self.configs.items()})
261
+ namespace, remainder = self.namespace_path(path)
262
+ target = self.get_config(namespace)
263
+ return target.get_path(remainder, default=default) if isinstance(target, Config) else get_nested(target, remainder, default=default)
264
+
265
+ def set_path(self, path, raw_value, *, save=False):
266
+ namespace, remainder = self.namespace_path(path)
267
+ target = self.get_config(namespace)
268
+ if isinstance(target, Config):
269
+ result = target.set_path(remainder, raw_value)
270
+ else:
271
+ parsed = parse_config_value(raw_value)
272
+ if not remainder:
273
+ if not isinstance(parsed, dict):
274
+ raise ValueError("Root config assignment expects a mapping value")
275
+ target.clear()
276
+ target.update(parsed)
277
+ result = target
278
+ else:
279
+ parent_path, _, leaf = remainder.rpartition(".")
280
+ parent = target if not parent_path else get_nested(target, parent_path)
281
+ if not isinstance(parent, dict):
282
+ raise TypeError(f"Config path {path!r} does not resolve to a mapping parent")
283
+ parent[leaf] = parsed
284
+ result = get_nested(target, remainder)
285
+ if namespace == "agent":
286
+ self.agent.config = self.config
287
+ if save:
288
+ self.save_config(namespace)
289
+ self.emit_state_changed(
290
+ reason="config_updated",
291
+ namespace="config",
292
+ name=namespace,
293
+ details=modict(path=path, value=self.get_path(path)),
294
+ )
295
+ return result
296
+
297
+ def export_tree(self):
298
+ return modict({
299
+ namespace: config
300
+ for namespace, config in self.configs.items()
301
+ if namespace == "agent" or bool(config)
302
+ })
303
+
187
304
  def emit_state_changed(self, **data):
188
305
  if hasattr(self.agent, "events"):
189
306
  self.agent.emit(RuntimeStateChangedEvent, **data)
@@ -221,22 +338,6 @@ class ConfigManager:
221
338
  self.config.pop(key, None)
222
339
  return self.config
223
340
 
224
- def pop_legacy_section(self, section):
225
- parts = str(section).split(".")
226
- parent = self.config
227
- for part in parts[:-1]:
228
- parent = parent.get(part) if isinstance(parent, dict) else None
229
- if parent is None:
230
- return None
231
- if isinstance(parent, dict):
232
- return parent.pop(parts[-1], None)
233
- return None
234
-
235
- def prune_empty_legacy_containers(self):
236
- plugins = self.config.get("plugins")
237
- if isinstance(plugins, dict) and not plugins:
238
- self.config.pop("plugins", None)
239
-
240
341
  def apply_config_defaults(
241
342
  self,
242
343
  namespace,
@@ -248,21 +349,7 @@ class ConfigManager:
248
349
  legacy_agent_sections=None,
249
350
  ):
250
351
  config = self.load_config(namespace, config_class=config_class, file=file, defaults=defaults)
251
- migrated = False
252
- for section in legacy_agent_sections or ():
253
- legacy = self.pop_legacy_section(section)
254
- if isinstance(legacy, dict):
255
- config.update(legacy)
256
- migrated = True
257
- for legacy_key, plugin_key in (legacy_agent_aliases or {}).items():
258
- if legacy_key in self.config:
259
- config[plugin_key] = self.config.pop(legacy_key)
260
- migrated = True
261
- self.prune_empty_legacy_containers()
262
352
  self.agent.config = self.config
263
- if migrated:
264
- self.save_config(namespace)
265
- self.save()
266
353
  return config
267
354
 
268
355
  def system_message(self):
@@ -263,13 +263,15 @@ class MainLoopManager:
263
263
  if command.name == "navigate_session":
264
264
  return self.agent.navigate_session(command.payload.direction)
265
265
  if command.name == "update_config":
266
- return self.agent.config_manager.update(
266
+ return self.agent.config_manager.update_config(
267
+ command.payload.get("namespace", "agent"),
267
268
  command.payload.get("config"),
268
269
  save=command.payload.get("save", True),
269
270
  **command.payload.get("kwargs", {}),
270
271
  )
271
272
  if command.name == "save_config":
272
- return self.agent.config_manager.save()
273
+ namespace = command.payload.get("namespace", "agent")
274
+ return self.agent.config_manager.save_all() if namespace is None else self.agent.config_manager.save_config(namespace)
273
275
  if command.name == "save_session":
274
276
  return self.agent.save_session()
275
277
  if command.name == "compact_session":
@@ -45,6 +45,7 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
45
45
  class ConfigUpdateRequest(BaseModel):
46
46
  values: dict = {}
47
47
  save: bool = True
48
+ namespace: str = "agent"
48
49
 
49
50
  class WakeupRequest(BaseModel):
50
51
  prompt: str
@@ -82,7 +83,7 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
82
83
 
83
84
  @app.patch("/config")
84
85
  def update_config(request: ConfigUpdateRequest):
85
- return jsonable(busy_guard(lambda: service.update_config(request.values, save=request.save)))
86
+ return jsonable(busy_guard(lambda: service.update_config(request.values, save=request.save, namespace=request.namespace)))
86
87
 
87
88
  @app.get("/session")
88
89
  def session():
@@ -61,7 +61,9 @@ def remembered_server_session():
61
61
  def create_server_agent(**agent_kwargs):
62
62
  if "session" not in agent_kwargs:
63
63
  agent_kwargs["session"] = remembered_server_session() or "latest"
64
- return Agent(**agent_kwargs)
64
+ agent = Agent(**agent_kwargs)
65
+ agent.config_manager.save()
66
+ return agent
65
67
 
66
68
  class AgentServer:
67
69
  """Thin REST/SSE bridge over one long-lived Agent instance.
@@ -75,6 +77,8 @@ class AgentServer:
75
77
  if runtime is None:
76
78
  raise ValueError("AgentServer requires an AgentRuntime")
77
79
  self.runtime = runtime
80
+ if hasattr(self.runtime, "agent") and getattr(self.runtime, "agent", None) is not None:
81
+ self.runtime.agent.config_manager.save()
78
82
  self.max_events = max_events
79
83
  self.events = deque(maxlen=max_events)
80
84
  self.event_lock = threading.Lock()