steerable-agent-runtime 0.6.41__tar.gz → 0.6.43__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 (161) hide show
  1. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/PKG-INFO +2 -2
  2. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/pyproject.toml +2 -2
  3. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/approval.py +56 -11
  4. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/ask_user.py +41 -3
  5. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/loop.py +46 -4
  6. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/native_bridge.py +17 -2
  7. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime.egg-info/PKG-INFO +2 -2
  8. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime.egg-info/requires.txt +1 -1
  9. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_tool_timeout.py +89 -0
  10. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/README.md +0 -0
  11. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/setup.cfg +0 -0
  12. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/__init__.py +0 -0
  13. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/antihallucination.py +0 -0
  14. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/approval_policy.py +0 -0
  15. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/branch.py +0 -0
  16. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/cache_control.py +0 -0
  17. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/calibration.py +0 -0
  18. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/compaction.py +0 -0
  19. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/compaction_policy.py +0 -0
  20. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/config.py +0 -0
  21. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/default.harness.json +0 -0
  22. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  23. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/delegation_gate.py +0 -0
  24. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/errors.py +0 -0
  25. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  26. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/handoff.py +0 -0
  27. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/harness.py +0 -0
  28. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/harness_spec.py +0 -0
  29. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/history.py +0 -0
  30. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/hooks.py +0 -0
  31. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  32. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  33. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/llm/compat.py +0 -0
  34. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/llm/errors.py +0 -0
  35. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  36. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  37. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  38. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/llm/parts.py +0 -0
  39. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/llm/presets.py +0 -0
  40. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  41. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/maintenance.py +0 -0
  42. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/mcp.py +0 -0
  43. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/mcp_server.py +0 -0
  44. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/model_catalog.py +0 -0
  45. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/model_info.py +0 -0
  46. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/model_resolve.py +0 -0
  47. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/observation_aging.py +0 -0
  48. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/orchestration.py +0 -0
  49. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/otel.py +0 -0
  50. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/plugins.py +0 -0
  51. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/pool.py +0 -0
  52. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/pricing.py +0 -0
  53. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/pseudo.py +0 -0
  54. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/recording.py +0 -0
  55. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/reminders.py +0 -0
  56. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/replay.py +0 -0
  57. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/resume.py +0 -0
  58. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/retry.py +0 -0
  59. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/sandboxed.py +0 -0
  60. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/skills.py +0 -0
  61. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/spill.py +0 -0
  62. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  63. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  64. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  65. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  66. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  67. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/subagent.py +0 -0
  68. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/todo.py +0 -0
  69. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/tokens.py +0 -0
  70. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/tool_schema.py +0 -0
  71. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/tool_search.py +0 -0
  72. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/tools.py +0 -0
  73. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/tracing.py +0 -0
  74. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  75. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  76. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  77. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime/world_state.py +0 -0
  78. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
  79. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  80. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  81. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_antihallucination.py +0 -0
  82. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_approval.py +0 -0
  83. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_approval_policy.py +0 -0
  84. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_ask_user.py +0 -0
  85. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_branch.py +0 -0
  86. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_cache_control.py +0 -0
  87. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_cache_instrumentation.py +0 -0
  88. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_calibration.py +0 -0
  89. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_compaction.py +0 -0
  90. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_compaction_policy.py +0 -0
  91. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_compaction_strategy.py +0 -0
  92. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_config.py +0 -0
  93. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_content_parts.py +0 -0
  94. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_delegation_gate.py +0 -0
  95. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_error_taxonomy.py +0 -0
  96. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_fragment_bounds.py +0 -0
  97. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_gateway_catalog.py +0 -0
  98. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_golden.py +0 -0
  99. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_handoff.py +0 -0
  100. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_harness.py +0 -0
  101. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_harness_spec.py +0 -0
  102. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_history.py +0 -0
  103. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_history_persistence.py +0 -0
  104. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_hooks.py +0 -0
  105. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_in_memory_storage.py +0 -0
  106. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_llm_gemini_wire.py +0 -0
  107. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_llm_responses_wire.py +0 -0
  108. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_llm_stream_e2e.py +0 -0
  109. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_llm_wire_helpers.py +0 -0
  110. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_long_session.py +0 -0
  111. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_loop.py +0 -0
  112. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_loop_cancellation.py +0 -0
  113. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_loop_replay.py +0 -0
  114. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_loop_sandbox_event.py +0 -0
  115. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_loop_terminal.py +0 -0
  116. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_maintenance.py +0 -0
  117. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_mcp.py +0 -0
  118. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_mcp_server.py +0 -0
  119. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_model_catalog.py +0 -0
  120. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_model_equivalence.py +0 -0
  121. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_model_info.py +0 -0
  122. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_model_resolve.py +0 -0
  123. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_native_default.py +0 -0
  124. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_observation_aging.py +0 -0
  125. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_orchestration.py +0 -0
  126. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_otel.py +0 -0
  127. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_parallel_tools.py +0 -0
  128. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_plugins.py +0 -0
  129. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_provider_compat.py +0 -0
  130. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_provider_presets.py +0 -0
  131. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_pseudo.py +0 -0
  132. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_pyo3_api_surface.py +0 -0
  133. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_recording.py +0 -0
  134. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_reminders.py +0 -0
  135. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_replay_crosslang.py +0 -0
  136. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_resume.py +0 -0
  137. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_retry_hooks.py +0 -0
  138. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_safety_gate.py +0 -0
  139. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_sandboxed.py +0 -0
  140. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_skills.py +0 -0
  141. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_soft_timeout.py +0 -0
  142. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_spill.py +0 -0
  143. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_sqlite_storage.py +0 -0
  144. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_steer.py +0 -0
  145. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_storage_contract.py +0 -0
  146. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_stream_strip.py +0 -0
  147. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_subagent.py +0 -0
  148. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_system_proxy.py +0 -0
  149. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_todo.py +0 -0
  150. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_todo_gate.py +0 -0
  151. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_tokens.py +0 -0
  152. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_tool_exposure.py +0 -0
  153. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_tool_hygiene.py +0 -0
  154. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_tool_router.py +0 -0
  155. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_tool_schema.py +0 -0
  156. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_trace_recorder.py +0 -0
  157. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_transport_jsonrpc.py +0 -0
  158. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_transport_sse.py +0 -0
  159. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_usage_attribution.py +0 -0
  160. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_world_state.py +0 -0
  161. {steerable_agent_runtime-0.6.41 → steerable_agent_runtime-0.6.43}/tests/test_write_lease.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.41
3
+ Version: 0.6.43
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -8,7 +8,7 @@ Description-Content-Type: text/markdown
8
8
  Requires-Dist: pydantic>=2.10.0
9
9
  Requires-Dist: steerable-agent-protocol<1.0.0,>=0.1.0
10
10
  Requires-Dist: steerable-agent-harness<1.0.0,>=0.1.0
11
- Requires-Dist: steerable-agent-runtime-native==0.6.39
11
+ Requires-Dist: steerable-agent-runtime-native==0.6.40
12
12
  Provides-Extra: sqlalchemy
13
13
  Requires-Dist: sqlalchemy>=2.0; extra == "sqlalchemy"
14
14
  Provides-Extra: fastapi
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.41"
3
+ version = "0.6.43"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  license = { text = "Apache-2.0" }
6
6
  readme = "README.md"
@@ -16,7 +16,7 @@ dependencies = [
16
16
  "pydantic>=2.10.0",
17
17
  "steerable-agent-protocol>=0.1.0,<1.0.0",
18
18
  "steerable-agent-harness>=0.1.0,<1.0.0",
19
- "steerable-agent-runtime-native==0.6.39",
19
+ "steerable-agent-runtime-native==0.6.40",
20
20
  ]
21
21
 
22
22
  [project.optional-dependencies]
@@ -24,6 +24,7 @@ invoker — instead of being baked into one registry.
24
24
  from __future__ import annotations
25
25
 
26
26
  import asyncio
27
+ import contextvars
27
28
  import json
28
29
  import logging
29
30
  import os
@@ -249,6 +250,41 @@ def _default_resolver(call: ToolCall, ctx: LoopContext) -> ApprovalRequest:
249
250
  )
250
251
 
251
252
 
253
+ class ToolTimeoutClock:
254
+ """Pauses the loop's tool cap while an approval prompt is unanswered.
255
+
256
+ The cap still covers the command after the user decides. The object is
257
+ shared through a contextvar so the waiting tool and ``ApprovalExecutor``
258
+ see the same clock.
259
+ """
260
+
261
+ def __init__(self) -> None:
262
+ self._depth = 0
263
+ self.resumed = asyncio.Event()
264
+ self.resumed.set()
265
+ self.paused = asyncio.Event()
266
+
267
+ def suspend(self) -> None:
268
+ self._depth += 1
269
+ self.resumed.clear()
270
+ self.paused.set()
271
+
272
+ def resume(self) -> None:
273
+ self._depth = max(0, self._depth - 1)
274
+ if self._depth == 0:
275
+ self.paused.clear()
276
+ self.resumed.set()
277
+
278
+ @property
279
+ def suspended(self) -> bool:
280
+ return self._depth > 0
281
+
282
+
283
+ tool_timeout_clock: contextvars.ContextVar[ToolTimeoutClock | None] = contextvars.ContextVar(
284
+ "tool_timeout_clock", default=None
285
+ )
286
+
287
+
252
288
  class ApprovalExecutor:
253
289
  """``ToolExecutor`` decorator enforcing the approval algebra.
254
290
 
@@ -365,18 +401,27 @@ class ApprovalExecutor:
365
401
  return self._durable
366
402
 
367
403
  async def _ask(self, request: ApprovalRequest) -> ApprovalDecision:
368
- if self._timeout_s is None:
369
- return await self._approver.approve(request)
404
+ # No timeout means wait until the user decides. That wait is not
405
+ # part of the tool cap; the command after the decision still is.
406
+ clock = tool_timeout_clock.get() if self._timeout_s is None else None
407
+ if clock is not None:
408
+ clock.suspend()
370
409
  try:
371
- return await asyncio.wait_for(
372
- self._approver.approve(request), timeout=self._timeout_s
373
- )
374
- except (TimeoutError, asyncio.TimeoutError):
375
- # Fail closed, keeping the variant for observability (codex's
376
- # TimedOut semantics: no decision arrived, do not execute).
377
- return ApprovalDecision(
378
- "timed_out", f"approval request timed out after {self._timeout_s}s"
379
- )
410
+ if self._timeout_s is None:
411
+ return await self._approver.approve(request)
412
+ try:
413
+ return await asyncio.wait_for(
414
+ self._approver.approve(request), timeout=self._timeout_s
415
+ )
416
+ except (TimeoutError, asyncio.TimeoutError):
417
+ # Fail closed, keeping the variant for observability (codex's
418
+ # TimedOut semantics: no decision arrived, do not execute).
419
+ return ApprovalDecision(
420
+ "timed_out", f"approval request timed out after {self._timeout_s}s"
421
+ )
422
+ finally:
423
+ if clock is not None:
424
+ clock.resume()
380
425
 
381
426
  def _persist(self, request: ApprovalRequest, decision: ApprovalDecision) -> None:
382
427
  if decision.kind in _SESSION_KINDS and self._session is not None:
@@ -15,8 +15,9 @@ record and the model's next context. This mirrors dsh's ``ask_user_question``
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import inspect
18
19
  from collections.abc import Awaitable, Callable
19
- from typing import Any, Protocol
20
+ from typing import Any, Protocol, cast
20
21
 
21
22
  from .errors import ToolDispatchError
22
23
 
@@ -220,6 +221,11 @@ class AskUserHandler(Protocol):
220
221
  mapping of question id → answer (a string, or a list of strings for a
221
222
  multi-select). Implementations must not raise on a user cancel — return
222
223
  an empty mapping so the loop records "no answer" and moves on.
224
+
225
+ A handler that declares ``context`` also receives the loop dispatch
226
+ context (``chat_id``, ``round``) so the product can scope the prompt to
227
+ the chat that asked. Handlers that omit the parameter are called without
228
+ it.
223
229
  """
224
230
 
225
231
  def __call__(
@@ -238,10 +244,13 @@ def make_ask_user_tool(handler: AskUserHandler) -> Callable[..., Awaitable[dict[
238
244
  """
239
245
 
240
246
  async def ask_user(
241
- intro: str, questions: list[dict[str, Any]], outro: str | None = None
247
+ intro: str,
248
+ questions: list[dict[str, Any]],
249
+ outro: str | None = None,
250
+ context: dict[str, Any] | None = None,
242
251
  ) -> dict[str, Any]:
243
252
  normalized = normalize_ask_user_questions(questions)
244
- answers = await handler(intro, normalized)
253
+ answers = await _call_handler(handler, intro, normalized, context)
245
254
  return {
246
255
  "intro": intro,
247
256
  "outro": outro,
@@ -263,3 +272,32 @@ def make_ask_user_tool(handler: AskUserHandler) -> Callable[..., Awaitable[dict[
263
272
  "exposure": "direct",
264
273
  }
265
274
  return ask_user
275
+
276
+
277
+ def _call_handler(
278
+ handler: AskUserHandler,
279
+ intro: str,
280
+ questions: list[dict[str, Any]],
281
+ context: dict[str, Any] | None,
282
+ ) -> Awaitable[dict[str, Any]]:
283
+ """Pass ``context`` only when the handler declares it.
284
+
285
+ Existing handlers take ``(intro, questions)``. The desktop host handler
286
+ also takes ``context`` so the question card can be scoped to ``chat_id``.
287
+ """
288
+ try:
289
+ signature = inspect.signature(handler)
290
+ except (TypeError, ValueError):
291
+ return handler(intro, questions)
292
+ accepts_context = any(
293
+ parameter.name == "context"
294
+ or parameter.kind is inspect.Parameter.VAR_KEYWORD
295
+ for parameter in signature.parameters.values()
296
+ )
297
+ if accepts_context:
298
+ contextual = cast(
299
+ Callable[..., Awaitable[dict[str, Any]]],
300
+ handler,
301
+ )
302
+ return contextual(intro, questions, context=context)
303
+ return handler(intro, questions)
@@ -61,6 +61,7 @@ from typing import Any, Literal, Protocol, runtime_checkable
61
61
  from steerable_agent_harness import BudgetLimit
62
62
  from steerable_agent_protocol.generated import ToolCall, ToolResult
63
63
 
64
+ from .approval import ToolTimeoutClock, tool_timeout_clock
64
65
  from .history import (
65
66
  KIND_ASSISTANT,
66
67
  KIND_SYSTEM,
@@ -673,14 +674,25 @@ class CoreLoop:
673
674
  blocked by a hung peer again.
674
675
  """
675
676
 
677
+ # ask_user blocks until the user answers. The generic tool cap would
678
+ # cancel that wait (default 5 minutes) and continue without an answer.
679
+ if call.name == "ask_user":
680
+ return await self._executor.execute(call, ctx)
676
681
  timeout_ms = self._effective_tool_timeout_ms()
677
682
  if timeout_ms is None:
678
683
  return await self._executor.execute(call, ctx)
684
+ clock = ToolTimeoutClock()
685
+ token = tool_timeout_clock.set(clock)
686
+ task = asyncio.create_task(self._executor.execute(call, ctx))
679
687
  try:
680
- return await asyncio.wait_for(
681
- self._executor.execute(call, ctx), timeout=timeout_ms / 1000
682
- )
683
- except asyncio.TimeoutError:
688
+ return await _await_tool_deadline(task, clock, timeout_ms / 1000)
689
+ except asyncio.CancelledError:
690
+ task.cancel()
691
+ await asyncio.gather(task, return_exceptions=True)
692
+ raise
693
+ except TimeoutError:
694
+ task.cancel()
695
+ await asyncio.gather(task, return_exceptions=True)
684
696
  return ToolResult(
685
697
  success=False,
686
698
  error="tool_timeout",
@@ -693,6 +705,8 @@ class CoreLoop:
693
705
  ),
694
706
  },
695
707
  )
708
+ finally:
709
+ tool_timeout_clock.reset(token)
696
710
 
697
711
  def _effective_tool_timeout_ms(self) -> int | None:
698
712
  """Resolve the wall-clock cap for the tool about to run.
@@ -848,6 +862,34 @@ _TOOL_TIMEOUT_MESSAGE = (
848
862
  )
849
863
 
850
864
 
865
+ async def _await_tool_deadline(
866
+ task: asyncio.Task[ToolResult], clock: ToolTimeoutClock, timeout_s: float
867
+ ) -> ToolResult:
868
+ """Wait for a tool, freezing the deadline while approval is unanswered."""
869
+ loop = asyncio.get_running_loop()
870
+ deadline = loop.time() + timeout_s
871
+ while True:
872
+ if clock.suspended:
873
+ resume = asyncio.create_task(clock.resumed.wait())
874
+ done, _pending = await asyncio.wait(
875
+ {task, resume}, return_when=asyncio.FIRST_COMPLETED
876
+ )
877
+ resume.cancel()
878
+ if task in done:
879
+ return task.result()
880
+ continue
881
+ remaining = deadline - loop.time()
882
+ if remaining <= 0:
883
+ raise TimeoutError
884
+ pause = asyncio.create_task(clock.paused.wait())
885
+ done, _pending = await asyncio.wait(
886
+ {task, pause}, timeout=remaining, return_when=asyncio.FIRST_COMPLETED
887
+ )
888
+ pause.cancel()
889
+ if task in done:
890
+ return task.result()
891
+
892
+
851
893
  def _honors_forced_tool_choice(provider: LLMProvider, tool_choice: str) -> bool:
852
894
  """Whether ``tool_choice`` survives to the wire on this provider.
853
895
 
@@ -82,7 +82,10 @@ def _config_json(loop: CoreLoop) -> str:
82
82
  "persist_tool_results": config.persist_tool_results,
83
83
  "parallel_tools": config.parallel_tools,
84
84
  "tool_dedup": config.tool_dedup,
85
- "tool_timeout_ms": config.tool_timeout_ms,
85
+ # Python `_execute_tool` owns this cap and exempts `ask_user` (it
86
+ # blocks until the user answers). The native loop would apply the
87
+ # same number to every call, including that wait.
88
+ "tool_timeout_ms": None,
86
89
  "soft_timeout_ms": config.soft_timeout_ms,
87
90
  "wrap_up_keeps_tools": config.wrap_up_keeps_tools,
88
91
  "wrap_up_max_tool_rounds": config.wrap_up_max_tool_rounds,
@@ -685,7 +688,14 @@ async def run_native(
685
688
  for task in tuple(active_stream_tasks):
686
689
  task.cancel()
687
690
 
688
- aio_loop.call_soon_threadsafe(cancel_now)
691
+ try:
692
+ current_loop = asyncio.get_running_loop()
693
+ except RuntimeError:
694
+ current_loop = None
695
+ if current_loop is aio_loop:
696
+ cancel_now()
697
+ else:
698
+ aio_loop.call_soon_threadsafe(cancel_now)
689
699
 
690
700
  def cancel() -> None:
691
701
  cancel_flag.set()
@@ -1006,6 +1016,11 @@ async def run_native(
1006
1016
  yield event
1007
1017
  finally:
1008
1018
  consumed.set()
1019
+ remaining_tasks = tuple(active_tool_tasks | active_stream_tasks)
1020
+ for task in remaining_tasks:
1021
+ task.cancel()
1022
+ if remaining_tasks:
1023
+ await asyncio.gather(*remaining_tasks, return_exceptions=True)
1009
1024
  if worker_error:
1010
1025
  raise worker_error[0]
1011
1026
  raw_history = worker_result[0].get("history", []) if worker_result else []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.41
3
+ Version: 0.6.43
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -8,7 +8,7 @@ Description-Content-Type: text/markdown
8
8
  Requires-Dist: pydantic>=2.10.0
9
9
  Requires-Dist: steerable-agent-protocol<1.0.0,>=0.1.0
10
10
  Requires-Dist: steerable-agent-harness<1.0.0,>=0.1.0
11
- Requires-Dist: steerable-agent-runtime-native==0.6.39
11
+ Requires-Dist: steerable-agent-runtime-native==0.6.40
12
12
  Provides-Extra: sqlalchemy
13
13
  Requires-Dist: sqlalchemy>=2.0; extra == "sqlalchemy"
14
14
  Provides-Extra: fastapi
@@ -1,7 +1,7 @@
1
1
  pydantic>=2.10.0
2
2
  steerable-agent-protocol<1.0.0,>=0.1.0
3
3
  steerable-agent-harness<1.0.0,>=0.1.0
4
- steerable-agent-runtime-native==0.6.39
4
+ steerable-agent-runtime-native==0.6.40
5
5
 
6
6
  [all]
7
7
  sqlalchemy>=2.0
@@ -21,7 +21,9 @@ from steerable_agent_runtime import (
21
21
  LoopEvent,
22
22
  RouterToolExecutor,
23
23
  ToolRouter,
24
+ make_ask_user_tool,
24
25
  )
26
+ from steerable_agent_runtime.approval import ApprovalDecision, ApprovalExecutor
25
27
  from steerable_agent_runtime.llm import LLMMessage, LLMStreamChunk
26
28
 
27
29
 
@@ -227,6 +229,93 @@ async def test_slow_tool_under_the_limit_is_untouched() -> None:
227
229
  assert results_of(events)[0].data["success"] is True
228
230
 
229
231
 
232
+ @pytest.mark.asyncio
233
+ async def test_ask_user_waits_for_the_answer_past_the_tool_timeout() -> None:
234
+ provider = make_provider(
235
+ [
236
+ {
237
+ "content": "",
238
+ "tool_calls": [
239
+ tc(
240
+ "ask_user",
241
+ {
242
+ "intro": "Pick one",
243
+ "questions": [
244
+ {
245
+ "id": "color",
246
+ "text": "Which color?",
247
+ "type": "select",
248
+ "options": ["red", "blue"],
249
+ "multiSelect": False,
250
+ }
251
+ ],
252
+ },
253
+ )
254
+ ],
255
+ },
256
+ {"content": "done"},
257
+ ]
258
+ )
259
+ router = ToolRouter()
260
+
261
+ async def handler(intro: str, questions: list[dict[str, Any]]) -> dict[str, str]:
262
+ await asyncio.sleep(0.05)
263
+ return {"color": "blue"}
264
+
265
+ fn = make_ask_user_tool(handler)
266
+ meta = fn.__steerable_tool_meta__
267
+ router.register(
268
+ fn,
269
+ name=meta["name"],
270
+ mode=meta["mode"],
271
+ description=meta["description"],
272
+ schema=meta["schema"],
273
+ require_consent=meta["require_consent"],
274
+ concurrency_safe=meta["concurrency_safe"],
275
+ exposure=meta["exposure"],
276
+ )
277
+ loop = CoreLoop(
278
+ provider,
279
+ RouterToolExecutor(router),
280
+ LoopConfig(tool_timeout_ms=20),
281
+ )
282
+ events = await collect(loop.run([LLMMessage.text_of("user", "go")]))
283
+ result = results_of(events)[0].data
284
+ assert result["success"] is True, result
285
+ assert "blue" in str(result)
286
+
287
+
288
+ @pytest.mark.asyncio
289
+ async def test_approval_wait_is_not_part_of_the_tool_timeout() -> None:
290
+ provider = make_provider(
291
+ [
292
+ {"content": "", "tool_calls": [tc("shell", {"cmd": "ls"})]},
293
+ {"content": "done"},
294
+ ]
295
+ )
296
+ router = ToolRouter()
297
+
298
+ async def shell(cmd: str) -> str:
299
+ return f"ran {cmd}"
300
+
301
+ router.register(shell)
302
+
303
+ class _SlowApprover:
304
+ async def approve(self, request: Any) -> ApprovalDecision:
305
+ await asyncio.sleep(0.05)
306
+ return ApprovalDecision("allow_once")
307
+
308
+ loop = CoreLoop(
309
+ provider,
310
+ ApprovalExecutor(RouterToolExecutor(router), _SlowApprover()),
311
+ LoopConfig(tool_timeout_ms=20),
312
+ )
313
+ events = await collect(loop.run([LLMMessage.text_of("user", "go")]))
314
+ result = results_of(events)[0].data
315
+ assert result["success"] is True, result
316
+ assert "ran ls" in str(result)
317
+
318
+
230
319
  def test_default_is_a_generous_backstop() -> None:
231
320
  assert LoopConfig().tool_timeout_ms == 300_000
232
321