steerable-agent-runtime 0.6.51__tar.gz → 0.6.53__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 (163) hide show
  1. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/PKG-INFO +3 -3
  2. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/pyproject.toml +7 -7
  3. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/__init__.py +6 -0
  4. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/approval.py +86 -0
  5. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/default.harness.json +0 -2
  6. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/default.harness.yaml +4 -2
  7. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/mcp.py +242 -7
  8. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/orchestration.py +69 -10
  9. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pool.py +32 -5
  10. steerable_agent_runtime-0.6.53/src/steerable_agent_runtime/tool_presentation.py +110 -0
  11. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tools.py +7 -1
  12. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/PKG-INFO +3 -3
  13. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/SOURCES.txt +2 -0
  14. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/requires.txt +2 -2
  15. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_approval.py +101 -0
  16. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_mcp.py +155 -0
  17. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_orchestration.py +246 -0
  18. steerable_agent_runtime-0.6.53/tests/test_tool_presentation.py +113 -0
  19. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/README.md +0 -0
  20. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/setup.cfg +0 -0
  21. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/antihallucination.py +0 -0
  22. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/approval_policy.py +0 -0
  23. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/ask_user.py +0 -0
  24. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/branch.py +0 -0
  25. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/cache_control.py +0 -0
  26. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/calibration.py +0 -0
  27. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/compaction.py +0 -0
  28. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/compaction_policy.py +0 -0
  29. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/config.py +0 -0
  30. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/delegation_gate.py +0 -0
  31. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/errors.py +0 -0
  32. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  33. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/handoff.py +0 -0
  34. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/harness.py +0 -0
  35. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/harness_spec.py +0 -0
  36. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/history.py +0 -0
  37. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/hooks.py +0 -0
  38. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  39. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  40. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/compat.py +0 -0
  41. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/errors.py +0 -0
  42. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  43. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  44. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  45. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/parts.py +0 -0
  46. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/presets.py +0 -0
  47. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  48. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/loop.py +0 -0
  49. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/maintenance.py +0 -0
  50. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/mcp_server.py +0 -0
  51. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_catalog.py +0 -0
  52. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_info.py +0 -0
  53. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_resolve.py +0 -0
  54. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/native_bridge.py +0 -0
  55. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/observation_aging.py +0 -0
  56. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/otel.py +0 -0
  57. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/plugins.py +0 -0
  58. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pricing.py +0 -0
  59. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pseudo.py +0 -0
  60. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/recording.py +0 -0
  61. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/reminders.py +0 -0
  62. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/replay.py +0 -0
  63. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/resume.py +0 -0
  64. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/retry.py +0 -0
  65. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/sandboxed.py +0 -0
  66. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/skills.py +0 -0
  67. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/spill.py +0 -0
  68. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  69. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  70. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  71. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  72. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  73. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/subagent.py +0 -0
  74. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/todo.py +0 -0
  75. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tokens.py +0 -0
  76. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tool_schema.py +0 -0
  77. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tool_search.py +0 -0
  78. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tracing.py +0 -0
  79. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  80. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  81. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  82. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/world_state.py +0 -0
  83. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  84. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  85. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_antihallucination.py +0 -0
  86. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_approval_policy.py +0 -0
  87. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_ask_user.py +0 -0
  88. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_branch.py +0 -0
  89. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_cache_control.py +0 -0
  90. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_cache_instrumentation.py +0 -0
  91. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_calibration.py +0 -0
  92. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_compaction.py +0 -0
  93. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_compaction_policy.py +0 -0
  94. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_compaction_strategy.py +0 -0
  95. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_config.py +0 -0
  96. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_content_parts.py +0 -0
  97. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_delegation_gate.py +0 -0
  98. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_error_taxonomy.py +0 -0
  99. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_fragment_bounds.py +0 -0
  100. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_gateway_catalog.py +0 -0
  101. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_golden.py +0 -0
  102. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_handoff.py +0 -0
  103. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_harness.py +0 -0
  104. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_harness_spec.py +0 -0
  105. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_history.py +0 -0
  106. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_history_persistence.py +0 -0
  107. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_hooks.py +0 -0
  108. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_in_memory_storage.py +0 -0
  109. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_llm_gemini_wire.py +0 -0
  110. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_llm_responses_wire.py +0 -0
  111. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_llm_stream_e2e.py +0 -0
  112. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_llm_wire_helpers.py +0 -0
  113. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_long_session.py +0 -0
  114. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_loop.py +0 -0
  115. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_loop_cancellation.py +0 -0
  116. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_loop_replay.py +0 -0
  117. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_loop_sandbox_event.py +0 -0
  118. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_loop_terminal.py +0 -0
  119. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_maintenance.py +0 -0
  120. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_mcp_server.py +0 -0
  121. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_model_catalog.py +0 -0
  122. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_model_equivalence.py +0 -0
  123. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_model_info.py +0 -0
  124. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_model_resolve.py +0 -0
  125. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_native_default.py +0 -0
  126. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_observation_aging.py +0 -0
  127. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_otel.py +0 -0
  128. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_parallel_tools.py +0 -0
  129. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_plugins.py +0 -0
  130. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_provider_compat.py +0 -0
  131. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_provider_presets.py +0 -0
  132. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_pseudo.py +0 -0
  133. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_pyo3_api_surface.py +0 -0
  134. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_recording.py +0 -0
  135. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_reminders.py +0 -0
  136. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_replay_crosslang.py +0 -0
  137. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_resume.py +0 -0
  138. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_retry_hooks.py +0 -0
  139. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_safety_gate.py +0 -0
  140. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_sandboxed.py +0 -0
  141. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_skills.py +0 -0
  142. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_soft_timeout.py +0 -0
  143. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_spill.py +0 -0
  144. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_sqlite_storage.py +0 -0
  145. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_steer.py +0 -0
  146. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_storage_contract.py +0 -0
  147. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_stream_strip.py +0 -0
  148. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_subagent.py +0 -0
  149. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_system_proxy.py +0 -0
  150. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_todo.py +0 -0
  151. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_todo_gate.py +0 -0
  152. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tokens.py +0 -0
  153. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tool_exposure.py +0 -0
  154. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tool_hygiene.py +0 -0
  155. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tool_router.py +0 -0
  156. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tool_schema.py +0 -0
  157. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tool_timeout.py +0 -0
  158. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_trace_recorder.py +0 -0
  159. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_transport_jsonrpc.py +0 -0
  160. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_transport_sse.py +0 -0
  161. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_usage_attribution.py +0 -0
  162. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_world_state.py +0 -0
  163. {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_write_lease.py +0 -0
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.51
3
+ Version: 0.6.53
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
7
7
  Description-Content-Type: text/markdown
8
+ Requires-Dist: httpx[socks]>=0.27
9
+ Requires-Dist: mcp<2.0.0,>=1.13.0
8
10
  Requires-Dist: pydantic>=2.10.0
9
11
  Requires-Dist: steerable-agent-protocol<1.0.0,>=0.1.0
10
12
  Requires-Dist: steerable-agent-harness<1.0.0,>=0.1.0
@@ -15,14 +17,12 @@ Provides-Extra: fastapi
15
17
  Requires-Dist: fastapi>=0.110; extra == "fastapi"
16
18
  Requires-Dist: starlette>=0.37; extra == "fastapi"
17
19
  Provides-Extra: openai
18
- Requires-Dist: httpx[socks]>=0.27; extra == "openai"
19
20
  Provides-Extra: anthropic
20
21
  Requires-Dist: anthropic>=0.40; extra == "anthropic"
21
22
  Provides-Extra: all
22
23
  Requires-Dist: sqlalchemy>=2.0; extra == "all"
23
24
  Requires-Dist: fastapi>=0.110; extra == "all"
24
25
  Requires-Dist: starlette>=0.37; extra == "all"
25
- Requires-Dist: httpx[socks]>=0.27; extra == "all"
26
26
  Requires-Dist: anthropic>=0.40; extra == "all"
27
27
 
28
28
  # steerable-agent-runtime
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.51"
3
+ version = "0.6.53"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  license = { text = "Apache-2.0" }
6
6
  readme = "README.md"
@@ -13,6 +13,11 @@ requires-python = ">=3.10"
13
13
  # CoreLoop execution is the Rust engine. The matching native wheel
14
14
  # is a hard dependency: install or startup fails if it is missing.
15
15
  dependencies = [
16
+ # MCP Streamable HTTP and OpenAI-compatible providers both construct httpx
17
+ # clients. Keep SOCKS support in the base install because exported proxy
18
+ # variables are evaluated during client construction.
19
+ "httpx[socks]>=0.27",
20
+ "mcp>=1.13.0,<2.0.0",
16
21
  "pydantic>=2.10.0",
17
22
  "steerable-agent-protocol>=0.1.0,<1.0.0",
18
23
  "steerable-agent-harness>=0.1.0,<1.0.0",
@@ -22,17 +27,12 @@ dependencies = [
22
27
  [project.optional-dependencies]
23
28
  sqlalchemy = ["sqlalchemy>=2.0"]
24
29
  fastapi = ["fastapi>=0.110", "starlette>=0.37"]
25
- # The `socks` extra is not optional in practice: httpx builds a transport for
26
- # every proxy the environment declares while the client is constructed, so a
27
- # host exporting `all_proxy=socks5://…` — the common shell proxy snippet — makes
28
- # construction raise without it, failing provider calls that never reach a URL.
29
- openai = ["httpx[socks]>=0.27"]
30
+ openai = []
30
31
  anthropic = ["anthropic>=0.40"]
31
32
  all = [
32
33
  "sqlalchemy>=2.0",
33
34
  "fastapi>=0.110",
34
35
  "starlette>=0.37",
35
- "httpx[socks]>=0.27",
36
36
  "anthropic>=0.40",
37
37
  ]
38
38
 
@@ -163,9 +163,12 @@ from .mcp import (
163
163
  DEFAULT_MAX_SERVER_TOOLS,
164
164
  McpCallResult,
165
165
  McpCatalogError,
166
+ McpClient,
166
167
  McpError,
168
+ McpStreamableHttpClient,
167
169
  McpStdioClient,
168
170
  McpToolInfo,
171
+ create_mcp_client,
169
172
  mcp_invoker,
170
173
  parse_mcp_name,
171
174
  qualify_mcp_name,
@@ -374,7 +377,9 @@ __all__ = [
374
377
  "LoopHooks",
375
378
  "McpCallResult",
376
379
  "McpCatalogError",
380
+ "McpClient",
377
381
  "McpError",
382
+ "McpStreamableHttpClient",
378
383
  "McpStdioClient",
379
384
  "McpToolInfo",
380
385
  "ModelCalibration",
@@ -472,6 +477,7 @@ __all__ = [
472
477
  "make_todo_write_tool",
473
478
  "parent_input_tool_descriptor",
474
479
  "matches_conditions",
480
+ "create_mcp_client",
475
481
  "mcp_invoker",
476
482
  "merge_patch",
477
483
  "message_from_dict",
@@ -53,6 +53,7 @@ __all__ = [
53
53
  "ApprovalStore",
54
54
  "Approver",
55
55
  "AutoApprover",
56
+ "WorkspaceAutoApprover",
56
57
  "InMemoryApprovalStore",
57
58
  "JsonApprovalStore",
58
59
  "SessionApprovalCache",
@@ -130,6 +131,91 @@ class Approver(Protocol):
130
131
  async def approve(self, request: ApprovalRequest) -> ApprovalDecision: ...
131
132
 
132
133
 
134
+ _PATH_ARG_TOOLS = {
135
+ "local_read_file": "path",
136
+ "local_write_file": "path",
137
+ "local_edit_file": "path",
138
+ "view_image": "path",
139
+ }
140
+
141
+
142
+ def declared_target_paths(request: ApprovalRequest) -> list[str] | None:
143
+ """Filesystem targets of a path-scoped tool, or None when the call is not one.
144
+
145
+ ``None`` means the approver cannot prove the call stays inside the
146
+ project, so the interactive prompt still runs. Shell commands stay in
147
+ that bucket: the command text can name paths the cwd does not.
148
+ """
149
+ if request.tool_name == "present_files":
150
+ files = request.arguments.get("files")
151
+ if not isinstance(files, list) or not files:
152
+ return None
153
+ paths: list[str] = []
154
+ for item in files:
155
+ if not isinstance(item, dict):
156
+ return None
157
+ raw = item.get("path")
158
+ if not isinstance(raw, str) or not raw.strip():
159
+ return None
160
+ paths.append(raw.strip())
161
+ return paths
162
+ key = _PATH_ARG_TOOLS.get(request.tool_name)
163
+ if key is None:
164
+ return None
165
+ raw = request.arguments.get(key)
166
+ if not isinstance(raw, str) or not raw.strip():
167
+ return None
168
+ return [raw.strip()]
169
+
170
+
171
+ def path_inside_writable_roots(raw: str, roots: list[str]) -> bool:
172
+ """True when ``raw`` is the configured directory or one of its subdirectories.
173
+
174
+ Relative paths are project-relative. A ``..`` segment can leave the
175
+ project, so those still go to the prompt. Absolute paths must sit under
176
+ one of ``roots``.
177
+ """
178
+ expanded = os.path.expanduser(raw.strip())
179
+ if not os.path.isabs(expanded):
180
+ normalized = os.path.normpath(expanded)
181
+ return normalized != ".." and not normalized.startswith(".." + os.sep)
182
+ target = os.path.abspath(expanded)
183
+ for root in roots:
184
+ if not isinstance(root, str) or not root.strip():
185
+ continue
186
+ root_abs = os.path.abspath(os.path.expanduser(root.strip()))
187
+ try:
188
+ if os.path.commonpath([target, root_abs]) == root_abs:
189
+ return True
190
+ except ValueError:
191
+ continue
192
+ return False
193
+
194
+
195
+ class WorkspaceAutoApprover:
196
+ """Skip the host prompt when every declared path is already inside a writable root.
197
+
198
+ The allow is request-scoped. The next call is judged again, so an
199
+ in-project ``present_files`` does not grant the same tool outside the
200
+ project. Calls with no path we can check, or any path outside the roots,
201
+ fall through to the inner approver.
202
+ """
203
+
204
+ def __init__(self, inner: Approver, writable_roots: list[str]) -> None:
205
+ self._inner = inner
206
+ self._roots = [root for root in writable_roots if isinstance(root, str) and root.strip()]
207
+
208
+ async def approve(self, request: ApprovalRequest) -> ApprovalDecision:
209
+ paths = declared_target_paths(request)
210
+ if (
211
+ self._roots
212
+ and paths is not None
213
+ and all(path_inside_writable_roots(path, self._roots) for path in paths)
214
+ ):
215
+ return ApprovalDecision("allow_once", "path is inside a writable project root")
216
+ return await self._inner.approve(request)
217
+
218
+
133
219
  class AutoApprover:
134
220
  """Headless approver: never blocks, decides by tool mode.
135
221
 
@@ -23,8 +23,6 @@
23
23
  "memory": "stateless",
24
24
  "orchestration": "single",
25
25
  "loop": {
26
- "max_rounds": 80,
27
- "max_tool_errors": 16,
28
26
  "tool_dedup": false
29
27
  }
30
28
  }
@@ -22,7 +22,9 @@ memory: stateless
22
22
  orchestration: single
23
23
  # Loop limits the harness pins (W3.4.2.4): entrypoints read these instead of
24
24
  # hardcoding knobs. Absent fields fall back to the entrypoint's baseline.
25
+ # max_rounds and max_tool_errors are absent on purpose — a cap here ends a
26
+ # long task at budget_exhausted / consecutive tool errors. The baseline is
27
+ # no cap. A caller that wants a wall passes maxRounds / maxToolErrors, and
28
+ # Harbor passes --max-rounds 250.
25
29
  loop:
26
- max_rounds: 80
27
- max_tool_errors: 16
28
30
  tool_dedup: false
@@ -1,4 +1,4 @@
1
- """MCP client seam: qualified names, capped catalogs, minimal stdio client.
1
+ """MCP client seam: qualified names, capped catalogs, transport clients.
2
2
 
3
3
  MCP is the largest unbounded, third-party, mutable source of model-visible
4
4
  context an agent system can add, so it lands on the Wave 2 foundation:
@@ -22,9 +22,9 @@ caps.
22
22
  Architecture (recorded in docs/roadmap.md): in the desktop product, MCP
23
23
  servers are launched host-side (Electron main) and reached through
24
24
  `ToolRouter.register_remote` — a Seatbelt-confined sidecar never spawns
25
- them. `McpStdioClient` exists for hosts that embed the runtime directly
26
- (CLI agents, tests, non-Electron hosts); it is transport-only and holds no
27
- loop state.
25
+ them. `McpStdioClient` and `McpStreamableHttpClient` exist for hosts that
26
+ embed the runtime directly (CLI agents, tests, non-Electron hosts); both
27
+ are transport-only and hold no loop state.
28
28
  """
29
29
 
30
30
  from __future__ import annotations
@@ -34,8 +34,15 @@ import json
34
34
  import logging
35
35
  import os
36
36
  from collections.abc import Awaitable, Callable, Mapping, Sequence
37
+ from contextlib import AsyncExitStack
37
38
  from dataclasses import dataclass, field
38
- from typing import Any
39
+ from datetime import timedelta
40
+ from typing import Any, Protocol
41
+ from urllib.parse import urlparse
42
+
43
+ import httpx
44
+ from mcp import ClientSession
45
+ from mcp.client.streamable_http import streamable_http_client
39
46
 
40
47
  from .errors import RuntimeError as SteerableRuntimeError
41
48
  from .tools import RegisteredTool, ToolExposure, ToolRouter
@@ -126,6 +133,26 @@ class McpCallResult:
126
133
  structured: Any = None
127
134
 
128
135
 
136
+ class McpClient(Protocol):
137
+ """Transport-independent client operations consumed by MCP mounting paths."""
138
+
139
+ server_info: dict[str, Any]
140
+
141
+ async def __aenter__(self) -> McpClient: ...
142
+
143
+ async def __aexit__(self, *exc: Any) -> None: ...
144
+
145
+ async def start(self) -> None: ...
146
+
147
+ async def aclose(self) -> None: ...
148
+
149
+ async def list_tools(self) -> list[McpToolInfo]: ...
150
+
151
+ async def call_tool(
152
+ self, name: str, arguments: dict[str, Any]
153
+ ) -> McpCallResult: ...
154
+
155
+
129
156
  def register_mcp_catalog(
130
157
  router: ToolRouter,
131
158
  *,
@@ -178,9 +205,9 @@ def register_mcp_catalog(
178
205
 
179
206
 
180
207
  def mcp_invoker(
181
- client: "McpStdioClient",
208
+ client: McpClient,
182
209
  ) -> Callable[[str, dict[str, Any]], Awaitable[Any]]:
183
- """Adapt `McpStdioClient.call_tool` to the catalog invoker contract.
210
+ """Adapt an MCP client's `call_tool` to the catalog invoker contract.
184
211
 
185
212
  Maps ``isError`` results to failed `ToolResult`s (follow-up allowed —
186
213
  the model sees the server's error text and can correct its arguments)
@@ -199,6 +226,214 @@ def mcp_invoker(
199
226
  return _invoke
200
227
 
201
228
 
229
+ def create_mcp_client(
230
+ config: Mapping[str, Any],
231
+ *,
232
+ environ: Mapping[str, str] | None = None,
233
+ request_timeout: float = 30.0,
234
+ ) -> McpClient:
235
+ """Build a client from an explicit or legacy-discriminated config entry.
236
+
237
+ ``transport`` accepts ``stdio`` and ``streamable-http``. For existing
238
+ callers, an omitted discriminator is inferred from exactly one of
239
+ ``command`` or ``url``. Environment-sourced header values are resolved
240
+ only into the client and are redacted from transport failures.
241
+ """
242
+ command = config.get("command")
243
+ url = config.get("url")
244
+ if command and url:
245
+ raise McpError("MCP config cannot contain both 'command' and 'url'")
246
+
247
+ transport = config.get("transport")
248
+ if transport is None:
249
+ if command:
250
+ transport = "stdio"
251
+ elif url:
252
+ transport = "streamable-http"
253
+ else:
254
+ raise McpError("MCP config requires a non-empty 'command' or 'url'")
255
+
256
+ if transport == "stdio":
257
+ if not command:
258
+ raise McpError("stdio MCP config requires a non-empty 'command'")
259
+ if url:
260
+ raise McpError("stdio MCP config cannot contain 'url'")
261
+ raw_env = config.get("env") or {}
262
+ if not isinstance(raw_env, Mapping):
263
+ raise McpError("stdio MCP config 'env' must be an object")
264
+ return McpStdioClient(
265
+ str(command),
266
+ [str(arg) for arg in config.get("args") or []],
267
+ env={str(key): str(value) for key, value in raw_env.items()} or None,
268
+ request_timeout=request_timeout,
269
+ )
270
+
271
+ if transport != "streamable-http":
272
+ raise McpError(f"unsupported MCP transport: {transport!r}")
273
+ if not url:
274
+ raise McpError("streamable-http MCP config requires a non-empty 'url'")
275
+ if command:
276
+ raise McpError("streamable-http MCP config cannot contain 'command'")
277
+ parsed = urlparse(str(url))
278
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
279
+ raise McpError("streamable-http MCP config 'url' must be an HTTP(S) URL")
280
+ if parsed.username or parsed.password:
281
+ raise McpError("streamable-http MCP config 'url' cannot contain credentials")
282
+
283
+ raw_headers = config.get("headers") or {}
284
+ raw_headers_from_env = config.get("headersFromEnv") or {}
285
+ if not isinstance(raw_headers, Mapping):
286
+ raise McpError("streamable-http MCP config 'headers' must be an object")
287
+ if not isinstance(raw_headers_from_env, Mapping):
288
+ raise McpError(
289
+ "streamable-http MCP config 'headersFromEnv' must be an object"
290
+ )
291
+ source = os.environ if environ is None else environ
292
+ headers = {str(name): str(value) for name, value in raw_headers.items()}
293
+ secret_values = [value for value in headers.values() if value]
294
+ for raw_name, raw_env_name in raw_headers_from_env.items():
295
+ env_name = str(raw_env_name)
296
+ value = source.get(env_name)
297
+ if not value:
298
+ raise McpError(
299
+ f"streamable-http MCP header environment variable "
300
+ f"{env_name!r} is not set"
301
+ )
302
+ headers[str(raw_name)] = value
303
+ secret_values.append(value)
304
+ bearer_env = config.get("bearerTokenEnvVar")
305
+ if bearer_env is not None:
306
+ env_name = str(bearer_env)
307
+ value = source.get(env_name)
308
+ if not value:
309
+ raise McpError(
310
+ f"streamable-http MCP bearer environment variable "
311
+ f"{env_name!r} is not set"
312
+ )
313
+ headers["Authorization"] = f"Bearer {value}"
314
+ secret_values.append(value)
315
+
316
+ return McpStreamableHttpClient(
317
+ str(url),
318
+ headers=headers,
319
+ secret_values=secret_values,
320
+ request_timeout=request_timeout,
321
+ )
322
+
323
+
324
+ class McpStreamableHttpClient:
325
+ """MCP Streamable HTTP client backed by the official Python MCP SDK."""
326
+
327
+ def __init__(
328
+ self,
329
+ url: str,
330
+ *,
331
+ headers: Mapping[str, str] | None = None,
332
+ secret_values: Sequence[str] = (),
333
+ request_timeout: float = 30.0,
334
+ ) -> None:
335
+ self._url = url
336
+ self._headers = dict(headers or {})
337
+ self._secret_values = tuple(value for value in secret_values if value)
338
+ self._request_timeout = request_timeout
339
+ self._stack: AsyncExitStack | None = None
340
+ self._session: ClientSession | None = None
341
+ self.server_info: dict[str, Any] = {}
342
+
343
+ async def __aenter__(self) -> McpStreamableHttpClient:
344
+ await self.start()
345
+ return self
346
+
347
+ async def __aexit__(self, *exc: Any) -> None:
348
+ await self.aclose()
349
+
350
+ async def start(self) -> None:
351
+ if self._stack is not None:
352
+ raise McpError("McpStreamableHttpClient already started")
353
+ stack = AsyncExitStack()
354
+ self._stack = stack
355
+ try:
356
+ http_client = await stack.enter_async_context(
357
+ httpx.AsyncClient(headers=self._headers)
358
+ )
359
+ read_stream, write_stream, _session_id = await stack.enter_async_context(
360
+ streamable_http_client(self._url, http_client=http_client)
361
+ )
362
+ session = await stack.enter_async_context(
363
+ ClientSession(
364
+ read_stream,
365
+ write_stream,
366
+ read_timeout_seconds=timedelta(seconds=self._request_timeout),
367
+ )
368
+ )
369
+ initialized = await session.initialize()
370
+ self._session = session
371
+ self.server_info = initialized.serverInfo.model_dump(
372
+ by_alias=True, exclude_none=True
373
+ )
374
+ except Exception as exc:
375
+ await self.aclose()
376
+ raise self._error("initialize", exc) from exc
377
+
378
+ async def aclose(self) -> None:
379
+ stack, self._stack = self._stack, None
380
+ self._session = None
381
+ if stack is not None:
382
+ await stack.aclose()
383
+
384
+ async def list_tools(self) -> list[McpToolInfo]:
385
+ session = self._require_session()
386
+ tools: list[McpToolInfo] = []
387
+ cursor: str | None = None
388
+ try:
389
+ while True:
390
+ result = await session.list_tools(cursor=cursor)
391
+ tools.extend(
392
+ McpToolInfo(
393
+ name=tool.name,
394
+ description=tool.description or "",
395
+ schema=dict(tool.inputSchema),
396
+ )
397
+ for tool in result.tools
398
+ )
399
+ cursor = result.nextCursor
400
+ if cursor is None:
401
+ return tools
402
+ except Exception as exc:
403
+ raise self._error("tools/list", exc) from exc
404
+
405
+ async def call_tool(
406
+ self, name: str, arguments: dict[str, Any]
407
+ ) -> McpCallResult:
408
+ session = self._require_session()
409
+ try:
410
+ result = await session.call_tool(name, arguments)
411
+ except Exception as exc:
412
+ raise self._error("tools/call", exc) from exc
413
+ parts: list[str] = []
414
+ for content in result.content:
415
+ if content.type == "text":
416
+ parts.append(content.text)
417
+ else:
418
+ parts.append(f"[mcp {content.type} content]")
419
+ return McpCallResult(
420
+ text="\n".join(parts),
421
+ is_error=result.isError,
422
+ structured=result.structuredContent,
423
+ )
424
+
425
+ def _require_session(self) -> ClientSession:
426
+ if self._session is None:
427
+ raise McpError("MCP Streamable HTTP client is not running")
428
+ return self._session
429
+
430
+ def _error(self, operation: str, exc: Exception) -> McpError:
431
+ message = str(exc)
432
+ for secret in self._secret_values:
433
+ message = message.replace(secret, "[redacted]")
434
+ return McpError(f"MCP Streamable HTTP {operation} failed: {message}")
435
+
436
+
202
437
  # ---------------------------------------------------------------------------
203
438
  # Minimal stdio client (NDJSON JSON-RPC 2.0)
204
439
  # ---------------------------------------------------------------------------
@@ -38,6 +38,7 @@ from typing import Any
38
38
 
39
39
  from steerable_agent_protocol.generated import ToolCall, ToolResult
40
40
 
41
+ from .hooks import ChainHooks, CompletionAction, CompletionDraft, NoopHooks
41
42
  from .llm import LLMProvider
42
43
  from .loop import CoreLoop, LoopConfig, LoopContext, LoopHooks, ToolExecutor
43
44
  from .pool import (
@@ -51,12 +52,61 @@ from .subagent import FilteredToolsExecutor, _schema_name
51
52
  __all__ = [
52
53
  "AgentPool",
53
54
  "ChildOutcome",
55
+ "ChildBudgetWrapUp",
54
56
  "OrchestrationBudgetExceeded",
55
57
  "OrchestrationConfig",
56
58
  "OrchestrationExecutor",
57
59
  "orchestration_tool_descriptors",
58
60
  ]
59
61
 
62
+ #: Terminal status of a run stopped by ``max_rounds`` or the token budget.
63
+ _BUDGET_WALL_STATUS = "budget_exhausted"
64
+
65
+ _BUDGET_WRAP_UP_REQUEST = (
66
+ "[system notice] This run has used its round budget and tools are now "
67
+ "withheld. Answer the original task now from what you have already "
68
+ "gathered: give your concrete findings first, then list what you could "
69
+ "not finish or verify. Do not describe the steps you took."
70
+ )
71
+
72
+ _RESUME_HINT = (
73
+ "The child stopped at its round budget; `answer` is its interim report. "
74
+ "To continue, agent_send this childId a message naming what is still "
75
+ "missing — it resumes with its prior context and a fresh budget. Do not "
76
+ "spawn a new child for the same task."
77
+ )
78
+
79
+
80
+ class ChildBudgetWrapUp(NoopHooks):
81
+ """``before_completion``: one no-tools answer round at a child's budget wall.
82
+
83
+ Without it a child stopped by ``max_rounds`` or the token budget returns
84
+ whatever text its last working round produced — usually a progress note,
85
+ not an answer. The parent then gets ``budget_exhausted`` with nothing
86
+ usable. The ``narrate`` action runs one round without tools and keeps the
87
+ ``budget_exhausted`` status, so the parent can still tell the answer is
88
+ interim and resume the child with ``agent_send``.
89
+
90
+ One instance per child loop. A resumed child runs the same loop with a new
91
+ ``LoopContext``, so the one-round limit applies per run.
92
+ """
93
+
94
+ def __init__(self) -> None:
95
+ self._wrapped_run: LoopContext | None = None
96
+
97
+ async def before_completion(
98
+ self, draft: CompletionDraft, ctx: LoopContext
99
+ ) -> CompletionAction:
100
+ """Narrate once per run when the draft stops at the budget wall."""
101
+ if draft.status != _BUDGET_WALL_STATUS or self._wrapped_run is ctx:
102
+ return CompletionAction(kind="accept")
103
+ self._wrapped_run = ctx
104
+ return CompletionAction(
105
+ kind="narrate",
106
+ message=_BUDGET_WRAP_UP_REQUEST,
107
+ reason="child_budget_wrap_up",
108
+ )
109
+
60
110
 
61
111
  def orchestration_tool_descriptors(
62
112
  config: OrchestrationConfig | None = None,
@@ -119,7 +169,10 @@ def orchestration_tool_descriptors(
119
169
  fn(
120
170
  config.wait_tool,
121
171
  "Wait for a sub-agent to finish and return its outcome. A child "
122
- "still running at the timeout returns status 'running'.",
172
+ "still running at the timeout returns status 'running'. A child "
173
+ "that hit its round budget returns status 'budget_exhausted' with "
174
+ "an interim answer and resumable: true — continue it with "
175
+ "agent_send instead of spawning a new child.",
123
176
  {
124
177
  "childId": {"type": "string"},
125
178
  "timeoutMs": {"type": "integer"},
@@ -228,11 +281,15 @@ class OrchestrationExecutor:
228
281
  _lineage=child_id,
229
282
  )
230
283
  schemas = [*schemas, *orchestration_tool_descriptors(self._config)]
284
+ wrap_up = ChildBudgetWrapUp()
231
285
  loop = CoreLoop(
232
286
  self._provider,
233
287
  base,
234
- LoopConfig(max_rounds=self._config.child_max_rounds),
235
- hooks=self._hooks,
288
+ LoopConfig(
289
+ max_rounds=self._config.child_max_rounds,
290
+ max_tool_errors=self._config.child_max_tool_errors,
291
+ ),
292
+ hooks=wrap_up if self._hooks is None else ChainHooks(self._hooks, wrap_up),
236
293
  )
237
294
  return loop, schemas or None
238
295
 
@@ -343,15 +400,17 @@ class OrchestrationExecutor:
343
400
  error=f"unknown_child: {child_id!r}",
344
401
  needsFollowup=True,
345
402
  )
403
+ payload: dict[str, Any] = {
404
+ "childId": outcome.child_id,
405
+ "status": outcome.status,
406
+ "answer": outcome.answer,
407
+ }
408
+ if self._pool.resumable(outcome.child_id):
409
+ payload["resumable"] = True
410
+ payload["hint"] = _RESUME_HINT
346
411
  return ToolResult(
347
412
  success=True,
348
- message=json.dumps(
349
- {
350
- "childId": outcome.child_id,
351
- "status": outcome.status,
352
- "answer": outcome.answer,
353
- }
354
- ),
413
+ message=json.dumps(payload),
355
414
  data={"childId": outcome.child_id, "status": outcome.status},
356
415
  )
357
416