omnimancer-cli 0.2.7__tar.gz → 0.2.10__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 (149) hide show
  1. {omnimancer_cli-0.2.7/omnimancer_cli.egg-info → omnimancer_cli-0.2.10}/PKG-INFO +5 -2
  2. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/README.md +4 -1
  3. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/approval_integration.py +44 -19
  4. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/command_dispatch.py +297 -29
  5. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/commands.py +5 -1
  6. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/headless.py +63 -22
  7. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/interface.py +159 -32
  8. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/tool_handler.py +66 -3
  9. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/chat_manager.py +18 -4
  10. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/engine.py +257 -8
  11. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/models.py +114 -1
  12. omnimancer_cli-0.2.10/omnimancer/core/rate_limit_fallback.py +245 -0
  13. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/base.py +10 -0
  14. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/openai.py +53 -8
  15. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10/omnimancer_cli.egg-info}/PKG-INFO +5 -2
  16. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer_cli.egg-info/SOURCES.txt +2 -0
  17. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/pyproject.toml +1 -1
  18. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_approval_preview_format.py +72 -1
  19. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_core_engine.py +186 -0
  20. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_models.py +65 -0
  21. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_openai_provider_tools.py +141 -0
  22. omnimancer_cli-0.2.10/tests/test_rate_limit_fallback.py +578 -0
  23. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_tool_calling_integration.py +244 -11
  24. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_tool_handler.py +82 -0
  25. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/LICENSE +0 -0
  26. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/__init__.py +0 -0
  27. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/__main__.py +0 -0
  28. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/__init__.py +0 -0
  29. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/agent_loop.py +0 -0
  30. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/approval_formatter.py +0 -0
  31. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/approval_prompt.py +0 -0
  32. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/completion.py +0 -0
  33. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/display.py +0 -0
  34. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/dynamic_commands.py +0 -0
  35. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/handlers/__init__.py +0 -0
  36. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/subagent.py +0 -0
  37. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/cli/system_prompts.py +0 -0
  38. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/__init__.py +0 -0
  39. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/__init__.py +0 -0
  40. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/approval_context.py +0 -0
  41. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/approval_interface.py +0 -0
  42. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/approval_manager.py +0 -0
  43. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/config.py +0 -0
  44. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/file_system_manager.py +0 -0
  45. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/program_executor.py +0 -0
  46. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/read_before_write_errors.py +0 -0
  47. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/status_core.py +0 -0
  48. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/status_manager.py +0 -0
  49. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/tool_definitions.py +0 -0
  50. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/types.py +0 -0
  51. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/web_client.py +0 -0
  52. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
  53. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent_engine.py +0 -0
  54. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent_managers.py +0 -0
  55. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/agent_mode_manager.py +0 -0
  56. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/config_manager.py +0 -0
  57. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/config_migration.py +0 -0
  58. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/config_validator.py +0 -0
  59. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/conversation_manager.py +0 -0
  60. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/env_loader.py +0 -0
  61. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/fallback_manager.py +0 -0
  62. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/health_monitor.py +0 -0
  63. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/history_manager.py +0 -0
  64. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/hooks.py +0 -0
  65. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/mcp_integration_layer.py +0 -0
  66. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/provider_capabilities.py +0 -0
  67. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/provider_initializer.py +0 -0
  68. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/provider_registry.py +0 -0
  69. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/security/__init__.py +0 -0
  70. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/security/approval_workflow.py +0 -0
  71. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/security/audit_logger.py +0 -0
  72. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/security/permission_controller.py +0 -0
  73. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/security/permission_rules.py +0 -0
  74. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/security/sandbox_manager.py +0 -0
  75. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/security/security_manager.py +0 -0
  76. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/core/signal_handler.py +0 -0
  77. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/mcp/__init__.py +0 -0
  78. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/mcp/client.py +0 -0
  79. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/mcp/manager.py +0 -0
  80. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/__init__.py +0 -0
  81. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/azure.py +0 -0
  82. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/bedrock.py +0 -0
  83. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/claude.py +0 -0
  84. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/claude_code.py +0 -0
  85. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/claude_credentials.py +0 -0
  86. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/cohere.py +0 -0
  87. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/digitalocean.py +0 -0
  88. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/factory.py +0 -0
  89. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/gemini.py +0 -0
  90. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/mistral.py +0 -0
  91. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/ollama.py +0 -0
  92. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/openrouter.py +0 -0
  93. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/perplexity.py +0 -0
  94. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/vertex.py +0 -0
  95. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/providers/xai.py +0 -0
  96. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/ui/__init__.py +0 -0
  97. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/ui/cancellation_handler.py +0 -0
  98. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/ui/progress_indicator.py +0 -0
  99. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/ui/streaming_display.py +0 -0
  100. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/utils/__init__.py +0 -0
  101. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/utils/error_handler.py +0 -0
  102. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/utils/errors.py +0 -0
  103. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer/utils/retry.py +0 -0
  104. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
  105. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer_cli.egg-info/entry_points.txt +0 -0
  106. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer_cli.egg-info/requires.txt +0 -0
  107. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/omnimancer_cli.egg-info/top_level.txt +0 -0
  108. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/setup.cfg +0 -0
  109. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_approval_integration_compatibility.py +0 -0
  110. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_approval_remember.py +0 -0
  111. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_approval_system.py +0 -0
  112. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_azure_provider.py +0 -0
  113. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_claude_credentials.py +0 -0
  114. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_claude_provider.py +0 -0
  115. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_cohere_provider.py +0 -0
  116. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_command_approval_security.py +0 -0
  117. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_config_comprehensive.py +0 -0
  118. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_conversation_history.py +0 -0
  119. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_conversation_manager.py +0 -0
  120. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_digitalocean_provider.py +0 -0
  121. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_enhanced_model_info.py +0 -0
  122. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_env_loader.py +0 -0
  123. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_error_handling_robustness.py +0 -0
  124. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_execution_timeout_errors.py +0 -0
  125. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_fallback_manager.py +0 -0
  126. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_fixtures.py +0 -0
  127. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_gemini_provider.py +0 -0
  128. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_hooks.py +0 -0
  129. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_httpx_compatibility.py +0 -0
  130. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_import_validation.py +0 -0
  131. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_interface_smoke_tests.py +0 -0
  132. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_mcp_integration.py +0 -0
  133. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_mcp_integration_layer.py +0 -0
  134. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_mistral_provider.py +0 -0
  135. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_ollama_provider.py +0 -0
  136. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_openai_context_fit.py +0 -0
  137. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_permission_rules.py +0 -0
  138. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_perplexity_provider.py +0 -0
  139. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_program_executor.py +0 -0
  140. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_provider_base.py +0 -0
  141. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_provider_capability_consistency.py +0 -0
  142. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_provider_integration.py +0 -0
  143. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_provider_registry.py +0 -0
  144. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_retry.py +0 -0
  145. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_security_validation.py +0 -0
  146. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_system_integration.py +0 -0
  147. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_system_prompts.py +0 -0
  148. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_workflows_comprehensive.py +0 -0
  149. {omnimancer_cli-0.2.7 → omnimancer_cli-0.2.10}/tests/test_xai_provider.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omnimancer-cli
3
- Version: 0.2.7
3
+ Version: 0.2.10
4
4
  Summary: A unified command-line interface for multiple AI language models
5
5
  Author-email: Kellan Strong <kellan@jite.ai>
6
6
  License: MIT
@@ -96,6 +96,9 @@ omn -p "fix the failing tests" --dangerously-skip-permissions
96
96
 
97
97
  # Verbose output / explicit config file
98
98
  omn -p "audit the security module" --verbose --config ~/.omnimancer/config.json
99
+
100
+ # Raise the tool-iteration cap for big jobs (default: 25)
101
+ omn -p "migrate every test file to pytest fixtures" --max-iterations 100
99
102
  ```
100
103
 
101
104
  The `--provider`, `--model`, and `--base-url` flags are **session overrides**: they
@@ -240,7 +243,7 @@ Tool names match the agent's toolset (`Read`, `Write`, `Edit`, `Bash`, `Glob`, `
240
243
  | `/help [command]` | Show help (optionally for a specific command) |
241
244
  | `/quit` | Exit (also: `/exit`, Ctrl+D) |
242
245
  | `/clear` | Clear terminal screen |
243
- | `/switch <provider> [model]` | Switch provider or model |
246
+ | `/switch <provider> [model]` | Switch provider or model (uncataloged models are registered on the fly) |
244
247
  | `/models [filter]` | List available models (alias: `/model`) |
245
248
  | `/providers` | List all providers with status |
246
249
  | `/agent on\|off\|status` | Toggle agent mode |
@@ -45,6 +45,9 @@ omn -p "fix the failing tests" --dangerously-skip-permissions
45
45
 
46
46
  # Verbose output / explicit config file
47
47
  omn -p "audit the security module" --verbose --config ~/.omnimancer/config.json
48
+
49
+ # Raise the tool-iteration cap for big jobs (default: 25)
50
+ omn -p "migrate every test file to pytest fixtures" --max-iterations 100
48
51
  ```
49
52
 
50
53
  The `--provider`, `--model`, and `--base-url` flags are **session overrides**: they
@@ -189,7 +192,7 @@ Tool names match the agent's toolset (`Read`, `Write`, `Edit`, `Bash`, `Glob`, `
189
192
  | `/help [command]` | Show help (optionally for a specific command) |
190
193
  | `/quit` | Exit (also: `/exit`, Ctrl+D) |
191
194
  | `/clear` | Clear terminal screen |
192
- | `/switch <provider> [model]` | Switch provider or model |
195
+ | `/switch <provider> [model]` | Switch provider or model (uncataloged models are registered on the fly) |
193
196
  | `/models [filter]` | List available models (alias: `/model`) |
194
197
  | `/providers` | List all providers with status |
195
198
  | `/agent on\|off\|status` | Toggle agent mode |
@@ -13,7 +13,11 @@ from typing import Any, Dict, List, Optional
13
13
 
14
14
  from rich.console import Console
15
15
 
16
- from ..core.agent.approval_manager import BatchApprovalRequest, EnhancedApprovalManager
16
+ from ..core.agent.approval_manager import (
17
+ BatchApprovalRequest,
18
+ ChangePreview,
19
+ EnhancedApprovalManager,
20
+ )
17
21
  from ..core.agent.types import Operation, OperationType
18
22
  from ..core.security.approval_workflow import ApprovalRequest, RiskLevel
19
23
  from ..core.security.permission_controller import PermissionController
@@ -181,24 +185,7 @@ class CLIApprovalIntegration:
181
185
  risk_level=RiskLevel.MEDIUM, # Default, can be enhanced
182
186
  )
183
187
 
184
- # Create change preview from operation
185
- # Map OperationType to ChangeType (they have matching values)
186
- from ..core.agent.approval_manager import ChangePreview, ChangeType
187
-
188
- try:
189
- change_type = ChangeType(operation.type.value)
190
- except ValueError:
191
- # Default to FILE_MODIFY if type not in ChangeType enum
192
- change_type = ChangeType.FILE_MODIFY
193
-
194
- change_preview = ChangePreview(
195
- change_type=change_type,
196
- description=operation.description,
197
- # Use string preview as proposed_state
198
- proposed_state=operation.preview,
199
- metadata=operation.data,
200
- reversible=operation.reversible,
201
- )
188
+ change_preview = await self._build_change_preview(operation)
202
189
 
203
190
  # Present approval dialog and get user decision
204
191
  decision = await self.prompt_handler.prompt_for_approval(
@@ -226,6 +213,44 @@ class CLIApprovalIntegration:
226
213
  logger.error(f"Error in single approval handler: {e}", exc_info=True)
227
214
  return (False, False)
228
215
 
216
+ async def _build_change_preview(self, operation: Operation) -> ChangePreview:
217
+ """Build the ChangePreview shown in the approval dialog.
218
+
219
+ File writes and deletes go through the approval manager's rich
220
+ generators, which read the current file and produce a real unified
221
+ diff. Other operation types get a generic preview built from the
222
+ operation's own description/preview strings.
223
+ """
224
+ from ..core.agent.approval_manager import ChangePreview, ChangeType
225
+
226
+ if operation.type in (OperationType.FILE_WRITE, OperationType.FILE_DELETE):
227
+ try:
228
+ preview = await self.approval_manager.generate_operation_preview(
229
+ operation
230
+ )
231
+ # The operation's description ("Edit file: ...") is more
232
+ # specific than the generator's generic one.
233
+ if operation.description:
234
+ preview.description = operation.description
235
+ return preview
236
+ except Exception as e:
237
+ logger.debug(f"Rich preview failed, using generic preview: {e}")
238
+
239
+ try:
240
+ change_type = ChangeType(operation.type.value)
241
+ except ValueError:
242
+ # Default to FILE_MODIFY if type not in ChangeType enum
243
+ change_type = ChangeType.FILE_MODIFY
244
+
245
+ return ChangePreview(
246
+ change_type=change_type,
247
+ description=operation.description,
248
+ # Use string preview as proposed_state
249
+ proposed_state=operation.preview,
250
+ metadata=operation.data,
251
+ reversible=operation.reversible,
252
+ )
253
+
229
254
  async def _handle_batch_approval(
230
255
  self, batch_request: BatchApprovalRequest
231
256
  ) -> Dict[str, Any]:
@@ -19,7 +19,7 @@ from rich.prompt import Confirm
19
19
  from rich.table import Table
20
20
 
21
21
  # Internal imports - Core
22
- from ..core.models import EnhancedModelInfo
22
+ from ..core.models import EnhancedModelInfo, FallbackConfig
23
23
  from ..providers.factory import ProviderFactory
24
24
 
25
25
  # Internal imports - CLI
@@ -124,6 +124,8 @@ class CommandDispatchMixin:
124
124
  await self._handle_prompts_command(command)
125
125
  elif slash_cmd == SlashCommand.SUBAGENTS:
126
126
  await self._handle_subagents_command(command)
127
+ elif slash_cmd == SlashCommand.FALLBACK:
128
+ await self._handle_fallback_command(command)
127
129
  elif slash_cmd is not None:
128
130
  self._show_info(f"Command {slash_cmd.value} is not yet implemented")
129
131
 
@@ -284,6 +286,16 @@ class CommandDispatchMixin:
284
286
  except Exception:
285
287
  continue
286
288
 
289
+ # Merge in custom models (added via /add-model or /switch) —
290
+ # they live in config, not in any provider's static catalog.
291
+ try:
292
+ for custom in self.engine.config_manager.get_custom_models():
293
+ bucket = all_models.setdefault(custom.provider, [])
294
+ if not any(m.name == custom.name for m in bucket):
295
+ bucket.append(custom)
296
+ except Exception:
297
+ pass
298
+
287
299
  # Apply filtering
288
300
  if filter_type == "provider" and filter_value:
289
301
  all_models = {
@@ -411,6 +423,36 @@ class CommandDispatchMixin:
411
423
  except Exception as e:
412
424
  return f"Error displaying models: {e}"
413
425
 
426
+ def _register_model_on_the_fly(self, provider_name: str, model_name: str) -> None:
427
+ """Register an uncataloged model as a custom model so /switch works.
428
+
429
+ The entry persists in config, shows up in /models, and can be removed
430
+ with /remove-model if the endpoint turns out to reject it.
431
+ """
432
+ model_info = EnhancedModelInfo(
433
+ name=model_name,
434
+ provider=provider_name,
435
+ description="Added on the fly via /switch",
436
+ max_tokens=4096,
437
+ cost_per_million_input=0.0,
438
+ cost_per_million_output=0.0,
439
+ swe_score=50.0,
440
+ available=True,
441
+ supports_tools=True,
442
+ supports_multimodal=False,
443
+ latest_version=False,
444
+ deprecated=False,
445
+ release_date=datetime.now(),
446
+ context_window=4096,
447
+ is_free=False,
448
+ )
449
+ self.engine.config_manager.add_custom_model(model_info)
450
+ self._show_warning(
451
+ f"Model '{model_name}' isn't in the {provider_name} catalog — "
452
+ "registered it as a custom model and switching anyway. "
453
+ f"(/remove-model {model_name} {provider_name} to undo)"
454
+ )
455
+
414
456
  async def _handle_switch_command(self, command: Command) -> None:
415
457
  """Handle switch command with enhanced provider type support."""
416
458
  args = command.args
@@ -484,34 +526,10 @@ class CommandDispatchMixin:
484
526
  all_model_names = model_names + custom_model_names
485
527
 
486
528
  if model_name not in all_model_names:
487
- self._show_error(
488
- f"Model '{model_name}' not"
489
- " available for provider"
490
- f" '{provider_name}'."
491
- )
492
- if all_model_names:
493
- # Show suggestions for similar model names
494
- suggestions = [
495
- m
496
- for m in all_model_names
497
- if model_name.lower() in m.lower()
498
- or m.lower() in model_name.lower()
499
- ]
500
- if suggestions:
501
- models_str = ", ".join(suggestions[:5])
502
- self._show_info(
503
- "Available models for"
504
- f" {provider_name}:"
505
- f" {models_str}"
506
- )
507
- else:
508
- models_str = ", ".join(all_model_names[:5])
509
- self._show_info(
510
- "Available models for"
511
- f" {provider_name}:"
512
- f" {models_str}"
513
- )
514
- return
529
+ # Static catalogs are forever stale and the endpoint
530
+ # accepts any model string — register it on the fly
531
+ # instead of refusing.
532
+ self._register_model_on_the_fly(provider_name, model_name)
515
533
 
516
534
  success = await self.engine.switch_model(provider_name, model_name)
517
535
 
@@ -1842,3 +1860,253 @@ Newest Entry: {stats['newest_entry'] or 'None'}"""
1842
1860
 
1843
1861
  except Exception as e:
1844
1862
  self._show_error(f"Failed to list custom models: {e}")
1863
+
1864
+ # ------------------------------------------------------------------
1865
+ # /fallback
1866
+ # ------------------------------------------------------------------
1867
+
1868
+ # --- helpers ----------------------------------------------------------
1869
+
1870
+ @staticmethod
1871
+ def _validate_toggle_arg(arg: str) -> Optional[bool]:
1872
+ """Return True/False for 'on'/'off', or None if the value is invalid.
1873
+
1874
+ Centralises validation of boolean toggle arguments so individual
1875
+ subhandlers don't duplicate the same ``if toggle not in ("on", "off")``
1876
+ check.
1877
+ """
1878
+ if arg == "on":
1879
+ return True
1880
+ if arg == "off":
1881
+ return False
1882
+ return None
1883
+
1884
+ def _persist_fallback_config(self, fb: FallbackConfig) -> None:
1885
+ """Persist *fb* to disk and immediately sync the engine.
1886
+
1887
+ This single method replaces the repeated trio of:
1888
+ ``config.fallback = fb``
1889
+ ``self.engine.config_manager.save_config()``
1890
+ ``self.engine.configure_fallback()``
1891
+ across every subcommand handler.
1892
+ """
1893
+ config = self.engine.config_manager.get_config()
1894
+ config.fallback = fb
1895
+ self.engine.config_manager.save_config()
1896
+ # Sync the engine's runtime handler immediately so changes take effect
1897
+ # without requiring a restart.
1898
+ self.engine.configure_fallback()
1899
+
1900
+ # --- dispatcher -------------------------------------------------------
1901
+
1902
+ async def _handle_fallback_command(self, command: Command) -> None:
1903
+ """Handle the /fallback command family.
1904
+
1905
+ Sub-commands
1906
+ ------------
1907
+ /fallback show current fallback configuration
1908
+ /fallback status same as above
1909
+ /fallback auto [on|off] toggle or query auto-fallback
1910
+ /fallback order <p1> <p2> … set the ordered fallback provider list
1911
+ /fallback order clear clear the fallback order (use any)
1912
+ /fallback on-rate-limit [on|off] toggle fallback on 429 errors
1913
+ /fallback on-quota [on|off] toggle fallback on quota errors
1914
+ /fallback help show this help text
1915
+ """
1916
+ try:
1917
+ config = self.engine.config_manager.get_config()
1918
+ # Use a fresh FallbackConfig() default if the config object does
1919
+ # not yet have a fallback section (e.g., existing installs).
1920
+ fb: FallbackConfig = getattr(config, "fallback", FallbackConfig())
1921
+
1922
+ args = list(command.args) if command.args else []
1923
+ sub = args[0].lower() if args else "status"
1924
+
1925
+ dispatch: dict[str, Any] = {
1926
+ "status": lambda: self._show_fallback_status(fb),
1927
+ "": lambda: self._show_fallback_status(fb),
1928
+ "help": self._show_fallback_help,
1929
+ "auto": lambda: self._handle_auto_subcommand(fb, args),
1930
+ "order": lambda: self._handle_order_subcommand(fb, args),
1931
+ "on-rate-limit": lambda: self._handle_on_rate_limit_subcommand(
1932
+ fb, args
1933
+ ),
1934
+ "on-quota": lambda: self._handle_on_quota_subcommand(fb, args),
1935
+ }
1936
+
1937
+ handler = dispatch.get(sub)
1938
+ if handler is None:
1939
+ self._show_error(
1940
+ f"Unknown sub-command '{sub}'. "
1941
+ "Run [cyan]/fallback help[/cyan] for usage."
1942
+ )
1943
+ else:
1944
+ result = handler()
1945
+ # Subhandlers may be coroutines in the future; await if needed.
1946
+ if asyncio.iscoroutine(result):
1947
+ await result
1948
+
1949
+ except ValueError as exc:
1950
+ # Raised by subhandlers for invalid user input.
1951
+ self._show_error(str(exc))
1952
+ except Exception as exc:
1953
+ logger.debug("Fallback command error", exc_info=True)
1954
+ self._show_error(f"Failed to update fallback config: {exc}")
1955
+
1956
+ # --- subhandlers ------------------------------------------------------
1957
+
1958
+ def _handle_auto_subcommand(self, fb: FallbackConfig, args: list[str]) -> None:
1959
+ """Handle ``/fallback auto [on|off]``."""
1960
+ if len(args) < 2:
1961
+ state = "on" if fb.auto_fallback else "off"
1962
+ self._show_info(
1963
+ f"Auto-fallback is currently [bold]{state}[/bold]. "
1964
+ f"Use [cyan]/fallback auto on[/cyan] or "
1965
+ f"[cyan]/fallback auto off[/cyan] to change."
1966
+ )
1967
+ return
1968
+
1969
+ enabled = self._validate_toggle_arg(args[1].lower())
1970
+ if enabled is None:
1971
+ raise ValueError("Usage: /fallback auto [on|off]")
1972
+
1973
+ fb.auto_fallback = enabled
1974
+ self._persist_fallback_config(fb)
1975
+ label = "enabled" if enabled else "disabled"
1976
+ self._show_success(f"Auto-fallback {label}.")
1977
+
1978
+ def _handle_order_subcommand(self, fb: FallbackConfig, args: list[str]) -> None:
1979
+ """Handle ``/fallback order [<p1> <p2> … | clear]``."""
1980
+ if len(args) < 2:
1981
+ order_str = " → ".join(fb.fallback_order) if fb.fallback_order else "(any)"
1982
+ self._show_info(f"Current fallback order: {order_str}")
1983
+ return
1984
+
1985
+ if args[1].lower() == "clear":
1986
+ fb.fallback_order = []
1987
+ self._persist_fallback_config(fb)
1988
+ self._show_success(
1989
+ "Fallback order cleared (will use any available provider)."
1990
+ )
1991
+ return
1992
+
1993
+ # Validate that each name is a known provider.
1994
+ known = set(self.engine.providers.keys())
1995
+ new_order: list[str] = []
1996
+ unknown: list[str] = []
1997
+ for name in args[1:]:
1998
+ if name in known:
1999
+ new_order.append(name)
2000
+ else:
2001
+ unknown.append(name)
2002
+
2003
+ if unknown:
2004
+ self._show_warning(
2005
+ f"Unknown provider(s) skipped: {', '.join(unknown)}. "
2006
+ f"Known: {', '.join(sorted(known)) or 'none'}"
2007
+ )
2008
+ if not new_order:
2009
+ raise ValueError("No valid providers specified. Nothing changed.")
2010
+
2011
+ fb.fallback_order = new_order
2012
+ self._persist_fallback_config(fb)
2013
+ self._show_success(f"Fallback order set: {' → '.join(new_order)}")
2014
+
2015
+ def _handle_on_rate_limit_subcommand(
2016
+ self, fb: FallbackConfig, args: list[str]
2017
+ ) -> None:
2018
+ """Handle ``/fallback on-rate-limit [on|off]``."""
2019
+ if len(args) < 2:
2020
+ state = "on" if fb.fallback_on_rate_limit else "off"
2021
+ self._show_info(f"Fallback on rate-limit (429) is [bold]{state}[/bold].")
2022
+ return
2023
+
2024
+ enabled = self._validate_toggle_arg(args[1].lower())
2025
+ if enabled is None:
2026
+ raise ValueError("Usage: /fallback on-rate-limit [on|off]")
2027
+
2028
+ fb.fallback_on_rate_limit = enabled
2029
+ self._persist_fallback_config(fb)
2030
+ label = "enabled" if enabled else "disabled"
2031
+ self._show_success(f"Fallback on rate-limit {label}.")
2032
+
2033
+ def _handle_on_quota_subcommand(self, fb: FallbackConfig, args: list[str]) -> None:
2034
+ """Handle ``/fallback on-quota [on|off]``."""
2035
+ if len(args) < 2:
2036
+ state = "on" if fb.fallback_on_quota else "off"
2037
+ self._show_info(f"Fallback on quota errors is [bold]{state}[/bold].")
2038
+ return
2039
+
2040
+ enabled = self._validate_toggle_arg(args[1].lower())
2041
+ if enabled is None:
2042
+ raise ValueError("Usage: /fallback on-quota [on|off]")
2043
+
2044
+ fb.fallback_on_quota = enabled
2045
+ self._persist_fallback_config(fb)
2046
+ label = "enabled" if enabled else "disabled"
2047
+ self._show_success(f"Fallback on quota errors {label}.")
2048
+
2049
+ # --- display helpers --------------------------------------------------
2050
+
2051
+ def _show_fallback_status(self, fb: FallbackConfig) -> None:
2052
+ """Print a Rich table with the current fallback configuration."""
2053
+ table = Table(
2054
+ title="Fallback Configuration", show_header=True, header_style="bold cyan"
2055
+ )
2056
+ table.add_column("Setting", style="bold")
2057
+ table.add_column("Value")
2058
+ table.add_column("Description", style="dim")
2059
+
2060
+ auto_label = "[green]on[/green]" if fb.auto_fallback else "[yellow]off[/yellow]"
2061
+ rl_label = (
2062
+ "[green]on[/green]" if fb.fallback_on_rate_limit else "[yellow]off[/yellow]"
2063
+ )
2064
+ q_label = (
2065
+ "[green]on[/green]" if fb.fallback_on_quota else "[yellow]off[/yellow]"
2066
+ )
2067
+ order_str = (
2068
+ " [dim]→[/dim] ".join(fb.fallback_order)
2069
+ if fb.fallback_order
2070
+ else "[dim](any available)[/dim]"
2071
+ )
2072
+
2073
+ table.add_row("auto", auto_label, "Switch without asking")
2074
+ table.add_row("on-rate-limit", rl_label, "Fallback on 429 / rate-limit")
2075
+ table.add_row("on-quota", q_label, "Fallback on quota exceeded")
2076
+ table.add_row("order", order_str, "Ordered list of providers to try")
2077
+
2078
+ self.console.print(table)
2079
+ self.console.print(
2080
+ "\n[dim]Tip: [cyan]/fallback auto on[/cyan] to auto-switch silently, "
2081
+ "[cyan]/fallback order claude openai[/cyan] to set priority.[/dim]"
2082
+ )
2083
+
2084
+ def _show_fallback_help(self) -> None:
2085
+ """Print /fallback usage."""
2086
+ help_text = (
2087
+ "[bold cyan]/fallback[/bold cyan] — manage rate-limit fallback "
2088
+ "behaviour\n\n"
2089
+ "[bold]Sub-commands[/bold]\n"
2090
+ " [cyan]/fallback[/cyan] "
2091
+ "show current configuration\n"
2092
+ " [cyan]/fallback auto on|off[/cyan] "
2093
+ "auto-switch on rate limit (no prompt)\n"
2094
+ " [cyan]/fallback order <p1> <p2> …[/cyan] "
2095
+ "set ordered fallback providers\n"
2096
+ " [cyan]/fallback order clear[/cyan] "
2097
+ "clear order (use any available)\n"
2098
+ " [cyan]/fallback on-rate-limit on|off[/cyan] "
2099
+ "enable/disable fallback on 429\n"
2100
+ " [cyan]/fallback on-quota on|off[/cyan] "
2101
+ "enable/disable fallback on quota errors\n\n"
2102
+ "[bold]Examples[/bold]\n"
2103
+ " [dim]/fallback auto on[/dim] "
2104
+ "silently switch when rate-limited\n"
2105
+ " [dim]/fallback order claude openai gemini[/dim] "
2106
+ "try in this order\n"
2107
+ " [dim]/fallback order clear[/dim] "
2108
+ "reset to default (any provider)\n\n"
2109
+ "When [bold]auto[/bold] is off (default), omnimancer will pause and ask:\n"
2110
+ " [yellow]⚠ Rate limit hit on claude. Fall back to openai? [Y/n][/yellow]"
2111
+ )
2112
+ self.console.print(Panel(help_text, title="Fallback Help", border_style="cyan"))
@@ -62,6 +62,7 @@ class SlashCommand(Enum):
62
62
  PERMISSIONS = "/permissions"
63
63
  PROMPTS = "/prompts"
64
64
  SUBAGENTS = "/subagents"
65
+ FALLBACK = "/fallback"
65
66
 
66
67
  @classmethod
67
68
  def from_string(cls, command_str: str) -> Optional["SlashCommand"]:
@@ -390,7 +391,10 @@ def _validate_command_args(command: SlashCommand, args: List[str]) -> List[str]:
390
391
  if filter_type not in valid_filters:
391
392
  filters_str = ", ".join(valid_filters)
392
393
  raise ValueError(
393
- "Invalid filter type. Valid" f" filters: {filters_str}"
394
+ f"Invalid filter type. Valid filters: {filters_str}. "
395
+ "To register a new model use "
396
+ "/add-model <model> <provider> — or just "
397
+ "/switch <provider> <model>."
394
398
  )
395
399
 
396
400
  # Validate second argument based on filter type
@@ -10,9 +10,19 @@ import uuid
10
10
  from enum import Enum
11
11
  from typing import Any, Dict, Optional
12
12
 
13
- from ..core.models import ChatResponse
13
+ from ..core.models import (
14
+ ChatResponse,
15
+ ToolResult,
16
+ ToolResultRecord,
17
+ parse_described_tool_calls,
18
+ )
14
19
  from .system_prompts import build_agent_prompt
15
- from .tool_handler import MAX_TOOL_ITERATIONS, ToolHandler
20
+ from .tool_handler import (
21
+ DUPLICATE_CALL_NUDGE,
22
+ MAX_TOOL_ITERATIONS,
23
+ RepeatedCallTracker,
24
+ ToolHandler,
25
+ )
16
26
 
17
27
 
18
28
  class OutputFormat(Enum):
@@ -212,10 +222,14 @@ class HeadlessRunner:
212
222
  output_format: OutputFormat = OutputFormat.TEXT,
213
223
  no_approval: bool = False,
214
224
  verbose: bool = False,
225
+ max_iterations: Optional[int] = None,
215
226
  ) -> None:
216
227
  self._engine = engine
217
228
  self._no_approval = no_approval
218
229
  self._verbose = verbose
230
+ # Headless runs are unattended, so a cap (not a check-in prompt) is
231
+ # the safety net; --max-iterations sizes it to the job.
232
+ self._max_iterations = max_iterations or MAX_TOOL_ITERATIONS
219
233
  session_id = str(uuid.uuid4())
220
234
  self._emitter = HeadlessOutputEmitter(output_format, session_id, verbose)
221
235
  self._tokens = TokenAccumulator()
@@ -242,10 +256,11 @@ class HeadlessRunner:
242
256
  # Record of the actions the agent took, surfaced in the JSON result.
243
257
  tool_log: list = []
244
258
  turns = 0
245
- # Detect runaway loops where a weak model repeats the same tool call.
246
- seen_calls: dict = {}
259
+ # Identical repeated calls get skipped with a corrective nudge; the
260
+ # run is aborted only if the model keeps repeating despite nudges.
261
+ repeat_tracker = RepeatedCallTracker()
247
262
 
248
- for iteration in range(MAX_TOOL_ITERATIONS):
263
+ for iteration in range(self._max_iterations):
249
264
  turns = iteration + 1
250
265
  response = await self._engine.send_message_with_tools(
251
266
  current_message, tools
@@ -269,29 +284,37 @@ class HeadlessRunner:
269
284
  last_stop_reason,
270
285
  )
271
286
 
287
+ # The model may mimic the "[Called tools: ...]" history notation
288
+ # as plain text instead of issuing native tool calls — recover
289
+ # those so the run doesn't end with the work undone.
272
290
  if not response.tool_calls:
273
- break
274
-
275
- repeated = False
276
- for tc in response.tool_calls:
277
- sig = (
278
- f"{tc.name}:{json.dumps(tc.arguments, sort_keys=True, default=str)}"
279
- )
280
- seen_calls[sig] = seen_calls.get(sig, 0) + 1
281
- if seen_calls[sig] >= 3:
282
- repeated = True
283
- if repeated:
291
+ recovered = parse_described_tool_calls(response.content)
292
+ if not recovered:
293
+ break
294
+ response.tool_calls = recovered
295
+
296
+ repeat_tracker.record(response.tool_calls)
297
+ offender = repeat_tracker.abort_offender(response.tool_calls)
298
+ if offender is not None:
284
299
  if not last_content:
285
300
  last_content = (
286
- "Stopped: the model kept repeating the same tool call."
301
+ f"Stopped: the model repeated the same tool call "
302
+ f"{repeat_tracker.count(offender)} times "
303
+ f"({offender.name}) despite duplicate warnings."
287
304
  )
288
305
  break
289
306
 
290
307
  result_parts = []
291
- for tc in response.tool_calls:
308
+ records = []
309
+ for i, tc in enumerate(response.tool_calls):
292
310
  self._emitter.emit_tool_use(tc.name, tc.arguments)
293
311
 
294
- result = await tool_handler.execute_tool_call(tc)
312
+ # Skip exact repeats with a nudge so the model can recover
313
+ # instead of the run being killed.
314
+ if repeat_tracker.is_duplicate(tc):
315
+ result = ToolResult(content=DUPLICATE_CALL_NUDGE)
316
+ else:
317
+ result = await tool_handler.execute_tool_call(tc)
295
318
  self._emitter.emit_tool_result(
296
319
  tc.name,
297
320
  result.content or "",
@@ -305,13 +328,29 @@ class HeadlessRunner:
305
328
  }
306
329
  )
307
330
 
331
+ # Label results with the call's arguments so the model can
332
+ # tell which call each result belongs to.
333
+ label = f"{tc.name}({json.dumps(tc.arguments, default=str)})"
308
334
  if result.error:
309
- result_parts.append(f"[{tc.name}] Error: {result.error}")
335
+ result_parts.append(f"[{label}] Error: {result.error}")
310
336
  else:
311
- result_parts.append(f"[{tc.name}] Result: {result.content}")
337
+ result_parts.append(f"[{label}] Result: {result.content}")
338
+ records.append(
339
+ ToolResultRecord(
340
+ tool_call_id=tc.id or f"call_{i}",
341
+ content=result.error or result.content,
342
+ )
343
+ )
312
344
 
313
345
  # Feed results back without forcing the model to "continue".
314
- current_message = "Tool results:\n\n" + "\n\n".join(result_parts)
346
+ results_text = "Tool results:\n\n" + "\n\n".join(result_parts)
347
+ if self._engine.provider_supports_native_tool_history() is True:
348
+ # Native protocol: structured history + empty continuation
349
+ # message (see interface._handle_tool_calling_flow).
350
+ self._engine.record_tool_results(results_text, records)
351
+ current_message = ""
352
+ else:
353
+ current_message = results_text
315
354
 
316
355
  usage = self._tokens.total
317
356
  self._emitter.emit_result(
@@ -335,6 +374,7 @@ async def run_headless(
335
374
  provider: Optional[str] = None,
336
375
  model: Optional[str] = None,
337
376
  base_url: Optional[str] = None,
377
+ max_iterations: Optional[int] = None,
338
378
  ) -> int:
339
379
  from ..core.config_manager import ConfigManager
340
380
  from ..core.engine import CoreEngine
@@ -370,5 +410,6 @@ async def run_headless(
370
410
  output_format=fmt,
371
411
  no_approval=no_approval,
372
412
  verbose=verbose,
413
+ max_iterations=max_iterations,
373
414
  )
374
415
  return await runner.run(prompt)