codex-agent-framework 0.1.26__tar.gz → 0.1.28__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 (132) hide show
  1. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/CHANGELOG.md +17 -0
  2. {codex_agent_framework-0.1.26/codex_agent_framework.egg-info → codex_agent_framework-0.1.28}/PKG-INFO +2 -1
  3. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/README.md +1 -0
  4. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/__init__.py +2 -1
  5. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/agent.py +39 -29
  6. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/agent_runtime.py +152 -5
  7. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/context/__init__.py +12 -0
  8. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/files/__init__.py +3 -3
  9. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/files/tools.py +14 -6
  10. codex_agent_framework-0.1.28/codex_agent/builtin_plugins/interface/__init__.py +53 -0
  11. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/context.py +43 -3
  12. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/event.py +4 -0
  13. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/server/core.py +5 -1
  14. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/status.py +124 -29
  15. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/stream.py +40 -0
  16. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/tui/chat.py +293 -21
  17. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/utils.py +151 -19
  18. codex_agent_framework-0.1.28/codex_agent/version.py +1 -0
  19. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28/codex_agent_framework.egg-info}/PKG-INFO +2 -1
  20. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/pyproject.toml +1 -1
  21. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_agent.py +56 -0
  22. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_chat.py +246 -25
  23. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_server.py +169 -0
  24. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_utils.py +64 -3
  25. codex_agent_framework-0.1.26/codex_agent/builtin_plugins/interface/__init__.py +0 -38
  26. codex_agent_framework-0.1.26/codex_agent/version.py +0 -1
  27. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/LICENSE +0 -0
  28. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/MANIFEST.in +0 -0
  29. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/__main__.py +0 -0
  30. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/ai.py +0 -0
  31. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/browser.py +0 -0
  32. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_commands.py +0 -0
  33. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/__init__.py +0 -0
  34. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  35. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
  36. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  37. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
  38. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  39. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
  40. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/content/system.py +0 -0
  41. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/content/vision.py +0 -0
  42. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  43. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
  44. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  45. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
  46. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
  47. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
  48. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
  49. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
  50. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
  51. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  52. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
  53. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  54. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
  55. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  56. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
  57. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
  58. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
  59. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
  60. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
  61. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
  62. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/realtime/session.py +0 -0
  63. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  64. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
  65. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
  66. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  67. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
  68. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
  69. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
  70. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/cli/__init__.py +0 -0
  71. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/cli/headless.py +0 -0
  72. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/cli/main.py +0 -0
  73. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/cli/output.py +0 -0
  74. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/cli/root.py +0 -0
  75. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/cli/runner.py +0 -0
  76. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/client.py +0 -0
  77. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/command.py +0 -0
  78. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/config.py +0 -0
  79. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/desktop.py +0 -0
  80. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/get_text/__init__.py +0 -0
  81. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/get_text/default_gitignore +0 -0
  82. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/get_text/get_text.py +0 -0
  83. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/get_text/simpler_get_text.py +0 -0
  84. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/hooks.py +0 -0
  85. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/image.py +0 -0
  86. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/latex.py +0 -0
  87. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/mainloop.py +0 -0
  88. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/message.py +0 -0
  89. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/plugin.py +0 -0
  90. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/prompts/system_prompt.txt +0 -0
  91. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/provider.py +0 -0
  92. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/runtime.py +0 -0
  93. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/scheduler.py +0 -0
  94. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  95. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/server/__init__.py +0 -0
  96. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/server/app.py +0 -0
  97. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/service.py +0 -0
  98. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/sessions.py +0 -0
  99. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/stream_utils.py +0 -0
  100. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/tool.py +0 -0
  101. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/tray.py +0 -0
  102. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/tui/__init__.py +0 -0
  103. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/tui/lifecycle.py +0 -0
  104. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent/worker.py +0 -0
  105. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent_framework.egg-info/SOURCES.txt +0 -0
  106. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  107. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  108. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent_framework.egg-info/requires.txt +0 -0
  109. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/codex_agent_framework.egg-info/top_level.txt +0 -0
  110. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/dependencies.txt +0 -0
  111. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/setup.cfg +0 -0
  112. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_agent_runtime.py +0 -0
  113. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_ai.py +0 -0
  114. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_browser.py +0 -0
  115. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_builtin_config.py +0 -0
  116. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_cli.py +0 -0
  117. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_cli_headless.py +0 -0
  118. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_cli_root.py +0 -0
  119. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_client.py +0 -0
  120. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_events.py +0 -0
  121. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_get_text_browser.py +0 -0
  122. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_hooks.py +0 -0
  123. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_image_message.py +0 -0
  124. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_local_desktop.py +0 -0
  125. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_memory.py +0 -0
  126. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_messages.py +0 -0
  127. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_planner.py +0 -0
  128. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_scheduler.py +0 -0
  129. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_service.py +0 -0
  130. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_tray.py +0 -0
  131. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_tui.py +0 -0
  132. {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.28}/tests/test_worker.py +0 -0
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project loosely follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and uses semantic versioning where practical.
6
6
 
7
+ ## [0.1.28] - 2026-05-15
8
+ ### Added
9
+ - Add `StreamManager` as a dedicated stream-processing manager and expose it through the package API.
10
+
11
+ ### Changed
12
+ - Improve Textual TUI streaming performance with coalesced replay/live deltas and safer incremental rendering.
13
+ - Make terminal resize handling re-render the TUI transcript and status cleanly.
14
+ - Treat the worker status cache as the single status source of truth, with process fallback limited to bootstrap.
15
+
16
+ ### Fixed
17
+ - Keep `/status`, context providers, and the TUI status bar aligned after worker restarts and busy turns.
18
+ - Preserve wrapper/message partition counters and token splits across context builds, API status updates, and restarts.
19
+ - Avoid status update events confusing stale-turn detection.
20
+
21
+ ### Tests
22
+ - Expand regression coverage for status cache replication, fallback bootstrap behavior, wrapper partitions, TUI delta batching, replay coalescing, and resize handling; the current suite validates at 576 passed.
23
+
7
24
  ## [0.1.25] - 2026-05-12
8
25
  ### Added
9
26
  - Add bundled plugin `requirements.txt` metadata for built-in capabilities and move image generation, realtime voice, and TTS into plugin-scoped packages.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.26
3
+ Version: 0.1.28
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -626,6 +626,7 @@ Core LLM calls, memory embeddings, non-streaming STT transcription, and realtime
626
626
 
627
627
  ## Recent releases
628
628
 
629
+ - `0.1.27`: upgrade the agentic `read` tool with folder trees, recursive folder pattern search, and precise `start_at_line`/`end_at_line` ranges; add visible wrapper/token split stats, compactable turn counts, and refreshed TUI status-bar context telemetry.
629
630
  - `0.1.26`: align SDK-facing config with Responses API shapes, add the `context` management plugin with selective non-destructive wrapper pruning, keep wrappers out of memory retrieval and compaction payloads, move embeddings/STT/realtime onto Codex backend auth, streamline voice/TTS plugin config, and improve TUI streaming/windowing.
630
631
  - `0.1.25`: move built-in capabilities further into plugin-scoped modules, add bundled plugin requirement metadata, and replace the TUI conversation pane with a selectable rich chat area that preserves mouse scrolling, copy selection, lazy transcript loading, and bottom-aware auto-scroll.
631
632
  - `0.1.24`: add slash-command lifecycle events and TUI progress/result rendering so commands such as `/compact` and `/help` show running, done, failed, result, and error feedback instead of going silent.
@@ -577,6 +577,7 @@ Core LLM calls, memory embeddings, non-streaming STT transcription, and realtime
577
577
 
578
578
  ## Recent releases
579
579
 
580
+ - `0.1.27`: upgrade the agentic `read` tool with folder trees, recursive folder pattern search, and precise `start_at_line`/`end_at_line` ranges; add visible wrapper/token split stats, compactable turn counts, and refreshed TUI status-bar context telemetry.
580
581
  - `0.1.26`: align SDK-facing config with Responses API shapes, add the `context` management plugin with selective non-destructive wrapper pruning, keep wrappers out of memory retrieval and compaction payloads, move embeddings/STT/realtime onto Codex backend auth, streamline voice/TTS plugin config, and improve TUI streaming/windowing.
581
582
  - `0.1.25`: move built-in capabilities further into plugin-scoped modules, add bundled plugin requirement metadata, and replace the TUI conversation pane with a selectable rich chat area that preserves mouse scrolling, copy selection, lazy transcript loading, and bottom-aware auto-scroll.
582
583
  - `0.1.24`: add slash-command lifecycle events and TUI progress/result rendering so commands such as `/compact` and `/help` show running, done, failed, result, and error feedback instead of going silent.
@@ -60,7 +60,7 @@ from .image import ImageMessage
60
60
  from .mainloop import MainLoopManager
61
61
  from .plugin import Plugin, PluginsManager, event_handler, hook_handler, stream_processor
62
62
  from .command import AgentCommand, AgentFuture, AgentTurn, CommandManager, command
63
- from .stream import AgentStream
63
+ from .stream import AgentStream, StreamManager
64
64
  from .provider import ProvidersManager, provider
65
65
  from .runtime import RuntimeManager
66
66
  from .sessions import AgentSession, SessionsManager
@@ -138,6 +138,7 @@ __all__ = [
138
138
  "AgentFuture",
139
139
  "AgentTurn",
140
140
  "AgentStream",
141
+ "StreamManager",
141
142
  "CommandManager",
142
143
  "command",
143
144
  "provider",
@@ -14,6 +14,7 @@ from .runtime import RuntimeManager
14
14
  from .scheduler import AgentScheduler
15
15
  from .sessions import SessionsManager
16
16
  from .status import StatusManager
17
+ from .stream import StreamManager
17
18
  from .tool import ToolsManager, reset_current_agent, set_current_agent
18
19
  from .ai import AIClient
19
20
  from .event import AssistantInterruptRequestedEvent, AssistantInterrupted, AssistantInterruptedEvent, EventBus
@@ -32,26 +33,20 @@ class Agent:
32
33
  ensure_runtime_dir()
33
34
  self.init_execution_environment()
34
35
  self.config_manager=ConfigManager(self)
35
- self.config=self.config_manager.config
36
36
  self.config_manager.load()
37
37
  self.config_manager.update(kwargs, save=False)
38
38
  self.config_manager.apply_openai_capability_defaults()
39
39
  self.events=EventBus()
40
40
  self.tools_manager=ToolsManager(self)
41
- self.tools=self.tools_manager.tools
42
41
  self.providers_manager=ProvidersManager(self)
43
- self.providers=self.providers_manager.providers
44
42
  self.command_manager=CommandManager(self)
45
- self.commands=self.command_manager.commands
46
43
  self.context_manager=ContextManager(self)
47
44
  self.plugins_manager=PluginsManager(self)
48
- self.plugins=self.plugins_manager.plugins
49
45
  self.sessions_manager=SessionsManager(self)
50
46
  self.runtime=RuntimeManager(self)
51
47
  self.scheduler=AgentScheduler(self)
52
48
  self.hooks=HookManager(self)
53
- self.stream_processors=modict()
54
- self.stream_processor_owners={}
49
+ self.stream_manager=StreamManager(self)
55
50
  self.mainloop_manager=MainLoopManager(self)
56
51
  self._interrupt_requested=ThreadEvent()
57
52
  self.current_session_id=None
@@ -86,6 +81,40 @@ class Agent:
86
81
  mainloop_manager.record_event(event)
87
82
  return event
88
83
 
84
+ @property
85
+ def config(self):
86
+ return self.config_manager.config
87
+
88
+ @config.setter
89
+ def config(self, value):
90
+ self.config_manager.config = value
91
+ if hasattr(self.config_manager, "configs"):
92
+ self.config_manager.configs.agent = value
93
+
94
+ @property
95
+ def tools(self):
96
+ return self.tools_manager.tools
97
+
98
+ @property
99
+ def providers(self):
100
+ return self.providers_manager.providers
101
+
102
+ @property
103
+ def commands(self):
104
+ return self.command_manager.commands
105
+
106
+ @property
107
+ def plugins(self):
108
+ return self.plugins_manager.plugins
109
+
110
+ @property
111
+ def stream_processors(self):
112
+ return self.stream_manager.processors
113
+
114
+ @property
115
+ def stream_processor_owners(self):
116
+ return self.stream_manager.owners
117
+
89
118
  @property
90
119
  def busy(self):
91
120
  return self.mainloop_manager.busy
@@ -274,32 +303,13 @@ class Agent:
274
303
  return self.plugins_manager.reload_plugin(plugin_or_name, timeout=timeout)
275
304
 
276
305
  def add_stream_processor(self, key, processor, owner=None):
277
- processors = self.stream_processors.setdefault(str(key), [])
278
- processors.append(processor)
279
- if owner is not None:
280
- self.stream_processor_owners.setdefault(str(key), {})[processor] = owner
281
- return processor
306
+ return self.stream_manager.add(key, processor, owner=owner)
282
307
 
283
308
  def get_stream_processors(self, key):
284
- return list(self.stream_processors.get(str(key), []))
309
+ return self.stream_manager.get(key)
285
310
 
286
311
  def remove_stream_processors_by_owner(self, owner):
287
- removed = modict()
288
- for key, processors in list(self.stream_processors.items()):
289
- owners = self.stream_processor_owners.get(key, {})
290
- kept = []
291
- for processor in processors:
292
- if owners.get(processor) == owner:
293
- removed.setdefault(key, []).append(processor)
294
- owners.pop(processor, None)
295
- else:
296
- kept.append(processor)
297
- if kept:
298
- self.stream_processors[key] = kept
299
- else:
300
- self.stream_processors.pop(key, None)
301
- self.stream_processor_owners.pop(key, None)
302
- return removed
312
+ return self.stream_manager.remove_by_owner(owner)
303
313
 
304
314
  def add_hook(self, key, func=None, owner=None):
305
315
  """Register a runtime hook.
@@ -8,8 +8,11 @@ from abc import ABC, abstractmethod
8
8
  from modict import modict
9
9
 
10
10
  from .agent import Agent
11
+ from .config import AgentConfig
11
12
  from .event import Event
12
- from .message import UserMessage, message_from_data
13
+ from .message import ToolResultWrapper, UserMessage, message_from_data
14
+ from .sessions import AgentSession
15
+ from .utils import token_count, truncate
13
16
 
14
17
 
15
18
  def jsonable(value):
@@ -270,13 +273,38 @@ class ProcessAgentRuntime(AgentRuntime):
270
273
  current_turn_id=None,
271
274
  current_session_id=None,
272
275
  status=modict(),
273
- config=modict(),
276
+ config=AgentConfig(),
274
277
  session=modict(),
275
278
  sessions=modict(current=None, sessions=[]),
276
279
  messages=[],
277
280
  tools=modict(),
278
281
  wakeups=[],
279
282
  )
283
+ self.hydrate_local_session_cache()
284
+
285
+ def local_session_id(self):
286
+ session_id = self.agent_kwargs.get("session")
287
+ if not session_id or session_id == "latest":
288
+ try:
289
+ sessions = AgentSession.list_sessions()
290
+ except Exception:
291
+ return None
292
+ return sessions[0] if sessions else None
293
+ return session_id if AgentSession.can_load(session_id) else None
294
+
295
+ def hydrate_local_session_cache(self):
296
+ session_id = self.local_session_id()
297
+ if not session_id:
298
+ return False
299
+ try:
300
+ session = AgentSession.load(session_id)
301
+ except Exception:
302
+ return False
303
+ self.cache.current_session_id = session.session_id
304
+ self.cache.session = session
305
+ self.cache.messages = list(session.messages)
306
+ self.refresh_cached_wrapper_status(hydrate=False)
307
+ return True
280
308
 
281
309
  def start(self):
282
310
  with self.start_lock:
@@ -293,6 +321,7 @@ class ProcessAgentRuntime(AgentRuntime):
293
321
  self.process.start()
294
322
  self.event_thread = threading.Thread(target=self._event_loop, name="agent-runtime-events", daemon=True)
295
323
  self.event_thread.start()
324
+ self.hydrate_local_session_cache()
296
325
  try:
297
326
  self.call("start", timeout=self.command_timeout, autostart=False)
298
327
  self.refresh_cache()
@@ -339,6 +368,8 @@ class ProcessAgentRuntime(AgentRuntime):
339
368
  except Exception:
340
369
  pass
341
370
  self.update_status_cache(self.cache.get("status") or {})
371
+ if not self.cache.get("status"):
372
+ self.refresh_cached_wrapper_status()
342
373
  return self.cache
343
374
 
344
375
  def call(self, name, payload=None, timeout=None, autostart=True):
@@ -391,6 +422,107 @@ class ProcessAgentRuntime(AgentRuntime):
391
422
  self.cache.current_session_id = status.get("session_id", self.cache.current_session_id)
392
423
  return status
393
424
 
425
+ def cached_message_from_event(self, message):
426
+ if not message:
427
+ return None
428
+ try:
429
+ return message_from_data(message)
430
+ except Exception:
431
+ return modict(message)
432
+
433
+ def is_cached_wrapper(self, message):
434
+ if isinstance(message, ToolResultWrapper):
435
+ return True
436
+ types = message.get("types") or []
437
+ return message.get("type") == "tool_result_wrapper" or "tool_result_wrapper" in types
438
+
439
+ def cached_wrapper_tokens(self, messages):
440
+ total = 0
441
+ config = modict(self.cache.get("config") or {})
442
+ max_tokens = int(config.get("max_input_tokens", 8000) or 8000)
443
+ model = config.get("model")
444
+ for message in messages:
445
+ content = message.get("content")
446
+ if not isinstance(content, str) or not content:
447
+ continue
448
+ total += token_count(truncate(content, max_tokens=max_tokens, model=model), model=model)
449
+ return total
450
+
451
+ def refresh_cached_wrapper_status(self, hydrate=True):
452
+ status = modict(self.cache.status or {})
453
+ if hydrate and not self.cache.get("messages"):
454
+ self.hydrate_local_session_cache()
455
+ messages = list(self.cache.get("messages") or [])
456
+ wrappers = [message for message in messages if self.is_cached_wrapper(message)]
457
+ active = [
458
+ message for message in wrappers
459
+ if isinstance(message.get("turns_left", -1), int) and message.get("turns_left", -1) > 0
460
+ ]
461
+ vanished = [message for message in wrappers if message.get("turns_left", -1) == 0]
462
+ active_tokens = self.cached_wrapper_tokens(active)
463
+ vanished_tokens = self.cached_wrapper_tokens(vanished)
464
+ visible_non_wrappers = [
465
+ message for message in messages
466
+ if not self.is_cached_wrapper(message) and message.get("turns_left", -1) != 0
467
+ ]
468
+ estimated_base_tokens = self.cached_wrapper_tokens(visible_non_wrappers)
469
+ # The fallback cache is updated by message events while the worker may be busy. Do not preserve
470
+ # a stale worker-provided context_current_tokens here: otherwise wrapper/message counts move but
471
+ # the context percentage appears frozen until a full worker status succeeds.
472
+ current_tokens = estimated_base_tokens + active_tokens
473
+ persistent = [message for message in messages if message.get("turns_left", -1) == -1]
474
+ temporary = [
475
+ message for message in messages
476
+ if isinstance(message.get("turns_left", -1), int) and message.get("turns_left", -1) > 0
477
+ ]
478
+ config = modict(self.cache.get("config") or {})
479
+ input_limit = int(status.get("context_limit_tokens") or config.get("input_token_limit", 0) or 0)
480
+ if not current_tokens:
481
+ current_tokens = estimated_base_tokens + active_tokens
482
+ base_tokens = max(0, current_tokens - active_tokens) if current_tokens else estimated_base_tokens
483
+ status.update(
484
+ context_current_tokens=current_tokens,
485
+ context_limit_tokens=input_limit,
486
+ context_percent=(current_tokens / input_limit * 100) if input_limit else 0,
487
+ model=status.get("model") or config.get("model"),
488
+ source="process_fallback",
489
+ total_session_messages=len(messages) or status.get("total_session_messages", 0),
490
+ persistent_session_messages=len(persistent),
491
+ temporary_session_messages=len(temporary),
492
+ active_wrapper_messages=len(active),
493
+ vanished_wrapper_messages=len(vanished),
494
+ total_wrapper_messages=len(wrappers),
495
+ active_wrapper_tokens=active_tokens,
496
+ vanished_wrapper_tokens=vanished_tokens,
497
+ base_context_tokens=base_tokens,
498
+ )
499
+ self.cache.status = status
500
+ return status
501
+
502
+ def has_worker_status_cache(self):
503
+ status = modict(self.cache.get("status") or {})
504
+ source = status.get("source")
505
+ return bool(source) and source != "process_fallback"
506
+
507
+ def append_cached_message(self, message):
508
+ message = self.cached_message_from_event(message)
509
+ if message is None:
510
+ return None
511
+ self.cache.messages.append(message)
512
+ if not self.has_worker_status_cache():
513
+ return self.refresh_cached_wrapper_status()
514
+ return self.cache.status
515
+
516
+ def age_cached_context_messages(self):
517
+ changed = False
518
+ for message in self.cache.get("messages") or []:
519
+ turns_left = message.get("turns_left", -1)
520
+ if isinstance(turns_left, int) and turns_left > 0:
521
+ message.turns_left = turns_left - 1
522
+ changed = True
523
+ if changed and not self.has_worker_status_cache():
524
+ self.refresh_cached_wrapper_status()
525
+
394
526
  def mark_turn_started(self, turn_id=None):
395
527
  self.cache.busy = True
396
528
  if turn_id is not None:
@@ -404,12 +536,22 @@ class ProcessAgentRuntime(AgentRuntime):
404
536
  event_type = event.get("type")
405
537
  if event_type == "assistant_turn_start_event":
406
538
  self.mark_turn_started(event.get("turn_id"))
539
+ self.age_cached_context_messages()
407
540
  elif event_type in ("assistant_turn_end_event", "assistant_turn_error_event"):
408
541
  self.mark_turn_finished()
542
+ elif event_type == "message_added_event":
543
+ message = self.cached_message_from_event(event.get("message"))
544
+ if message is not None:
545
+ self.cache.messages.append(message)
546
+ if not self.has_worker_status_cache():
547
+ self.refresh_cached_wrapper_status()
409
548
  elif event_type == "session_loaded_event":
410
549
  self.cache.current_session_id = event.get("session_id")
411
550
  self.cache.session = event.get("session") or self.cache.session
412
- elif event_type == "compaction_completed_event" and event.get("status"):
551
+ self.cache.messages = list((self.cache.session or {}).get("messages") or [])
552
+ if not self.has_worker_status_cache():
553
+ self.refresh_cached_wrapper_status()
554
+ elif event_type in ("status_updated_event", "compaction_completed_event") and event.get("status"):
413
555
  self.update_status_cache(event.status)
414
556
  for handler in list(self.event_handlers):
415
557
  try:
@@ -434,8 +576,13 @@ class ProcessAgentRuntime(AgentRuntime):
434
576
  return self.update_status_cache(self.call("get_status", timeout=self.status_timeout))
435
577
  except Exception:
436
578
  status = modict(self.cache.status or {})
437
- status.setdefault("busy", self.cache.busy)
438
- status.setdefault("current_turn_id", self.cache.current_turn_id)
579
+ if not self.has_worker_status_cache():
580
+ status = self.refresh_cached_wrapper_status()
581
+ status.update(
582
+ busy=self.cache.busy,
583
+ current_turn_id=self.cache.current_turn_id,
584
+ session_id=self.cache.current_session_id,
585
+ )
439
586
  return status
440
587
 
441
588
  def config(self):
@@ -33,6 +33,15 @@ Tool result wrappers expose stable `wrapper_id` values in their XML representati
33
33
  total_history_messages=status.get("total_history_messages", 0),
34
34
  persistent_session_messages=status.get("persistent_session_messages", 0),
35
35
  temporary_session_messages=status.get("temporary_session_messages", 0),
36
+ active_wrapper_messages=status.get("active_wrapper_messages", 0),
37
+ vanished_wrapper_messages=status.get("vanished_wrapper_messages", 0),
38
+ total_wrapper_messages=status.get("total_wrapper_messages", 0),
39
+ base_context_tokens=status.get("base_context_tokens", status.used_tokens),
40
+ active_wrapper_tokens=status.get("active_wrapper_tokens", 0),
41
+ vanished_wrapper_tokens=status.get("vanished_wrapper_tokens", 0),
42
+ compactable_turns=status.get("compactable_turns", 0),
43
+ compactable_messages=status.get("compactable_messages", 0),
44
+ compactable_tokens=status.get("compactable_tokens", 0),
36
45
  context_session_messages=status.get("context_session_messages", 0),
37
46
  image_messages=status.get("image_messages", 0),
38
47
  pending_messages=status.get("pending_messages", 0),
@@ -48,8 +57,11 @@ Tool result wrappers expose stable `wrapper_id` values in their XML representati
48
57
  return "\n".join([
49
58
  "Context status:",
50
59
  f"- tokens: {status.used_tokens}/{status.input_token_limit} ({status.percent:.2f}%)",
60
+ f"- token split base/active_wrappers/vanished_wrappers: {status.base_context_tokens}/{status.active_wrapper_tokens}/{status.vanished_wrapper_tokens}",
51
61
  f"- messages sent/total: {status.sent_session_messages}/{status.total_session_messages}",
52
62
  f"- persistent/temporary context messages: {status.persistent_session_messages}/{status.temporary_session_messages}",
63
+ f"- wrappers active/vanished/total: {status.active_wrapper_messages}/{status.vanished_wrapper_messages}/{status.total_wrapper_messages}",
64
+ f"- compactable turns/messages/tokens: {status.compactable_turns}/{status.compactable_messages}/{status.compactable_tokens}",
53
65
  f"- visible/total history messages: {status.visible_history_messages}/{status.total_history_messages}",
54
66
  f"- tool spec tokens: {status.tool_spec_tokens}",
55
67
  f"- auto_compact: {status.auto_compact} threshold={status.auto_compact_threshold}",
@@ -7,13 +7,13 @@ from . import tools as file_tools
7
7
 
8
8
  class FilesPlugin(Plugin):
9
9
  name = "files"
10
- description = "Strict local UTF-8 file read/write/edit operations."
10
+ description = "Strict local UTF-8 file/folder read and search plus write/edit operations."
11
11
  prefix_tools = False
12
12
  system_prompt = """
13
13
  # File Tools
14
14
 
15
- Use the specialized file tools for local UTF-8 text file operations:
16
- - `read`: read line-numbered local UTF-8 files precisely, with optional pattern search.
15
+ Use the specialized file tools for local UTF-8 text operations:
16
+ - `read`: read line-numbered local UTF-8 files precisely, list local folders as a compact one-level tree, or search folders recursively with a pattern.
17
17
  - `write`: create or overwrite complete UTF-8 text files; use with caution.
18
18
  - `edit`: perform targeted exact-string replacements in local UTF-8 text files.
19
19
 
@@ -8,10 +8,11 @@ from ...utils import read_document_content, view_document_content
8
8
  def read(reads):
9
9
  """
10
10
  description: |
11
- Strictly read one or more local UTF-8 text files as source-of-truth content.
12
- This tool always prefixes returned content with reliable source line numbers.
13
- It does not extract document formats, fetch URLs, inspect Python objects, or render directory trees.
14
- Use `view` for folders, URLs, PDFs, DOCX, ODT, HTML, XLSX, and other extracted or transformed content.
11
+ Read one or more local UTF-8 text files or folders as source-of-truth content.
12
+ File reads always prefix returned content with reliable source line numbers.
13
+ Folder reads without `pattern` render a compact tree with one level of recursion.
14
+ Folder reads with `pattern` recursively search UTF-8 files under the folder and return numbered snippets per matching file.
15
+ This tool does not extract document formats, fetch URLs, or inspect Python objects; use `view` for URLs, PDFs, DOCX, ODT, HTML, XLSX, archives, and other extracted or transformed content.
15
16
 
16
17
  For large outputs: the output is automatically truncated. When truncated, a message indicates the omitted line range.
17
18
  When `pattern` is provided, it is treated as a Python regex by default and all matches are returned as separated numbered snippets with up to 10 lines before and after each match.
@@ -20,16 +21,19 @@ def read(reads):
20
21
  reads:
21
22
  description: |
22
23
  A single read object or a list of read objects.
23
- Each object must contain `source` and may contain `start_at_line`, `pattern`, and `regex`.
24
+ Each object must contain `source` and may contain `start_at_line`, `end_at_line`, `pattern`, and `regex`.
24
25
  anyOf:
25
26
  - type: object
26
27
  properties:
27
28
  source:
28
- description: Local UTF-8 text file path only. For directories, URLs, rich documents, or Python objects, use `view` instead.
29
+ description: Local UTF-8 text file or folder path. Folders are listed as a compact tree, or searched recursively when pattern is provided. For URLs, rich documents, archives, or Python objects, use `view` instead.
29
30
  type: string
30
31
  start_at_line:
31
32
  description: Line number to start reading from (1-indexed). Default is 1 (start from beginning).
32
33
  type: integer
34
+ end_at_line:
35
+ description: Optional inclusive line number to stop reading at for direct file reads.
36
+ type: integer
33
37
  pattern:
34
38
  description: Optional search pattern. Treated as a Python regular expression by default.
35
39
  type: string
@@ -43,9 +47,12 @@ def read(reads):
43
47
  type: object
44
48
  properties:
45
49
  source:
50
+ description: Local UTF-8 text file or folder path. Folders are listed as a compact tree, or searched recursively when pattern is provided.
46
51
  type: string
47
52
  start_at_line:
48
53
  type: integer
54
+ end_at_line:
55
+ type: integer
49
56
  pattern:
50
57
  type: string
51
58
  regex:
@@ -79,6 +86,7 @@ def apply_file_read(item):
79
86
  return read_document_content(
80
87
  source,
81
88
  start_at_line=item.get("start_at_line", 1),
89
+ end_at_line=item.get("end_at_line"),
82
90
  max_tokens=max_tokens,
83
91
  pattern=item.get("pattern"),
84
92
  regex=item.get("regex", True),
@@ -0,0 +1,53 @@
1
+ """Built-in local interface plugin."""
2
+
3
+ from ...plugin import Plugin
4
+ from ...provider import provider
5
+ from ...tool import tool
6
+ from ...tui import current_tui, open_tui_terminal, terminate_tui
7
+
8
+
9
+ class InterfacePlugin(Plugin):
10
+ name = "interface"
11
+ description = "Open and close the local TUI interface."
12
+ prefix_tools = False
13
+
14
+ @provider
15
+ def tui_status(self):
16
+ """Expose whether the local TUI client is currently open."""
17
+ tui = current_tui()
18
+ if not tui:
19
+ return "TUI status: closed."
20
+ return (
21
+ "TUI status: open. "
22
+ f"pid={tui.get('pid')} "
23
+ f"base_url={tui.get('base_url')} "
24
+ f"timestamp={tui.get('timestamp')}"
25
+ )
26
+
27
+ @tool(name="open_tui")
28
+ def open_tui(self):
29
+ """
30
+ description: Open the Textual chat TUI client in a separate terminal connected to the local agent server.
31
+ parameters: {}
32
+ required: []
33
+ """
34
+ tui = current_tui()
35
+ if tui:
36
+ return {"status": "already_open", "tui": tui}
37
+ process = open_tui_terminal(base_url=self.agent.config.get("server_url", "http://127.0.0.1:8765"))
38
+ if process is None:
39
+ raise RuntimeError("No supported terminal emulator found to open the TUI")
40
+ return {"status": "opened", "pid": process.pid}
41
+
42
+ @tool(name="close_tui")
43
+ def close_tui(self):
44
+ """
45
+ description: Close the currently registered Textual chat TUI client process.
46
+ parameters: {}
47
+ required: []
48
+ """
49
+ tui = current_tui()
50
+ if not tui:
51
+ return {"status": "not_open"}
52
+ terminate_tui(tui.pid)
53
+ return {"status": "closed", "tui": tui}
@@ -1,7 +1,7 @@
1
1
  from modict import modict
2
2
 
3
3
  from .image import ImageMessage
4
- from .message import Message, ProviderMessage, SystemMessage
4
+ from .message import Message, ProviderMessage, SystemMessage, ToolResultWrapper
5
5
  from .tool import reset_current_agent, set_current_agent
6
6
  from .utils import sort, token_count_payload, truncate
7
7
 
@@ -100,14 +100,45 @@ class ContextManager:
100
100
  formatted_context = list(msg.format(context=self.agent.context_namespace) for msg in context)
101
101
  total_session_messages = len(self.agent.session.messages) if self.agent.session else 0
102
102
  sent_session_messages = len(history) + len(images)
103
+ all_session_messages = list(self.agent.session.messages) if self.agent.session else []
103
104
  persistent_session_messages = sum(
104
- 1 for msg in session_messages
105
+ 1 for msg in all_session_messages
105
106
  if msg.get("turns_left", -1) == -1
106
107
  )
107
108
  temporary_session_messages = sum(
108
- 1 for msg in session_messages
109
+ 1 for msg in all_session_messages
109
110
  if isinstance(msg.get("turns_left", -1), int) and msg.get("turns_left", -1) > 0
110
111
  )
112
+ wrappers = [msg for msg in all_session_messages if isinstance(msg, ToolResultWrapper)]
113
+ active_wrappers = [
114
+ msg for msg in wrappers
115
+ if isinstance(msg.get("turns_left", -1), int) and msg.get("turns_left", -1) > 0
116
+ ]
117
+ vanished_wrappers = [msg for msg in wrappers if msg.get("turns_left", -1) == 0]
118
+
119
+ def wrapper_token_count(messages):
120
+ total = 0
121
+ for msg in messages:
122
+ total += self.response_input_token_count([msg], response_tools=[])
123
+ return total
124
+
125
+ active_context_wrappers = [msg for msg in history if isinstance(msg, ToolResultWrapper)]
126
+ active_wrapper_tokens = wrapper_token_count(active_context_wrappers)
127
+ vanished_wrapper_tokens = wrapper_token_count(vanished_wrappers)
128
+ base_context_tokens = max(0, current_count - active_wrapper_tokens)
129
+ if self.agent.session:
130
+ compaction_plan = self.agent.session.compaction_plan(
131
+ max_input_tokens=max_input_tokens,
132
+ model=self.agent.config.model,
133
+ max_compacted_turns=None,
134
+ )
135
+ compactable_turns = compaction_plan.completed_turn_count
136
+ compactable_messages = compaction_plan.compacted_message_count
137
+ compactable_tokens = compaction_plan.compacted_token_count
138
+ else:
139
+ compactable_turns = 0
140
+ compactable_messages = 0
141
+ compactable_tokens = 0
111
142
  token_count_source = "local_estimate"
112
143
  try:
113
144
  current_count = self.agent.ai.count_input_tokens(
@@ -125,6 +156,15 @@ class ContextManager:
125
156
  sent_session_message_percent=(sent_session_messages / total_session_messages * 100) if total_session_messages else 0,
126
157
  persistent_session_messages=persistent_session_messages,
127
158
  temporary_session_messages=temporary_session_messages,
159
+ active_wrapper_messages=len(active_wrappers),
160
+ vanished_wrapper_messages=len(vanished_wrappers),
161
+ total_wrapper_messages=len(wrappers),
162
+ base_context_tokens=base_context_tokens,
163
+ active_wrapper_tokens=active_wrapper_tokens,
164
+ vanished_wrapper_tokens=vanished_wrapper_tokens,
165
+ compactable_turns=compactable_turns,
166
+ compactable_messages=compactable_messages,
167
+ compactable_tokens=compactable_tokens,
128
168
  token_count_source=token_count_source,
129
169
  ))
130
170
  hook = self.agent.run_hook("context.build.after", messages=formatted_context)
@@ -102,6 +102,10 @@ class MessageAddedEvent(Event):
102
102
  message = None
103
103
 
104
104
 
105
+ class StatusUpdatedEvent(Event):
106
+ status = None
107
+
108
+
105
109
  class MessageSubmittedEvent(Event):
106
110
  message = None
107
111
  turn_id = None
@@ -396,9 +396,13 @@ class AgentServer:
396
396
 
397
397
  def event_stream_status(self, stale_timeout=120):
398
398
  with self.event_lock:
399
- last_event = self.events[-1] if self.events else None
400
399
  active = bool(self.turn_replay_active)
401
400
  current_turn_events = list(self.current_turn_events)
401
+ source_events = current_turn_events if current_turn_events else list(self.events)
402
+ last_event = next(
403
+ (event for event in reversed(source_events) if event.get("type") != "status_updated_event"),
404
+ None,
405
+ )
402
406
  last_timestamp = self.event_timestamp(last_event)
403
407
  now = time.time()
404
408
  return modict(