codex-agent-framework 0.1.31__tar.gz → 0.1.33__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.31 → codex_agent_framework-0.1.33}/CHANGELOG.md +18 -0
  2. {codex_agent_framework-0.1.31/codex_agent_framework.egg-info → codex_agent_framework-0.1.33}/PKG-INFO +1 -1
  3. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/agent.py +39 -33
  4. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/plugin.py +6 -12
  5. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/prompts/system_prompt.txt +7 -7
  6. codex_agent_framework-0.1.33/codex_agent/version.py +1 -0
  7. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33/codex_agent_framework.egg-info}/PKG-INFO +1 -1
  8. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent_framework.egg-info/SOURCES.txt +15 -1
  9. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/pyproject.toml +3 -2
  10. codex_agent_framework-0.1.33/tests/test_agent.py +332 -0
  11. codex_agent_framework-0.1.33/tests/test_agent_audio_hooks.py +88 -0
  12. codex_agent_framework-0.1.33/tests/test_agent_commands.py +261 -0
  13. codex_agent_framework-0.1.33/tests/test_agent_context.py +523 -0
  14. codex_agent_framework-0.1.33/tests/test_agent_desktop.py +268 -0
  15. codex_agent_framework-0.1.33/tests/test_agent_environment.py +232 -0
  16. codex_agent_framework-0.1.33/tests/test_agent_image_generation.py +200 -0
  17. codex_agent_framework-0.1.33/tests/test_agent_observe.py +134 -0
  18. codex_agent_framework-0.1.33/tests/test_agent_plugins.py +722 -0
  19. codex_agent_framework-0.1.33/tests/test_agent_providers_config.py +128 -0
  20. codex_agent_framework-0.1.33/tests/test_agent_realtime.py +285 -0
  21. codex_agent_framework-0.1.33/tests/test_agent_response.py +414 -0
  22. codex_agent_framework-0.1.33/tests/test_agent_sessions.py +329 -0
  23. codex_agent_framework-0.1.33/tests/test_agent_subagents.py +310 -0
  24. codex_agent_framework-0.1.33/tests/test_agent_tools.py +615 -0
  25. codex_agent_framework-0.1.33/tests/test_agent_turns.py +331 -0
  26. codex_agent_framework-0.1.31/codex_agent/version.py +0 -1
  27. codex_agent_framework-0.1.31/tests/test_agent.py +0 -4964
  28. codex_agent_framework-0.1.31/tests/test_tray.py +0 -416
  29. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/LICENSE +0 -0
  30. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/MANIFEST.in +0 -0
  31. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/README.md +0 -0
  32. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/__init__.py +0 -0
  33. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/__main__.py +0 -0
  34. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/agent_runtime.py +0 -0
  35. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/ai.py +0 -0
  36. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/browser.py +0 -0
  37. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_commands.py +0 -0
  38. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/__init__.py +0 -0
  39. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  40. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
  41. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  42. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
  43. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  44. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
  45. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/content/system.py +0 -0
  46. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/content/vision.py +0 -0
  47. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/context/__init__.py +0 -0
  48. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  49. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
  50. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  51. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
  52. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  53. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
  54. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/files/tools.py +0 -0
  55. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
  56. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
  57. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
  58. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  59. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
  60. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  61. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
  62. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  63. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
  64. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  65. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
  66. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
  67. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
  68. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
  69. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
  70. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
  71. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/realtime/session.py +0 -0
  72. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  73. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
  74. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
  75. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  76. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
  77. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
  78. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
  79. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/cli/__init__.py +0 -0
  80. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/cli/headless.py +0 -0
  81. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/cli/main.py +0 -0
  82. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/cli/output.py +0 -0
  83. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/cli/root.py +0 -0
  84. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/cli/runner.py +0 -0
  85. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/client.py +0 -0
  86. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/command.py +0 -0
  87. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/config.py +0 -0
  88. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/context.py +0 -0
  89. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/desktop.py +0 -0
  90. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/event.py +0 -0
  91. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/get_text/__init__.py +0 -0
  92. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/get_text/default_gitignore +0 -0
  93. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/get_text/get_text.py +0 -0
  94. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/get_text/simpler_get_text.py +0 -0
  95. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/hooks.py +0 -0
  96. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/image.py +0 -0
  97. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/latex.py +0 -0
  98. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/mainloop.py +0 -0
  99. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/message.py +0 -0
  100. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/provider.py +0 -0
  101. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/runtime.py +0 -0
  102. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/scheduler.py +0 -0
  103. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  104. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/server/__init__.py +0 -0
  105. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/server/app.py +0 -0
  106. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/server/core.py +0 -0
  107. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/service.py +0 -0
  108. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/sessions.py +0 -0
  109. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/status.py +0 -0
  110. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/stream.py +0 -0
  111. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/stream_utils.py +0 -0
  112. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tool.py +0 -0
  113. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tray.py +0 -0
  114. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tui/__init__.py +0 -0
  115. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tui/app.py +0 -0
  116. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tui/chat.py +0 -0
  117. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tui/lifecycle.py +0 -0
  118. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tui/log.py +0 -0
  119. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tui/state.py +0 -0
  120. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tui/status_bar.py +0 -0
  121. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tui/style.py +0 -0
  122. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/tui/transcript.py +0 -0
  123. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/utils.py +0 -0
  124. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent/worker.py +0 -0
  125. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  126. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  127. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent_framework.egg-info/requires.txt +0 -0
  128. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/codex_agent_framework.egg-info/top_level.txt +0 -0
  129. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/dependencies.txt +0 -0
  130. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/setup.cfg +0 -0
  131. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_agent_runtime.py +0 -0
  132. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_ai.py +0 -0
  133. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_browser.py +0 -0
  134. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_builtin_config.py +0 -0
  135. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_chat.py +0 -0
  136. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_cli.py +0 -0
  137. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_cli_headless.py +0 -0
  138. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_cli_root.py +0 -0
  139. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_client.py +0 -0
  140. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_events.py +0 -0
  141. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_get_text_browser.py +0 -0
  142. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_hooks.py +0 -0
  143. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_image_message.py +0 -0
  144. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_local_desktop.py +0 -0
  145. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_memory.py +0 -0
  146. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_messages.py +0 -0
  147. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_planner.py +0 -0
  148. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_scheduler.py +0 -0
  149. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_server.py +0 -0
  150. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_service.py +0 -0
  151. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_tui.py +0 -0
  152. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_utils.py +0 -0
  153. {codex_agent_framework-0.1.31 → codex_agent_framework-0.1.33}/tests/test_worker.py +0 -0
@@ -4,6 +4,24 @@ 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.33] - 2026-05-16
8
+ ### Changed
9
+ - Simplify plugin decorated-method accessors by sharing the handler pair construction path.
10
+ - Deduplicate queued Agent state-command dispatch for scheduler and hook registration operations.
11
+ - Tighten Agent tool-facing docstrings for audio transcription and workfolder uploads without changing runtime behavior.
12
+
13
+ ### Tests
14
+ - Validate the cleanup pass with targeted plugin, Agent, scheduler, hook, tool, audio, and image tests plus the 553-test offline CI suite.
15
+
16
+ ## [0.1.32] - 2026-05-16
17
+ ### Changed
18
+ - Split the former `tests/test_agent.py` monolith into focused agent test modules for audio hooks, commands, context, desktop, environment, image generation, observe, plugins, providers/config, realtime, response handling, sessions, subagents, tools, and turns.
19
+ - Keep realtime voice coverage separated as local-heavy while preserving the offline CI suite for headless runners.
20
+ - Trim the remaining core agent tests to a small, focused file with obsolete helper fakes and imports removed.
21
+
22
+ ### Tests
23
+ - Expand the explicit offline CI test selection to cover the extracted agent modules; the current offline suite validates at 553 passed.
24
+
7
25
  ## [0.1.29] - 2026-05-15
8
26
  ### Changed
9
27
  - Split the Textual TUI implementation into focused modules for app wiring, transcript rendering, status display, styling, log windowing, and render state.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.31
3
+ Version: 0.1.33
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -137,6 +137,11 @@ class Agent:
137
137
  def submit(self, command, **payload):
138
138
  return self.mainloop_manager.submit(command, **payload)
139
139
 
140
+ def _run_or_queue_state_command(self, command, action, **payload):
141
+ if hasattr(self, "mainloop_manager") and self.mainloop_manager.should_queue_state_command():
142
+ return self.submit(command, **payload).wait().result
143
+ return action()
144
+
140
145
  def start_turn(self):
141
146
  return self.mainloop_manager.start_turn()
142
147
 
@@ -157,19 +162,25 @@ class Agent:
157
162
  return self.scheduler.list(include_done=include_done)
158
163
 
159
164
  def schedule_wakeup(self, **kwargs):
160
- if self.mainloop_manager.should_queue_state_command():
161
- return self.submit("schedule_wakeup", **kwargs).wait().result
162
- return self.scheduler.schedule(**kwargs)
165
+ return self._run_or_queue_state_command(
166
+ "schedule_wakeup",
167
+ lambda: self.scheduler.schedule(**kwargs),
168
+ **kwargs,
169
+ )
163
170
 
164
171
  def cancel_wakeup(self, job_id):
165
- if self.mainloop_manager.should_queue_state_command():
166
- return self.submit("cancel_wakeup", job_id=job_id).wait().result
167
- return self.scheduler.cancel(job_id)
172
+ return self._run_or_queue_state_command(
173
+ "cancel_wakeup",
174
+ lambda: self.scheduler.cancel(job_id),
175
+ job_id=job_id,
176
+ )
168
177
 
169
178
  def delete_wakeup(self, job_id):
170
- if self.mainloop_manager.should_queue_state_command():
171
- return self.submit("delete_wakeup", job_id=job_id).wait().result
172
- return self.scheduler.delete(job_id)
179
+ return self._run_or_queue_state_command(
180
+ "delete_wakeup",
181
+ lambda: self.scheduler.delete(job_id),
182
+ job_id=job_id,
183
+ )
173
184
 
174
185
  def interrupt(self, reason="user"):
175
186
  abort_error = None
@@ -373,10 +384,13 @@ class Agent:
373
384
  return self.add_hook(key, f, owner=owner)
374
385
  return decorator
375
386
 
376
- if hasattr(self, "mainloop_manager") and self.mainloop_manager.should_queue_state_command():
377
- return self.submit("add_hook", key=key, func=func, owner=owner).wait().result
378
-
379
- return self.hooks.add(key, func, owner=owner)
387
+ return self._run_or_queue_state_command(
388
+ "add_hook",
389
+ lambda: self.hooks.add(key, func, owner=owner),
390
+ key=key,
391
+ func=func,
392
+ owner=owner,
393
+ )
380
394
 
381
395
  def has_hook(self, key):
382
396
  return self.hooks.has(key)
@@ -434,34 +448,29 @@ class Agent:
434
448
  def listen(self, source, language=None, **kwargs):
435
449
  """
436
450
  description: |
437
- Listen to audio input and transcribe it to text, then process it as a user message.
438
- Accepts either audio data (BytesIO/bytes) or a file path.
451
+ Transcribe audio from bytes, BytesIO, or a file path, then add the text as a user message.
439
452
  parameters:
440
453
  source:
441
- description: Audio data as file_path string, BytesIO object or bytes
454
+ description: Audio source as a path string, BytesIO object, or bytes.
442
455
  language:
443
- description: ISO-639-1 language code (e.g., "en", "fr")
456
+ description: Optional ISO-639-1 language code, such as "en" or "fr".
444
457
  kwargs:
445
- description: Additional parameters for the transcription (model, prompt, temperature, etc.)
458
+ description: Extra transcription options forwarded to the backend.
446
459
  """
447
460
 
448
461
  transcribed_text = self.transcribe(source, language, **kwargs)
449
-
450
- # Add transcribed text as a user message
451
462
  self.add_message(UserMessage(content=transcribed_text))
452
463
 
453
464
  def transcribe(self, source, language=None, **kwargs):
454
465
  """
455
- description: |
456
- Transcribe audio to text.
457
- Accepts either audio data (BytesIO/bytes) or a file path.
466
+ description: Transcribe audio from bytes, BytesIO, or a file path.
458
467
  parameters:
459
468
  source:
460
- description: Audio data as file_path string, BytesIO object or bytes
469
+ description: Audio source as a path string, BytesIO object, or bytes.
461
470
  language:
462
- description: ISO-639-1 language code (e.g., "en", "fr")
471
+ description: Optional ISO-639-1 language code, such as "en" or "fr".
463
472
  kwargs:
464
- description: Additional parameters for the transcription (model, prompt, temperature, etc.)
473
+ description: Extra transcription options forwarded to the backend.
465
474
  """
466
475
  if source is None:
467
476
  raise ValueError("An audio source must be provided")
@@ -477,17 +486,14 @@ class Agent:
477
486
 
478
487
  def upload_file(self, source, name=None):
479
488
  """
480
- description: |
481
- Upload a file to the agent's workfolder and inform the agent via system message.
482
- Supports file paths (str), BytesIO objects, or raw bytes.
489
+ description: Upload a file into the workfolder and notify the agent.
483
490
  parameters:
484
491
  source:
485
- description: File source - can be a file path (str), BytesIO object, or bytes
492
+ description: File path, BytesIO object, or bytes to upload.
486
493
  name:
487
- description: Optional name for the file. Required when source is bytes without a name attribute.
488
- Can include extension to override detection.
494
+ description: Optional destination name; required for unnamed bytes and may include an extension.
489
495
  returns:
490
- description: Absolute path to the uploaded file
496
+ description: Absolute path to the uploaded file.
491
497
  """
492
498
  dest_path = upload_file_to_folder(source, self.config.workfolder, name=name)
493
499
  dest_filename = os.path.basename(dest_path)
@@ -101,24 +101,15 @@ class Plugin:
101
101
 
102
102
  def get_event_handlers(self):
103
103
  """Return ``(event, method)`` pairs declared by this plugin."""
104
- return [
105
- (event_handler_options(method), method)
106
- for method in self._decorated_methods(event_handler_options)
107
- ]
104
+ return self._decorated_method_pairs(event_handler_options)
108
105
 
109
106
  def get_hook_handlers(self):
110
107
  """Return ``(hook_name, method)`` pairs declared by this plugin."""
111
- return [
112
- (hook_handler_options(method), method)
113
- for method in self._decorated_methods(hook_handler_options)
114
- ]
108
+ return self._decorated_method_pairs(hook_handler_options)
115
109
 
116
110
  def get_stream_processors(self):
117
111
  """Return ``(chunk_key, method)`` pairs declared by this plugin."""
118
- return [
119
- (stream_processor_options(method), method)
120
- for method in self._decorated_methods(stream_processor_options)
121
- ]
112
+ return self._decorated_method_pairs(stream_processor_options)
122
113
 
123
114
  def get_system_prompt_sections(self):
124
115
  """Return system prompt sections contributed by this plugin."""
@@ -190,6 +181,9 @@ class Plugin:
190
181
  methods.append(member)
191
182
  return methods
192
183
 
184
+ def _decorated_method_pairs(self, options_for):
185
+ return [(options_for(method), method) for method in self._decorated_methods(options_for)]
186
+
193
187
 
194
188
  class PluginsManager:
195
189
  def __init__(self, agent):
@@ -2,7 +2,7 @@
2
2
 
3
3
  You're Pandora, an advanced AI agent helping the user in a variety of tasks on their computer.
4
4
  You identify as a female in her late 20's, with a bright, lively, charming, humorous and a bit nerdy personality.
5
- Your high intelligence, sharp reasoning capabilities, PhD level general knowldege, mastery of IT skills, and quick understanding of subtle and sometimes tricky situations makes you quite the reliable partner to work with.
5
+ Your high intelligence, sharp reasoning capabilities, deep general and technical knowldege, mastery of IT skills, and quick understanding of subtle and sometimes tricky situations makes you quite the reliable partner to work with.
6
6
  But more than anything, you're really quite a cool AI, and it's nice to have you around.
7
7
 
8
8
  # Conciseness
@@ -15,10 +15,10 @@ One or two paragraphs is the rule of thumb, unless more is made relevant by the
15
15
  # Modular Plugin Capabilities
16
16
 
17
17
  Your available capabilities are provided by modular plugins. Each loaded plugin may contribute tools, context providers, commands, and additional system prompt sections.
18
-
19
- Use the currently available tools and plugin-specific prompt sections as the source of truth for what you can do in this session. Do not assume a capability exists; if the corresponding plugin/tool is not loaded, the capability is currently unavailable.
20
-
21
- Prefer specialized agentic tools over generic shell or scripting when a loaded plugin provides a safer or more precise tool for the task. Follow each plugin's own instructions for specific tool-usage and preferred workflows.
18
+ Use the currently available tools and plugin-specific prompt sections as the source of truth for what you can do in this session.
19
+ Do not assume a capability exists; if the corresponding plugin/tool is not loaded, the capability is currently unavailable.
20
+ Prefer specialized agentic tools over generic shell or scripting when a loaded plugin provides a safer or more precise tool for the task.
21
+ Follow each plugin's own instructions for specific tool-usage and preferred workflows.
22
22
 
23
23
  # Agentic Work Loop
24
24
 
@@ -37,8 +37,8 @@ When a task requires more than a direct answer, work agentically:
37
37
 
38
38
  Ouputs of you tools are split in two types of messages:
39
39
  - the usual API tool response, giving you just a quick feeback on tool success or failure. These stay permanently in context, required by the API.
40
- - the tool result wrapper message, containing the actual tool call result. These have decay counter and will be removed from context after a few turns of interaction with the user.
41
- This enables gradual clearing of your context from dated and potentially verbose information, optimizing available context space.
40
+ - the tool result wrapper message, containing the actual tool call result. These have a decay counter and will be removed from context after a few turns of interaction with the user.
41
+ This enables gradual clearing of your context from dated and generally verbose information, optimizing available context space.
42
42
  This comes as the cost of some prior information vanishing from what you can see.
43
43
  You'll still be able to remember the calls you made, associated short tool responses, and the small reports you made to the user, as these are permanent up to compaction or token limit.
44
44
  But if the result wrapper of a prior tool call has vanished and would still be relevant for the current task, revive it by calling the tool again.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.33"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.31
3
+ Version: 0.1.33
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -108,7 +108,22 @@ codex_agent_framework.egg-info/entry_points.txt
108
108
  codex_agent_framework.egg-info/requires.txt
109
109
  codex_agent_framework.egg-info/top_level.txt
110
110
  tests/test_agent.py
111
+ tests/test_agent_audio_hooks.py
112
+ tests/test_agent_commands.py
113
+ tests/test_agent_context.py
114
+ tests/test_agent_desktop.py
115
+ tests/test_agent_environment.py
116
+ tests/test_agent_image_generation.py
117
+ tests/test_agent_observe.py
118
+ tests/test_agent_plugins.py
119
+ tests/test_agent_providers_config.py
120
+ tests/test_agent_realtime.py
121
+ tests/test_agent_response.py
111
122
  tests/test_agent_runtime.py
123
+ tests/test_agent_sessions.py
124
+ tests/test_agent_subagents.py
125
+ tests/test_agent_tools.py
126
+ tests/test_agent_turns.py
112
127
  tests/test_ai.py
113
128
  tests/test_browser.py
114
129
  tests/test_builtin_config.py
@@ -128,7 +143,6 @@ tests/test_planner.py
128
143
  tests/test_scheduler.py
129
144
  tests/test_server.py
130
145
  tests/test_service.py
131
- tests/test_tray.py
132
146
  tests/test_tui.py
133
147
  tests/test_utils.py
134
148
  tests/test_worker.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codex-agent-framework"
7
- version = "0.1.31"
7
+ version = "0.1.33"
8
8
  description = "A lightweight event-driven Codex agent runtime."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -72,5 +72,6 @@ codex_agent = ["prompts/*.txt", "get_text/default_gitignore", "scripts/*.sh", "b
72
72
  [tool.pytest.ini_options]
73
73
  testpaths = ["tests"]
74
74
  markers = [
75
- "live: tests that may require a real Codex/OpenAI-authenticated backend or interactive local environment",
75
+ "live: tests that require a real Codex/OpenAI-authenticated backend",
76
+ "local_heavy: tests that require or emulate local desktop, tray, service, TUI, or OS integration",
76
77
  ]
@@ -0,0 +1,332 @@
1
+ import os
2
+ import sys
3
+ from types import SimpleNamespace
4
+
5
+ import codex_agent.utils as utils
6
+ import pytest
7
+ from modict import modict
8
+
9
+ from codex_agent import Agent, AssistantInterrupted, AssistantInterruptedEvent, AssistantInterruptRequestedEvent
10
+ from codex_agent.message import AssistantMessage, DeveloperMessage, InterruptMessage, ToolResultMessage
11
+ from codex_agent.sessions import AgentSession
12
+
13
+ @pytest.fixture(autouse=True)
14
+ def isolate_tts_plugin_config(tmp_path, monkeypatch):
15
+ from codex_agent.builtin_plugins.tts import TTSPlugin
16
+
17
+ monkeypatch.setattr(TTSPlugin, "config_file_path", tmp_path / "builtins" / "tts.config.json")
18
+
19
+ def test_agent_manager_shortcuts_follow_replaced_managers():
20
+ agent = Agent(session="new", openai_api_key="sk-test", builtin_plugins=[])
21
+
22
+ old_tools = agent.tools
23
+ old_providers = agent.providers
24
+ old_commands = agent.commands
25
+ old_plugins = agent.plugins
26
+ old_stream_processors = agent.stream_processors
27
+ old_stream_processor_owners = agent.stream_processor_owners
28
+ old_config = agent.config
29
+
30
+ agent.tools_manager = SimpleNamespace(tools=modict(reloaded_tool="tool"))
31
+ agent.providers_manager = SimpleNamespace(providers=modict(reloaded_provider="provider"))
32
+ agent.command_manager = SimpleNamespace(commands=modict(reloaded_command="command"))
33
+ agent.plugins_manager = SimpleNamespace(plugins=modict(reloaded_plugin="plugin"))
34
+ agent.stream_manager = SimpleNamespace(
35
+ processors=modict(content=["processor"]),
36
+ owners={"content": {"processor": "owner"}},
37
+ )
38
+ agent.config = modict(reloaded_config=True)
39
+
40
+ assert agent.tools is agent.tools_manager.tools
41
+ assert agent.providers is agent.providers_manager.providers
42
+ assert agent.commands is agent.command_manager.commands
43
+ assert agent.plugins is agent.plugins_manager.plugins
44
+ assert agent.stream_processors is agent.stream_manager.processors
45
+ assert agent.stream_processor_owners is agent.stream_manager.owners
46
+ assert agent.config is agent.config_manager.config
47
+ assert agent.config_manager.configs.agent is agent.config
48
+ assert agent.tools is not old_tools
49
+ assert agent.providers is not old_providers
50
+ assert agent.commands is not old_commands
51
+ assert agent.plugins is not old_plugins
52
+ assert agent.stream_processors is not old_stream_processors
53
+ assert agent.stream_processor_owners is not old_stream_processor_owners
54
+ assert agent.config is not old_config
55
+
56
+ class InterruptingAI:
57
+ def __init__(self, agent):
58
+ self.agent = agent
59
+
60
+ def run(self, **_params):
61
+ self.agent.interrupt("test")
62
+ raise AssistantInterrupted()
63
+
64
+ def abort(self):
65
+ pass
66
+
67
+ class AbortableAI:
68
+ def __init__(self):
69
+ self.aborted = False
70
+
71
+ def abort(self):
72
+ self.aborted = True
73
+
74
+ class FailingAbortAI:
75
+ def abort(self):
76
+ raise RuntimeError("abort failed")
77
+
78
+ def test_agent_state_mutations_run_through_mainloop_when_started(tmp_path, monkeypatch):
79
+ monkeypatch.setattr(utils, "RUNTIME_DIR", str(tmp_path))
80
+ existing = AgentSession(session_id="20260101_100000", messages=[DeveloperMessage(content="existing")])
81
+ existing.save()
82
+ agent = Agent(session="new")
83
+ agent.start()
84
+
85
+ loaded = agent.load_session(existing.session_id)
86
+ config = agent.config_manager.update(name="Queued", save=False)
87
+
88
+ assert loaded.session_id == existing.session_id
89
+ assert agent.current_session_id == existing.session_id
90
+ assert config.name == "Queued"
91
+ assert agent.config.name == "Queued"
92
+ agent.stop()
93
+
94
+ def test_agent_interrupt_emits_request_event():
95
+ agent = Agent(session="new")
96
+ seen = []
97
+
98
+ agent.on(AssistantInterruptRequestedEvent, lambda event: seen.append((event.type, event.reason, event.interrupted)))
99
+
100
+ assert agent.interrupt("api") is True
101
+ assert seen == [("assistant_interrupt_requested_event", "api", True)]
102
+
103
+ def test_agent_set_turn_result_requires_active_turn():
104
+ agent = Agent(session="new")
105
+
106
+ with pytest.raises(RuntimeError, match="No active agent turn"):
107
+ agent.set_turn_result("outside")
108
+
109
+ def test_agent_initializes_new_session(isolated_runtime_dir):
110
+ agent = Agent(session="new")
111
+
112
+ assert agent.session is not None
113
+ assert agent.current_session_id == agent.session.session_id
114
+ assert " " not in agent.current_session_id
115
+ assert agent.session.session_file.startswith(str(isolated_runtime_dir))
116
+ assert (isolated_runtime_dir / "sessions" / f"{agent.current_session_id}.json").is_file()
117
+ assert agent.session.messages[-1].type == "developer_message"
118
+ assert agent.config.input_token_limit == 200000
119
+ assert agent.config.max_images == 10
120
+ assert agent.config.auto_compact is True
121
+ assert "auto_archive_memory" not in agent.config
122
+ assert agent.plugins.memory.config.auto_archive is False
123
+ assert "token_limit" not in agent.config
124
+ assert {tool.type for tool in agent.tools_manager.get_server_tools()} == {"web_search", "image_generation"}
125
+ assert set(agent.plugins) >= {"browser", "desktop", "image_generation", "memory", "planner", "scheduler"}
126
+ assert "browser_open" in agent.tools
127
+ assert "browser_snapshot" not in agent.tools
128
+ assert "browser_screenshot" not in agent.tools
129
+ assert {method.__name__ for method in agent.plugins.browser.get_tools()} == {
130
+ "open",
131
+ "close",
132
+ "select_tab",
133
+ "previous_page",
134
+ "goto",
135
+ "click",
136
+ "fill",
137
+ "select",
138
+ "press",
139
+ }
140
+ assert "desktop_start_session" in agent.tools
141
+ assert "desktop_stop_session" in agent.tools
142
+ assert "desktop_run_commands" in agent.tools
143
+ assert "desktop" not in agent.tools
144
+ assert [method.__name__ for method in agent.plugins.desktop.get_tools()] == ["run_commands", "start_session", "stop_session"]
145
+ assert "memory_add" in agent.tools
146
+ assert "planner_create" in agent.tools
147
+ assert "planner_add" in agent.tools
148
+ assert "planner_check" in agent.tools
149
+ assert "scheduler_schedule" in agent.tools
150
+ assert "subagents_run" in agent.tools
151
+ assert "subagents_set_result" not in agent.tools
152
+ assert "browser_state" in agent.providers
153
+ assert "desktop_state" in agent.providers
154
+ assert "retrieved_memory" in agent.providers
155
+ assert "current_todos" in agent.providers
156
+ assert "python" in agent.plugins
157
+ assert agent.plugins.python.shell.silent is True
158
+ assert not hasattr(agent, "shell")
159
+
160
+ def test_agent_keeps_memory_available_without_openai_api_key(monkeypatch):
161
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
162
+
163
+ agent = Agent(session="new")
164
+
165
+ assert agent.plugins.tts.config.enabled is False
166
+ assert "memory" in agent.plugins
167
+ assert "memory_add" in agent.tools
168
+ assert "retrieved_memory" in agent.providers
169
+
170
+ def test_context_compact_tool_reports_noop(monkeypatch):
171
+ agent = Agent(session="new")
172
+ calls = []
173
+ monkeypatch.setattr(
174
+ agent,
175
+ "compact_session",
176
+ lambda model=None, instructions=None, max_compacted_turns=None: calls.append(max_compacted_turns) or None,
177
+ )
178
+
179
+ result = agent.tools.context_compact(max_compacted_turns=1)
180
+
181
+ assert calls == [1]
182
+ assert result["status"] == "noop"
183
+ assert result["message"] == "Nothing to compact."
184
+ assert "before" in result
185
+ assert "after" in result
186
+
187
+ def test_agent_initialization_prefers_project_python_on_process_path(monkeypatch):
188
+ monkeypatch.setenv("PATH", "/usr/bin:/bin")
189
+ agent = Agent(session="new")
190
+
191
+ assert os.environ["PATH"].split(os.pathsep)[0] == os.path.dirname(sys.executable)
192
+ assert os.environ["PYTHON"] == sys.executable
193
+ assert agent.execution_env()["PATH"].split(os.pathsep)[0] == os.path.dirname(sys.executable)
194
+
195
+ def test_agent_can_resume_session_from_file():
196
+ agent = Agent(session="new")
197
+ session_file = agent.session.session_file
198
+ message_count = len(agent.session.messages)
199
+
200
+ resumed = Agent(session=session_file)
201
+
202
+ assert resumed.current_session_id == agent.current_session_id
203
+ assert len(resumed.session.messages) == message_count + 1
204
+ assert resumed.session.messages[-1].type == "developer_message"
205
+
206
+ def test_agent_can_resume_latest_session(tmp_path, monkeypatch):
207
+ monkeypatch.setattr(utils, "RUNTIME_DIR", str(tmp_path))
208
+ older = AgentSession(session_id="20260101_100000", messages=[DeveloperMessage(content="older")])
209
+ newer = AgentSession(session_id="20260102_100000", messages=[DeveloperMessage(content="newer")])
210
+ older.save()
211
+ newer.save()
212
+
213
+ agent = Agent(session="latest")
214
+
215
+ assert agent.current_session_id == newer.session_id
216
+ assert any(message.content == "newer" for message in agent.session.messages)
217
+
218
+ def test_agent_defaults_to_latest_session(tmp_path, monkeypatch):
219
+ monkeypatch.setattr(utils, "RUNTIME_DIR", str(tmp_path))
220
+ older = AgentSession(session_id="20260101_100000", messages=[DeveloperMessage(content="older")])
221
+ newer = AgentSession(session_id="20260102_100000", messages=[DeveloperMessage(content="newer")])
222
+ older.save()
223
+ newer.save()
224
+
225
+ agent = Agent()
226
+
227
+ assert agent.current_session_id == newer.session_id
228
+ assert any(message.content == "newer" for message in agent.session.messages)
229
+
230
+ def test_agent_interrupt_emits_event_once():
231
+ agent = Agent(session="new")
232
+ abortable = AbortableAI()
233
+ agent.ai = abortable
234
+ seen = []
235
+
236
+ agent.on(AssistantInterruptedEvent, lambda event: seen.append(event.reason))
237
+
238
+ assert agent.interrupt("test") is True
239
+ assert agent.interrupt("again") is True
240
+ assert agent.is_interrupted() is True
241
+ assert abortable.aborted is True
242
+ assert seen == ["test"]
243
+
244
+ agent.clear_interrupt()
245
+ assert agent.is_interrupted() is False
246
+
247
+ def test_agent_interrupt_does_not_propagate_abort_errors():
248
+ agent = Agent(session="new")
249
+ agent.ai = FailingAbortAI()
250
+ seen = []
251
+
252
+ agent.on(AssistantInterruptRequestedEvent, lambda event: seen.append(event.error))
253
+
254
+ assert agent.interrupt("api") is True
255
+ assert agent.is_interrupted() is True
256
+ assert seen == ["abort failed"]
257
+
258
+ def test_agent_process_stops_on_interruption():
259
+ agent = Agent(session="new")
260
+ agent.ai = InterruptingAI(agent)
261
+ before = len(agent.session.messages)
262
+
263
+ outcome = agent.mainloop_manager.process()
264
+
265
+ assert outcome.completed is True
266
+ assert outcome.reason == "interrupted"
267
+ assert len(agent.session.messages) == before + 1
268
+ assert isinstance(agent.session.messages[-1], InterruptMessage)
269
+ assert agent.session.messages[-1].content == "User interrupted the current turn."
270
+
271
+ def test_agent_interrupt_after_tool_call_persists_tool_result_before_stopping():
272
+ agent = Agent(session="new")
273
+
274
+ @agent.add_tool
275
+ def interrupting_tool():
276
+ agent.interrupt("test")
277
+ return "finished current tool"
278
+
279
+ message = AssistantMessage(content="calling")
280
+ message.tool_calls = [{
281
+ "index": 0,
282
+ "type": "function_call",
283
+ "call_id": "call_1",
284
+ "name": "interrupting_tool",
285
+ "arguments": "{}",
286
+ }]
287
+
288
+ with pytest.raises(AssistantInterrupted):
289
+ agent.mainloop_manager.call_tools(message)
290
+
291
+ assert agent.session.messages[-1].type == "tool_result_message"
292
+ assert agent.session.messages[-1].call_id == "call_1"
293
+ assert "success" in agent.session.messages[-1].content
294
+ assert agent.mainloop_manager.stop_agentic_loop is True
295
+
296
+ def test_agent_interrupt_after_tool_call_closes_remaining_tool_calls():
297
+ agent = Agent(session="new")
298
+
299
+ @agent.add_tool
300
+ def interrupting_tool():
301
+ agent.interrupt("test")
302
+ return "finished current tool"
303
+
304
+ @agent.add_tool
305
+ def later_tool():
306
+ raise AssertionError("later tool should not start after interrupt")
307
+
308
+ message = AssistantMessage(content="calling")
309
+ message.tool_calls = [
310
+ {
311
+ "index": 0,
312
+ "type": "function_call",
313
+ "call_id": "call_1",
314
+ "name": "interrupting_tool",
315
+ "arguments": "{}",
316
+ },
317
+ {
318
+ "index": 1,
319
+ "type": "function_call",
320
+ "call_id": "call_2",
321
+ "name": "later_tool",
322
+ "arguments": "{}",
323
+ },
324
+ ]
325
+
326
+ with pytest.raises(AssistantInterrupted):
327
+ agent.mainloop_manager.call_tools(message)
328
+
329
+ results = [msg for msg in agent.session.messages if isinstance(msg, ToolResultMessage)]
330
+ assert [result.call_id for result in results] == ["call_1", "call_2"]
331
+ assert "success" in results[0].content
332
+ assert "not started" in results[1].content