steerable-agent-runtime 0.6.28__tar.gz → 0.6.30__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.28 → steerable_agent_runtime-0.6.30}/PKG-INFO +9 -1
  2. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/README.md +8 -0
  3. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/pyproject.toml +1 -1
  4. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/__init__.py +2 -0
  5. steerable_agent_runtime-0.6.30/src/steerable_agent_runtime/delegation_gate.py +100 -0
  6. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/loop.py +16 -5
  7. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/native_bridge.py +206 -78
  8. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/orchestration.py +4 -4
  9. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/pool.py +3 -0
  10. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/subagent.py +34 -5
  11. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime.egg-info/PKG-INFO +9 -1
  12. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime.egg-info/SOURCES.txt +3 -0
  13. steerable_agent_runtime-0.6.30/tests/test_delegation_gate.py +108 -0
  14. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_loop.py +129 -1
  15. steerable_agent_runtime-0.6.30/tests/test_native_default.py +44 -0
  16. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_soft_timeout.py +73 -0
  17. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_subagent.py +74 -0
  18. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/setup.cfg +0 -0
  19. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/antihallucination.py +0 -0
  20. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/approval.py +0 -0
  21. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/approval_policy.py +0 -0
  22. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/ask_user.py +0 -0
  23. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/branch.py +0 -0
  24. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/cache_control.py +0 -0
  25. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/calibration.py +0 -0
  26. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/compaction.py +0 -0
  27. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/compaction_policy.py +0 -0
  28. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/config.py +0 -0
  29. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/default.harness.json +0 -0
  30. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  31. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/errors.py +0 -0
  32. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  33. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/handoff.py +0 -0
  34. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/harness.py +0 -0
  35. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/harness_spec.py +0 -0
  36. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/history.py +0 -0
  37. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/hooks.py +0 -0
  38. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  39. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  40. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/compat.py +0 -0
  41. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/errors.py +0 -0
  42. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  43. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  44. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  45. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/parts.py +0 -0
  46. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/presets.py +0 -0
  47. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  48. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/maintenance.py +0 -0
  49. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/mcp.py +0 -0
  50. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/mcp_server.py +0 -0
  51. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/model_catalog.py +0 -0
  52. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/model_info.py +0 -0
  53. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/model_resolve.py +0 -0
  54. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/observation_aging.py +0 -0
  55. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/otel.py +0 -0
  56. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/plugins.py +0 -0
  57. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/pricing.py +0 -0
  58. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/pseudo.py +0 -0
  59. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/recording.py +0 -0
  60. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/reminders.py +0 -0
  61. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/replay.py +0 -0
  62. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/resume.py +0 -0
  63. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/retry.py +0 -0
  64. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/sandboxed.py +0 -0
  65. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/skills.py +0 -0
  66. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/spill.py +0 -0
  67. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  68. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  69. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  70. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  71. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  72. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/todo.py +0 -0
  73. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/tokens.py +0 -0
  74. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/tool_schema.py +0 -0
  75. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/tool_search.py +0 -0
  76. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/tools.py +0 -0
  77. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/tracing.py +0 -0
  78. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  79. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  80. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  81. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/world_state.py +0 -0
  82. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  83. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
  84. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  85. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_antihallucination.py +0 -0
  86. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_approval.py +0 -0
  87. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_approval_policy.py +0 -0
  88. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_ask_user.py +0 -0
  89. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_branch.py +0 -0
  90. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_cache_control.py +0 -0
  91. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_cache_instrumentation.py +0 -0
  92. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_calibration.py +0 -0
  93. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_compaction.py +0 -0
  94. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_compaction_policy.py +0 -0
  95. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_compaction_strategy.py +0 -0
  96. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_config.py +0 -0
  97. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_content_parts.py +0 -0
  98. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_error_taxonomy.py +0 -0
  99. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_fragment_bounds.py +0 -0
  100. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_gateway_catalog.py +0 -0
  101. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_golden.py +0 -0
  102. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_handoff.py +0 -0
  103. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_harness.py +0 -0
  104. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_harness_spec.py +0 -0
  105. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_history.py +0 -0
  106. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_history_persistence.py +0 -0
  107. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_hooks.py +0 -0
  108. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_in_memory_storage.py +0 -0
  109. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_llm_gemini_wire.py +0 -0
  110. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_llm_responses_wire.py +0 -0
  111. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_llm_stream_e2e.py +0 -0
  112. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_llm_wire_helpers.py +0 -0
  113. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_long_session.py +0 -0
  114. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_loop_cancellation.py +0 -0
  115. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_loop_replay.py +0 -0
  116. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_loop_sandbox_event.py +0 -0
  117. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_loop_terminal.py +0 -0
  118. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_maintenance.py +0 -0
  119. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_mcp.py +0 -0
  120. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_mcp_server.py +0 -0
  121. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_model_catalog.py +0 -0
  122. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_model_equivalence.py +0 -0
  123. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_model_info.py +0 -0
  124. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_model_resolve.py +0 -0
  125. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_observation_aging.py +0 -0
  126. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_orchestration.py +0 -0
  127. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_otel.py +0 -0
  128. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_parallel_tools.py +0 -0
  129. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_plugins.py +0 -0
  130. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_provider_compat.py +0 -0
  131. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_provider_presets.py +0 -0
  132. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_pseudo.py +0 -0
  133. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_pyo3_api_surface.py +0 -0
  134. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_recording.py +0 -0
  135. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_reminders.py +0 -0
  136. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_replay_crosslang.py +0 -0
  137. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_resume.py +0 -0
  138. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_retry_hooks.py +0 -0
  139. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_safety_gate.py +0 -0
  140. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_sandboxed.py +0 -0
  141. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_skills.py +0 -0
  142. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_spill.py +0 -0
  143. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_sqlite_storage.py +0 -0
  144. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_steer.py +0 -0
  145. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_storage_contract.py +0 -0
  146. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_stream_strip.py +0 -0
  147. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_system_proxy.py +0 -0
  148. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_todo.py +0 -0
  149. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_todo_gate.py +0 -0
  150. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tokens.py +0 -0
  151. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tool_exposure.py +0 -0
  152. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tool_hygiene.py +0 -0
  153. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tool_router.py +0 -0
  154. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tool_schema.py +0 -0
  155. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tool_timeout.py +0 -0
  156. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_trace_recorder.py +0 -0
  157. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_transport_jsonrpc.py +0 -0
  158. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_transport_sse.py +0 -0
  159. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_usage_attribution.py +0 -0
  160. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_world_state.py +0 -0
  161. {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/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.28
3
+ Version: 0.6.30
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -59,3 +59,11 @@ pip install "steerable-agent-runtime[anthropic]"
59
59
  pip install "steerable-agent-runtime[sqlalchemy]"
60
60
  pip install "steerable-agent-runtime[fastapi]"
61
61
  ```
62
+
63
+ The CoreLoop defaults to the Rust binding when
64
+ `steerable-agent-runtime-native` is importable (`STEERABLE_RUST_CORELOOP=0`
65
+ keeps the Python loop):
66
+
67
+ ```bash
68
+ pip install steerable-agent-runtime-native
69
+ ```
@@ -34,3 +34,11 @@ pip install "steerable-agent-runtime[anthropic]"
34
34
  pip install "steerable-agent-runtime[sqlalchemy]"
35
35
  pip install "steerable-agent-runtime[fastapi]"
36
36
  ```
37
+
38
+ The CoreLoop defaults to the Rust binding when
39
+ `steerable-agent-runtime-native` is importable (`STEERABLE_RUST_CORELOOP=0`
40
+ keeps the Python loop):
41
+
42
+ ```bash
43
+ pip install steerable-agent-runtime-native
44
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.28"
3
+ version = "0.6.30"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -51,6 +51,7 @@ from .todo import (
51
51
  make_todo_write_tool,
52
52
  todo_write_tool_descriptor,
53
53
  )
54
+ from .delegation_gate import RequiredDelegationGate
54
55
  from .config import (
55
56
  DEFAULT_CONFIG_PATH,
56
57
  ConfigError,
@@ -387,6 +388,7 @@ __all__ = [
387
388
  "RecordingProvider",
388
389
  "RegisteredTool",
389
390
  "RequestSink",
391
+ "RequiredDelegationGate",
390
392
  "RetryAction",
391
393
  "RetryHooks",
392
394
  "RewriteRequest",
@@ -0,0 +1,100 @@
1
+ """Required-delegation completion gate.
2
+
3
+ A host that names sub-agents for a turn (the desktop's ``@`` mentions) can
4
+ only *ask* the model to delegate: the dispatch instruction is prompt text,
5
+ and every existing discipline guard lets a narrated hand-off through. The
6
+ claimed-execution check in ``antihallucination`` fires only on a round with
7
+ no tool calls at all, so a turn that ran unrelated tools and then wrote "I
8
+ launched the three agents" ends ``completed`` with zero delegations.
9
+
10
+ This gate closes that hole: the host declares which profiles must receive a
11
+ delegation, the gate watches ``delegate_subagent`` calls, and a ``completed``
12
+ draft that skipped one is converted into a retry naming the missing
13
+ profiles.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from collections.abc import Iterable
19
+ from typing import Any
20
+
21
+ from steerable_agent_protocol.generated import ToolCall, ToolResult
22
+
23
+ from .hooks import CompletionAction, CompletionDraft, NoopHooks
24
+
25
+ #: Retries this gate grants before accepting a turn that skipped a profile.
26
+ #: The loop's own completion-redo budget bounds the total; this keeps one
27
+ #: uncooperative model from spending all of it on delegation alone.
28
+ DEFAULT_MAX_RETRIES = 2
29
+
30
+
31
+ class RequiredDelegationGate(NoopHooks):
32
+ """``before_completion`` veto: every required profile must be delegated.
33
+
34
+ A profile counts as satisfied once a delegation was *attempted*, whether
35
+ the child succeeded or not — a child that failed (its own tool error, a
36
+ budget wall) is the parent's to report, and re-delegating to it would
37
+ spin. Only a profile the model never called at all blocks finishing.
38
+
39
+ One instance per run: it accumulates the turn's delegations.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ required_profiles: Iterable[str],
45
+ *,
46
+ tool_name: str = "delegate_subagent",
47
+ max_retries: int = DEFAULT_MAX_RETRIES,
48
+ ) -> None:
49
+ """
50
+ @param required_profiles: profile names that must each receive a
51
+ delegation; duplicates and blanks are dropped.
52
+ @param tool_name: the delegation tool to watch.
53
+ @param max_retries: retries granted before accepting a skip.
54
+ """
55
+ self._required = tuple(
56
+ dict.fromkeys(name.strip() for name in required_profiles if name.strip())
57
+ )
58
+ self._tool_name = tool_name
59
+ self._max_retries = max_retries
60
+ self._delegated: set[str] = set()
61
+ self._retries = 0
62
+
63
+ async def post_tool_result(
64
+ self, result: ToolResult, call: ToolCall, ctx: Any
65
+ ) -> ToolResult:
66
+ """Record the delegated profile, passing the result through."""
67
+ if call.name == self._tool_name:
68
+ profile = str(call.arguments.get("subagent_type") or "").strip()
69
+ if profile:
70
+ self._delegated.add(profile)
71
+ return result
72
+
73
+ async def before_completion(
74
+ self, draft: CompletionDraft, ctx: Any
75
+ ) -> CompletionAction:
76
+ """Retry while a required profile has no delegation, else accept.
77
+
78
+ Only ``completed`` is gated: ``budget_exhausted`` and error stops have
79
+ their own continuation paths (the host's auto-continue resumes with a
80
+ fresh budget), and vetoing them here would fight those.
81
+ """
82
+ if draft.status != "completed":
83
+ return CompletionAction(kind="accept")
84
+ missing = [name for name in self._required if name not in self._delegated]
85
+ if not missing or self._retries >= self._max_retries:
86
+ return CompletionAction(kind="accept")
87
+ self._retries += 1
88
+ listing = "、".join(missing)
89
+ return CompletionAction(
90
+ kind="retry",
91
+ message=(
92
+ f"用户点名的子代理还没拿到任务:{listing}。"
93
+ "现在就用 delegate_subagent 给每一个补上一份自包含的子任务"
94
+ "(subagent_type 填画像名),不要用文字描述代替调用。"
95
+ ),
96
+ reason="required_delegation_missing",
97
+ )
98
+
99
+
100
+ __all__ = ["DEFAULT_MAX_RETRIES", "RequiredDelegationGate"]
@@ -545,6 +545,16 @@ class CoreLoop:
545
545
  """
546
546
  self._compact_event.set()
547
547
 
548
+ @property
549
+ def record_id(self) -> str | None:
550
+ """Durable-record id this loop appends to, when one was configured.
551
+
552
+ Hosts that want to read a run's transcript back (a pooled child's
553
+ process view) need the id the loop writes under, not the one they
554
+ guessed.
555
+ """
556
+ return self._record_id
557
+
548
558
  @property
549
559
  def last_run_usage(self) -> LLMUsage | None:
550
560
  """Accumulated billable usage of the current/last run (W6-9).
@@ -848,12 +858,13 @@ class CoreLoop:
848
858
  tools: list[dict[str, Any]] | None = None,
849
859
  chat_id: str | None = None,
850
860
  ) -> AsyncIterator[LoopEvent]:
851
- if os.environ.get("STEERABLE_RUST_CORELOOP") == "1":
852
- from .native_bridge import run_native
861
+ if os.environ.get("STEERABLE_RUST_CORELOOP") != "0":
862
+ from .native_bridge import run_native, uses_native_coreloop
853
863
 
854
- async for event in run_native(self, messages, tools=tools, chat_id=chat_id):
855
- yield event
856
- return
864
+ if uses_native_coreloop():
865
+ async for event in run_native(self, messages, tools=tools, chat_id=chat_id):
866
+ yield event
867
+ return
857
868
  ctx = LoopContext(chat_id=chat_id)
858
869
  self._run_context = ctx
859
870
  # The transcript is a projection of the append-only record; nothing
@@ -1,7 +1,9 @@
1
1
  """Drive the Rust CoreLoop from Python via `steerable_agent_runtime_native`.
2
2
 
3
3
  Python keeps the provider, executor, and hooks (async, GIL-friendly). Rust
4
- owns think → act → observe. Enabled when ``STEERABLE_RUST_CORELOOP=1``.
4
+ owns think → act → observe. The Rust binding is the default when the
5
+ module is importable; ``STEERABLE_RUST_CORELOOP=0`` forces Python and
6
+ ``=1`` still forces Rust.
5
7
  """
6
8
 
7
9
  from __future__ import annotations
@@ -9,6 +11,7 @@ from __future__ import annotations
9
11
  import asyncio
10
12
  import json
11
13
  import logging
14
+ import os
12
15
  import queue
13
16
  import threading
14
17
  import time
@@ -40,7 +43,25 @@ def native_available() -> bool:
40
43
  return True
41
44
 
42
45
 
46
+ def uses_native_coreloop() -> bool:
47
+ """Prefer Rust when the PyO3 module is importable.
48
+
49
+ ``STEERABLE_RUST_CORELOOP=0`` keeps the Python loop. ``=1`` selects
50
+ Rust even if the caller has not probed availability first (the
51
+ subsequent import then fails loudly). Unset follows
52
+ :func:`native_available`.
53
+ """
54
+ flag = os.environ.get("STEERABLE_RUST_CORELOOP")
55
+ if flag == "0":
56
+ return False
57
+ if flag == "1":
58
+ return True
59
+ return native_available()
60
+
61
+
43
62
  def _config_json(loop: CoreLoop) -> str:
63
+ import steerable_agent_runtime.loop as loop_module
64
+
44
65
  config = loop._config
45
66
  payload: dict[str, Any] = {
46
67
  "max_rounds": config.max_rounds,
@@ -54,6 +75,10 @@ def _config_json(loop: CoreLoop) -> str:
54
75
  "wrap_up_max_tool_rounds": config.wrap_up_max_tool_rounds,
55
76
  "wrap_up_tool_timeout_ms": config.wrap_up_tool_timeout_ms,
56
77
  "wrap_up_hard_cap_ms": config.wrap_up_hard_cap_ms,
78
+ "idle_stream_timeout_ms": config.idle_stream_timeout_ms,
79
+ "idle_stream_max_chars": config.idle_stream_max_chars,
80
+ "reasoning_without_progress_chars": config.reasoning_without_progress_chars,
81
+ "idle_reasoning_gap_ms": int(loop_module._IDLE_REASONING_GAP_SEC * 1000),
57
82
  "steer_mode": config.steer_mode,
58
83
  }
59
84
  if config.budget is not None:
@@ -167,7 +192,12 @@ async def _collect_stream(
167
192
  cancel_flag: threading.Event,
168
193
  run_started: float,
169
194
  surrogate_mode: list[bool],
170
- ) -> str:
195
+ output: queue.Queue[str],
196
+ native_stream_id: str,
197
+ ) -> None:
198
+ def send(kind: str, **payload: Any) -> None:
199
+ output.put(json.dumps({"kind": kind, **payload}))
200
+
171
201
  request = json.loads(messages_json)
172
202
  if isinstance(request, dict):
173
203
  message_items = request.get("messages", [])
@@ -183,31 +213,40 @@ async def _collect_stream(
183
213
  and message.content_text.startswith("[system notice] The time budget")
184
214
  for message in messages
185
215
  )
216
+ stream_kwargs: dict[str, Any] = {}
217
+ if tool_choice:
218
+ stream_kwargs["tool_choice"] = tool_choice
219
+ if loop._config.temperature is not None:
220
+ stream_kwargs["temperature"] = loop._config.temperature
221
+ if loop._config.max_tokens is not None:
222
+ stream_kwargs["max_tokens"] = loop._config.max_tokens
186
223
  stream = loop._provider.stream(
187
224
  messages,
188
225
  tools=tools if tools_enabled else None,
189
- **({"tool_choice": tool_choice} if tool_choice else {}),
226
+ **stream_kwargs,
190
227
  )
191
- chunks: list[dict[str, Any]] = []
192
228
  stream_started = time.monotonic()
193
- last_chunk_at = stream_started
194
- active_ms = 0
195
- stream_chars = 0
196
229
  tool_seen = False
197
230
  raw_content: list[str] = []
198
231
  stripper = PseudoStreamStripper()
199
232
  content_carry = ""
200
233
  cut_finish_reason: str | None = None
201
- import steerable_agent_runtime.loop as loop_module
202
-
203
234
  try:
204
235
  while True:
205
236
  soft_timeout_ms = loop._config.soft_timeout_ms
206
237
  wait_seconds = None
207
- if soft_timeout_ms is not None and not wrap_up_request:
238
+ timeout_finish_reason = "__soft_timeout_cut__"
239
+ if wrap_up_request and loop._config.wrap_up_tool_timeout_ms is not None:
240
+ wait_seconds = max(
241
+ loop._config.wrap_up_tool_timeout_ms / 1000
242
+ - (time.monotonic() - stream_started),
243
+ 0.05,
244
+ )
245
+ timeout_finish_reason = "__wrap_stream_cut__"
246
+ elif soft_timeout_ms is not None and not wrap_up_request:
208
247
  wait_seconds = max(
209
248
  soft_timeout_ms / 1000 - (time.monotonic() - run_started),
210
- 0.000_001,
249
+ 0.05,
211
250
  )
212
251
  try:
213
252
  if wait_seconds is None:
@@ -217,13 +256,14 @@ async def _collect_stream(
217
256
  except StopAsyncIteration:
218
257
  break
219
258
  except TimeoutError:
220
- cut_finish_reason = "__soft_timeout_cut__"
259
+ cut_finish_reason = timeout_finish_reason
221
260
  break
222
- now = time.monotonic()
223
- gap = now - last_chunk_at
224
- last_chunk_at = now
225
- if gap <= loop_module._IDLE_REASONING_GAP_SEC:
226
- active_ms += int(gap * 1000)
261
+ observe_chunk = getattr(loop._hooks, "on_stream_chunk", None)
262
+ if callable(observe_chunk):
263
+ try:
264
+ observe_chunk(chunk, loop._run_context)
265
+ except Exception: # noqa: BLE001 — observation cannot break streaming
266
+ logger.exception("native_bridge_on_stream_chunk_failed")
227
267
  payload = _chunk_to_json(chunk)
228
268
  if chunk.content_delta:
229
269
  raw_content.append(chunk.content_delta)
@@ -235,71 +275,43 @@ async def _collect_stream(
235
275
  ):
236
276
  surrogate_mode[0] = True
237
277
  payload["content_delta"] = stripper.feed(emit_text) or None
238
- chunks.append(payload)
278
+ send("chunk", chunk=payload)
239
279
  if chunk.tool_call_delta is not None:
240
280
  tool_seen = True
241
- loop._run_context.reasoning_since_progress = 0
242
- if not tool_seen:
243
- delta_chars = len(chunk.reasoning_delta or "") + len(
244
- chunk.content_delta or ""
245
- )
246
- stream_chars += delta_chars
247
- loop._run_context.reasoning_since_progress += len(
248
- chunk.reasoning_delta or ""
249
- )
250
- trigger = None
251
- idle_ms = loop._config.idle_stream_timeout_ms
252
- max_chars = loop._config.idle_stream_max_chars
253
- stale_cap = loop._config.reasoning_without_progress_chars
254
- if idle_ms is not None and active_ms >= idle_ms:
255
- trigger = "active_ms"
256
- elif max_chars is not None and stream_chars >= max_chars:
257
- trigger = "chars"
258
- elif (
259
- stale_cap is not None
260
- and loop._run_context.reasoning_since_progress >= stale_cap
261
- ):
262
- trigger = "no_progress"
263
- if trigger is not None:
264
- stale_chars = loop._run_context.reasoning_since_progress
265
- cut_finish_reason = (
266
- f"__idle_stream_cut__:{trigger}:{stream_chars}:"
267
- f"{stale_chars}"
268
- )
269
- if trigger == "no_progress":
270
- loop._run_context.reasoning_since_progress = 0
271
- break
272
281
  if cancel_flag.is_set() or loop._cancel_event.is_set():
273
282
  break
283
+ except asyncio.CancelledError:
284
+ send("error", error="cancelled", error_kind="transport")
285
+ return
274
286
  except Exception as exc: # noqa: BLE001 — Rust applies on_request_error
275
- return json.dumps(
276
- {
277
- "ok": False,
278
- "error": str(exc),
279
- "kind": getattr(exc, "kind", "unknown"),
280
- "status_code": getattr(exc, "status_code", None),
281
- "provider": getattr(exc, "provider", None),
282
- "retry_after_ms": getattr(exc, "retry_after_ms", None),
283
- }
287
+ send(
288
+ "error",
289
+ error=str(exc),
290
+ error_kind=getattr(exc, "kind", "unknown"),
291
+ status_code=getattr(exc, "status_code", None),
292
+ provider=getattr(exc, "provider", None),
293
+ retry_after_ms=getattr(exc, "retry_after_ms", None),
284
294
  )
295
+ return
285
296
  display_tail = stripper.feed(content_carry) + stripper.flush()
286
297
  if display_tail:
287
- chunks.append({"content_delta": display_tail})
298
+ send("chunk", chunk={"content_delta": display_tail})
288
299
  if cut_finish_reason is not None:
289
- chunks.append({"finish_reason": cut_finish_reason})
300
+ send("chunk", chunk={"finish_reason": cut_finish_reason})
290
301
  elif not tool_seen:
291
302
  recovered, _ = extract_inline_tool_calls("".join(raw_content))
292
303
  for index, call in enumerate(recovered):
293
- chunks.append(
294
- {
304
+ send(
305
+ "chunk",
306
+ chunk={
295
307
  "tool_call_delta": {
296
- "id": f"recovered_{index}",
308
+ "id": f"recovered_{native_stream_id}_{index}",
297
309
  "name": call["name"],
298
310
  "arguments": call.get("arguments") or {},
299
311
  }
300
- }
312
+ },
301
313
  )
302
- return json.dumps(chunks)
314
+ send("done")
303
315
 
304
316
 
305
317
  async def _exec_tool(loop: CoreLoop, call_json: str) -> str:
@@ -351,9 +363,12 @@ async def _post_tool_result(loop: CoreLoop, payload_json: str) -> str:
351
363
  )
352
364
  updated = await loop._hooks.post_tool_result(result, call, loop._run_context)
353
365
  made_progress = getattr(loop._hooks, "tool_made_progress", None)
354
- if callable(made_progress) and made_progress(updated, call):
366
+ progressed = bool(callable(made_progress) and made_progress(updated, call))
367
+ if progressed:
355
368
  loop._run_context.reasoning_since_progress = 0
356
- return json.dumps(_result_to_json(updated))
369
+ return json.dumps(
370
+ {"result": _result_to_json(updated), "madeProgress": progressed}
371
+ )
357
372
 
358
373
 
359
374
  async def _pre_step(
@@ -432,11 +447,34 @@ async def _on_request_error(
432
447
 
433
448
 
434
449
  def _native_history_kind(message: LLMMessage) -> str | None:
450
+ if message.role == "user" and any(
451
+ isinstance(part, ImagePart) for part in message.content
452
+ ):
453
+ return "tool.image"
435
454
  if (
436
455
  message.role == "user"
437
456
  and message.content_text.startswith("[system notice] The time budget")
438
457
  ):
439
458
  return "loop.soft_timeout_notice"
459
+ if (
460
+ message.role == "user"
461
+ and "was cut off mid-reasoning" in message.content_text
462
+ ):
463
+ return "loop.stream_cut_notice"
464
+ if (
465
+ message.role == "user"
466
+ and message.content_text.startswith(
467
+ "[system notice] The task ended without a natural-language summary"
468
+ )
469
+ ):
470
+ return "loop.narration_request"
471
+ if (
472
+ message.role == "user"
473
+ and message.content_text.startswith(
474
+ "[system notice] The previous reply described an intended action"
475
+ )
476
+ ):
477
+ return "loop.discipline_retry_notice"
440
478
  if (
441
479
  message.role == "tool"
442
480
  and "not executed" in message.content_text
@@ -455,6 +493,18 @@ def _native_history_kind(message: LLMMessage) -> str | None:
455
493
  and "mid-turn message" in message.content_text
456
494
  ):
457
495
  return "loop.steer_skip"
496
+ if (
497
+ message.role == "tool"
498
+ and "not executed" in message.content_text
499
+ and "too many consecutive tool errors" in message.content_text
500
+ ):
501
+ return "loop.breaker_skip"
502
+ if (
503
+ message.role == "tool"
504
+ and "not executed" in message.content_text
505
+ and "earlier tool ended the turn" in message.content_text
506
+ ):
507
+ return "loop.terminal_skip"
458
508
  return None
459
509
 
460
510
 
@@ -465,6 +515,12 @@ async def _sync_history(
465
515
  chat_id: str | None,
466
516
  ) -> str:
467
517
  incoming = [_msg_from_json(item) for item in json.loads(history_json)]
518
+ if any(
519
+ message.role == "user"
520
+ and message.content_text.startswith("[system notice] The time budget")
521
+ for message in incoming
522
+ ):
523
+ loop._wrap_up_active = True
468
524
  current = manager.projection
469
525
  if incoming[: len(current)] == current:
470
526
  for message in incoming[len(current) :]:
@@ -512,6 +568,7 @@ async def run_native(
512
568
  aio_loop = asyncio.get_running_loop()
513
569
  out: asyncio.Queue[tuple[LoopEvent, threading.Event] | None] = asyncio.Queue()
514
570
  loop._run_context = LoopContext(chat_id=chat_id)
571
+ loop._wrap_up_active = False
515
572
  record_id = loop._record_id or chat_id
516
573
  seed = list(messages)
517
574
  first_seq = 0
@@ -540,6 +597,10 @@ async def run_native(
540
597
  cancel_flag = threading.Event()
541
598
  steer_q: queue.Queue[str] = queue.Queue()
542
599
  active_tool_tasks: set[asyncio.Task[str]] = set()
600
+ active_stream_tasks: set[asyncio.Task[None]] = set()
601
+ stream_queues: dict[str, queue.Queue[str]] = {}
602
+ prefetched_stream_frames: dict[str, str] = {}
603
+ stream_counter = [0]
543
604
  tool_cancel_reason = ["cancelled"]
544
605
  run_started = time.monotonic()
545
606
  surrogate_mode = [False]
@@ -562,6 +623,8 @@ async def run_native(
562
623
  def cancel_now() -> None:
563
624
  for task in tuple(active_tool_tasks):
564
625
  task.cancel()
626
+ for task in tuple(active_stream_tasks):
627
+ task.cancel()
565
628
 
566
629
  aio_loop.call_soon_threadsafe(cancel_now)
567
630
 
@@ -625,17 +688,78 @@ async def run_native(
625
688
  consumed.wait()
626
689
 
627
690
  def stream_llm(messages_json: str) -> str:
628
- return _await_on_loop(
629
- _collect_stream(
630
- loop,
631
- messages_json,
632
- tools,
633
- cancel_flag,
634
- run_started,
635
- surrogate_mode,
636
- ),
637
- aio_loop,
638
- )
691
+ stream_counter[0] += 1
692
+ stream_id = f"stream-{stream_counter[0]}"
693
+ output: queue.Queue[str] = queue.Queue()
694
+ stream_queues[stream_id] = output
695
+
696
+ def start() -> None:
697
+ task = aio_loop.create_task(
698
+ _collect_stream(
699
+ loop,
700
+ messages_json,
701
+ tools,
702
+ cancel_flag,
703
+ run_started,
704
+ surrogate_mode,
705
+ output,
706
+ stream_id,
707
+ )
708
+ )
709
+ active_stream_tasks.add(task)
710
+
711
+ def done(completed: asyncio.Task[None]) -> None:
712
+ active_stream_tasks.discard(completed)
713
+ try:
714
+ completed.result()
715
+ except asyncio.CancelledError:
716
+ output.put(
717
+ json.dumps(
718
+ {
719
+ "kind": "error",
720
+ "error": "cancelled",
721
+ "error_kind": "transport",
722
+ }
723
+ )
724
+ )
725
+ except BaseException as exc: # noqa: BLE001
726
+ output.put(
727
+ json.dumps(
728
+ {
729
+ "kind": "error",
730
+ "error": str(exc),
731
+ "error_kind": "unknown",
732
+ }
733
+ )
734
+ )
735
+
736
+ task.add_done_callback(done)
737
+
738
+ aio_loop.call_soon_threadsafe(start)
739
+ first_frame = output.get()
740
+ first = json.loads(first_frame)
741
+ if first.get("kind") == "error":
742
+ stream_queues.pop(stream_id, None)
743
+ first["ok"] = False
744
+ return json.dumps(first)
745
+ prefetched_stream_frames[stream_id] = first_frame
746
+ return json.dumps({"ok": True, "stream_id": stream_id})
747
+
748
+ def next_stream_chunk(stream_id: str) -> str:
749
+ output = stream_queues.get(stream_id)
750
+ if output is None:
751
+ return json.dumps(
752
+ {
753
+ "kind": "error",
754
+ "error": f"unknown stream: {stream_id}",
755
+ "error_kind": "unknown",
756
+ }
757
+ )
758
+ frame = prefetched_stream_frames.pop(stream_id, None) or output.get()
759
+ kind = json.loads(frame).get("kind")
760
+ if kind in ("done", "error"):
761
+ stream_queues.pop(stream_id, None)
762
+ return frame
639
763
 
640
764
  def exec_tool(call_json: str) -> str:
641
765
  box: queue.Queue[str] = queue.Queue()
@@ -718,6 +842,9 @@ async def run_native(
718
842
  ctx.accumulated_completion_tokens = int(
719
843
  raw.get("accumulated_completion_tokens") or 0
720
844
  )
845
+ ctx.reasoning_since_progress = int(
846
+ raw.get("reasoning_since_progress") or 0
847
+ )
721
848
  return json.dumps({"ok": True})
722
849
 
723
850
  def before_completion(draft_json: str) -> str:
@@ -776,6 +903,7 @@ async def run_native(
776
903
  getattr(loop._provider, "model", "unknown"),
777
904
  emit,
778
905
  stream_llm,
906
+ next_stream_chunk,
779
907
  exec_tool,
780
908
  tool_concurrency_safe,
781
909
  tool_dedup_exempt,
@@ -384,16 +384,16 @@ class OrchestrationExecutor:
384
384
  return bool(inner_exempt and inner_exempt(call))
385
385
 
386
386
  def concurrency_safe(self, call: ToolCall) -> bool:
387
- # spawn/wait/list mutate nothing themselves (the pool task does the
388
- # work), so they may batch; send/close/interrupt are serialized with
389
- # siblings.
387
+ # Spawn consumes the bounded parallel-capacity pool, so batching two
388
+ # spawns makes which call succeeds scheduler-dependent. Wait/list only
389
+ # observe pool state and may batch.
390
390
  if call.name in {
391
- self._config.spawn_tool,
392
391
  self._config.wait_tool,
393
392
  self._config.list_tool,
394
393
  }:
395
394
  return True
396
395
  if call.name in {
396
+ self._config.spawn_tool,
397
397
  self._config.send_tool,
398
398
  self._config.close_tool,
399
399
  self._config.interrupt_tool,
@@ -202,6 +202,9 @@ class AgentPool:
202
202
  "childId": child_id,
203
203
  "depth": self._depth + 1,
204
204
  "task": task,
205
+ # Present only when the factory gave the child a durable
206
+ # record; hosts read that record to show the child's process.
207
+ **({"recordId": loop.record_id} if loop.record_id else {}),
205
208
  **(event_extra or {}),
206
209
  },
207
210
  )