steerable-agent-runtime 0.6.34__tar.gz → 0.6.35__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.34 → steerable_agent_runtime-0.6.35}/PKG-INFO +1 -1
  2. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/pyproject.toml +1 -1
  3. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/__init__.py +4 -0
  4. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/ask_user.py +4 -2
  5. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/pool.py +14 -1
  6. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/subagent.py +133 -8
  7. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
  8. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_subagent.py +232 -16
  9. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/README.md +0 -0
  10. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/setup.cfg +0 -0
  11. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/antihallucination.py +0 -0
  12. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/approval.py +0 -0
  13. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/approval_policy.py +0 -0
  14. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/branch.py +0 -0
  15. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/cache_control.py +0 -0
  16. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/calibration.py +0 -0
  17. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/compaction.py +0 -0
  18. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/compaction_policy.py +0 -0
  19. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/config.py +0 -0
  20. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/default.harness.json +0 -0
  21. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  22. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/delegation_gate.py +0 -0
  23. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/errors.py +0 -0
  24. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  25. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/handoff.py +0 -0
  26. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/harness.py +0 -0
  27. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/harness_spec.py +0 -0
  28. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/history.py +0 -0
  29. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/hooks.py +0 -0
  30. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  31. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  32. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/compat.py +0 -0
  33. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/errors.py +0 -0
  34. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  35. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  36. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  37. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/parts.py +0 -0
  38. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/presets.py +0 -0
  39. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  40. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/loop.py +0 -0
  41. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/maintenance.py +0 -0
  42. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/mcp.py +0 -0
  43. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/mcp_server.py +0 -0
  44. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/model_catalog.py +0 -0
  45. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/model_info.py +0 -0
  46. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/model_resolve.py +0 -0
  47. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/native_bridge.py +0 -0
  48. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/observation_aging.py +0 -0
  49. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/orchestration.py +0 -0
  50. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/otel.py +0 -0
  51. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/plugins.py +0 -0
  52. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/pricing.py +0 -0
  53. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/pseudo.py +0 -0
  54. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/recording.py +0 -0
  55. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/reminders.py +0 -0
  56. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/replay.py +0 -0
  57. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/resume.py +0 -0
  58. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/retry.py +0 -0
  59. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/sandboxed.py +0 -0
  60. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/skills.py +0 -0
  61. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/spill.py +0 -0
  62. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  63. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  64. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  65. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  66. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  67. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/todo.py +0 -0
  68. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/tokens.py +0 -0
  69. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/tool_schema.py +0 -0
  70. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/tool_search.py +0 -0
  71. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/tools.py +0 -0
  72. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/tracing.py +0 -0
  73. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  74. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  75. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  76. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/world_state.py +0 -0
  77. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
  78. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  79. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
  80. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  81. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_antihallucination.py +0 -0
  82. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_approval.py +0 -0
  83. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_approval_policy.py +0 -0
  84. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_ask_user.py +0 -0
  85. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_branch.py +0 -0
  86. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_cache_control.py +0 -0
  87. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_cache_instrumentation.py +0 -0
  88. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_calibration.py +0 -0
  89. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_compaction.py +0 -0
  90. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_compaction_policy.py +0 -0
  91. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_compaction_strategy.py +0 -0
  92. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_config.py +0 -0
  93. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_content_parts.py +0 -0
  94. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_delegation_gate.py +0 -0
  95. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_error_taxonomy.py +0 -0
  96. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_fragment_bounds.py +0 -0
  97. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_gateway_catalog.py +0 -0
  98. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_golden.py +0 -0
  99. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_handoff.py +0 -0
  100. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_harness.py +0 -0
  101. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_harness_spec.py +0 -0
  102. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_history.py +0 -0
  103. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_history_persistence.py +0 -0
  104. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_hooks.py +0 -0
  105. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_in_memory_storage.py +0 -0
  106. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_llm_gemini_wire.py +0 -0
  107. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_llm_responses_wire.py +0 -0
  108. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_llm_stream_e2e.py +0 -0
  109. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_llm_wire_helpers.py +0 -0
  110. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_long_session.py +0 -0
  111. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_loop.py +0 -0
  112. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_loop_cancellation.py +0 -0
  113. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_loop_replay.py +0 -0
  114. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_loop_sandbox_event.py +0 -0
  115. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_loop_terminal.py +0 -0
  116. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_maintenance.py +0 -0
  117. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_mcp.py +0 -0
  118. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_mcp_server.py +0 -0
  119. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_model_catalog.py +0 -0
  120. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_model_equivalence.py +0 -0
  121. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_model_info.py +0 -0
  122. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_model_resolve.py +0 -0
  123. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_native_default.py +0 -0
  124. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_observation_aging.py +0 -0
  125. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_orchestration.py +0 -0
  126. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_otel.py +0 -0
  127. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_parallel_tools.py +0 -0
  128. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_plugins.py +0 -0
  129. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_provider_compat.py +0 -0
  130. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_provider_presets.py +0 -0
  131. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_pseudo.py +0 -0
  132. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_pyo3_api_surface.py +0 -0
  133. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_recording.py +0 -0
  134. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_reminders.py +0 -0
  135. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_replay_crosslang.py +0 -0
  136. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_resume.py +0 -0
  137. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_retry_hooks.py +0 -0
  138. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_safety_gate.py +0 -0
  139. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_sandboxed.py +0 -0
  140. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_skills.py +0 -0
  141. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_soft_timeout.py +0 -0
  142. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_spill.py +0 -0
  143. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_sqlite_storage.py +0 -0
  144. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_steer.py +0 -0
  145. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_storage_contract.py +0 -0
  146. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_stream_strip.py +0 -0
  147. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_system_proxy.py +0 -0
  148. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_todo.py +0 -0
  149. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_todo_gate.py +0 -0
  150. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tokens.py +0 -0
  151. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tool_exposure.py +0 -0
  152. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tool_hygiene.py +0 -0
  153. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tool_router.py +0 -0
  154. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tool_schema.py +0 -0
  155. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tool_timeout.py +0 -0
  156. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_trace_recorder.py +0 -0
  157. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_transport_jsonrpc.py +0 -0
  158. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_transport_sse.py +0 -0
  159. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_usage_attribution.py +0 -0
  160. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_world_state.py +0 -0
  161. {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/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.34
3
+ Version: 0.6.35
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.34"
3
+ version = "0.6.35"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -250,9 +250,11 @@ from .spill import FilesystemSpillStore, InMemorySpillStore, SpillHooks, SpillSt
250
250
  from .storage import StorageAdapter
251
251
  from .subagent import (
252
252
  FilteredToolsExecutor,
253
+ PARENT_INPUT_TOOL_NAME,
253
254
  SubagentConfig,
254
255
  SubagentExecutor,
255
256
  SubagentRegistry,
257
+ parent_input_tool_descriptor,
256
258
  subagent_tool_descriptor,
257
259
  )
258
260
  from .tokens import (
@@ -285,6 +287,7 @@ __all__ = [
285
287
  "MODEL_INFOS",
286
288
  "MODEL_PRICES",
287
289
  "MODEL_TOKEN_FACTORS",
290
+ "PARENT_INPUT_TOOL_NAME",
288
291
  "REASONING_EFFORT_ORDER",
289
292
  "RECORD_FORMAT_VERSION",
290
293
  "ReasoningEffortUnsupported",
@@ -465,6 +468,7 @@ __all__ = [
465
468
  "load_transcript",
466
469
  "make_ask_user_tool",
467
470
  "make_todo_write_tool",
471
+ "parent_input_tool_descriptor",
468
472
  "matches_conditions",
469
473
  "mcp_invoker",
470
474
  "merge_patch",
@@ -125,7 +125,9 @@ def _derive_header(text: str) -> str:
125
125
  return truncated
126
126
 
127
127
 
128
- def _normalize_questions(questions: list[dict[str, Any]]) -> list[dict[str, Any]]:
128
+ def normalize_ask_user_questions(
129
+ questions: list[dict[str, Any]],
130
+ ) -> list[dict[str, Any]]:
129
131
  """Map known alias keys onto the canonical payload fields, enforce the
130
132
  Claude Code ``AskUserQuestion`` bounds (1-4 questions, 2-4 options per
131
133
  select, ``header`` <=12 chars, ``multiSelect`` explicit), and require the
@@ -238,7 +240,7 @@ def make_ask_user_tool(handler: AskUserHandler) -> Callable[..., Awaitable[dict[
238
240
  async def ask_user(
239
241
  intro: str, questions: list[dict[str, Any]], outro: str | None = None
240
242
  ) -> dict[str, Any]:
241
- normalized = _normalize_questions(questions)
243
+ normalized = normalize_ask_user_questions(questions)
242
244
  answers = await handler(intro, normalized)
243
245
  return {
244
246
  "intro": intro,
@@ -22,6 +22,9 @@ Design:
22
22
  - **Children stay out of the parent record.** A child's transcript is its
23
23
  own loop's; hosts that want child traces subscribe via ``event_sink``
24
24
  (lifecycle events with lineage) or wrap children in their own hooks.
25
+ - **Session state is isolated.** A child runs with its durable record id as
26
+ ``chat_id`` when available, otherwise a pool-local id, so session-scoped
27
+ tools such as ``todo_write`` cannot overwrite a sibling or parent list.
25
28
  """
26
29
 
27
30
  from __future__ import annotations
@@ -30,6 +33,7 @@ import asyncio
30
33
  import logging
31
34
  from collections.abc import Callable
32
35
  from dataclasses import dataclass
36
+ from itertools import count
33
37
  from typing import Any
34
38
 
35
39
  from .llm import LLMMessage
@@ -40,6 +44,7 @@ logger = logging.getLogger(__name__)
40
44
  #: Grace between a cooperative close (``loop.cancel()``) and the hard-cancel
41
45
  #: backstop for a child that fails to wind down.
42
46
  _CLOSE_GRACE_S = 2.0
47
+ _POOL_CONTEXT_SEQUENCE = count(1)
43
48
 
44
49
 
45
50
  class OrchestrationBudgetExceeded(Exception):
@@ -144,6 +149,7 @@ class AgentPool:
144
149
  self._config = config
145
150
  self._depth = depth
146
151
  self._lineage = lineage
152
+ self._context_namespace = next(_POOL_CONTEXT_SEQUENCE)
147
153
  self._loop_factory = loop_factory
148
154
  self._event_sink = event_sink
149
155
  self._children: dict[str, _ChildHandle] = {}
@@ -241,7 +247,14 @@ class AgentPool:
241
247
  answer_parts: list[str] = []
242
248
  status = "completed"
243
249
  try:
244
- async for event in handle.loop.run(seed, tools=handle.schemas):
250
+ async for event in handle.loop.run(
251
+ seed,
252
+ tools=handle.schemas,
253
+ chat_id=(
254
+ handle.loop.record_id
255
+ or f"subagent:{self._context_namespace}:{handle.child_id}"
256
+ ),
257
+ ):
245
258
  if event.kind == "content_delta":
246
259
  answer_parts.append(str(event.data.get("delta") or ""))
247
260
  elif event.kind == "completion":
@@ -27,6 +27,10 @@ Design:
27
27
  ``recordId``), which is how a host renders the delegation's process
28
28
  afterwards. Without them the child runs storage-free, as before.
29
29
  - The child's answer is its accumulated assistant text at completion.
30
+ - User interaction stays with the parent. Every child receives the
31
+ ``request_parent_input`` coordination tool instead of ``ask_user``; a
32
+ request ends the child run and returns structured ``needs_input`` data so
33
+ the parent can ask and re-delegate with the answers.
30
34
  - Opt-out: hosts advertise ``subagent_tool_descriptor`` in the tools list
31
35
  and wrap their executor; the sidecar does both by default and
32
36
  ``params.subagent: false`` turns delegation off.
@@ -47,21 +51,54 @@ Design:
47
51
 
48
52
  from __future__ import annotations
49
53
 
54
+ import json
50
55
  from collections.abc import Callable
51
56
  from dataclasses import dataclass
52
57
  from typing import Any
53
58
 
54
59
  from steerable_agent_protocol.generated import ToolCall, ToolResult
55
60
 
61
+ from .ask_user import (
62
+ ASK_USER_SCHEMA,
63
+ ASK_USER_TOOL_NAME,
64
+ normalize_ask_user_questions,
65
+ )
66
+ from .errors import ToolDispatchError
56
67
  from .llm import LLMProvider
57
68
  from .loop import CoreLoop, LoopConfig, LoopContext, LoopHooks, ToolExecutor
58
- from .pool import AgentPool, LoopFactory, OrchestrationBudgetExceeded, OrchestrationConfig
69
+ from .pool import (
70
+ AgentPool,
71
+ LoopFactory,
72
+ OrchestrationBudgetExceeded,
73
+ OrchestrationConfig,
74
+ )
59
75
 
60
76
  #: ``profile`` label on lifecycle events for a delegation that named no
61
77
  #: ``subagent_type`` — mirrors the tool schema's "default general-purpose
62
78
  #: profile" wording.
63
79
  DEFAULT_PROFILE_LABEL = "general-purpose"
64
80
 
81
+ #: Child-only coordination tool. A child reports missing user-owned input to
82
+ #: its parent instead of opening an interactive question card itself.
83
+ PARENT_INPUT_TOOL_NAME = "request_parent_input"
84
+
85
+
86
+ def parent_input_tool_descriptor() -> dict[str, Any]:
87
+ """OpenAI schema for a child to return structured questions to its parent."""
88
+ return {
89
+ "type": "function",
90
+ "function": {
91
+ "name": PARENT_INPUT_TOOL_NAME,
92
+ "description": (
93
+ "Report information that only the user can provide to the "
94
+ "parent agent. Do not call ask_user from a sub-agent. After "
95
+ "calling this tool, stop and let the parent ask the user, then "
96
+ "re-delegate a self-contained task with the answers."
97
+ ),
98
+ "parameters": ASK_USER_SCHEMA,
99
+ },
100
+ }
101
+
65
102
 
66
103
  @dataclass(frozen=True, slots=True)
67
104
  class SubagentConfig:
@@ -106,7 +143,9 @@ class SubagentConfig:
106
143
  description: str = (
107
144
  "Delegate a self-contained subtask to a sub-agent with its own "
108
145
  "reasoning loop. Good for parallelizable or context-heavy subtasks; "
109
- "the sub-agent returns only its final answer."
146
+ "the sub-agent returns only its final answer. If it returns status "
147
+ "needs_input, ask the user the returned questions and then delegate "
148
+ "again with the answers included in the task."
110
149
  )
111
150
 
112
151
 
@@ -174,9 +213,7 @@ def subagent_tool_descriptor(
174
213
  f"- {name}: {(registry.get(name) or config).description}"
175
214
  for name in registry.names()
176
215
  )
177
- description = (
178
- f"{description}\n\nAvailable subagent_type profiles:\n{roster}"
179
- )
216
+ description = f"{description}\n\nAvailable subagent_type profiles:\n{roster}"
180
217
  return {
181
218
  "type": "function",
182
219
  "function": {
@@ -234,6 +271,72 @@ class FilteredToolsExecutor:
234
271
  return bool(inner_safe and inner_safe(call))
235
272
 
236
273
 
274
+ class _ParentInputExecutor:
275
+ """Keep user interaction at the parent while recording child questions."""
276
+
277
+ def __init__(
278
+ self,
279
+ inner: ToolExecutor,
280
+ on_request: Callable[[dict[str, Any]], None],
281
+ ) -> None:
282
+ self._inner = inner
283
+ self._on_request = on_request
284
+
285
+ async def execute(self, call: ToolCall, ctx: LoopContext) -> ToolResult:
286
+ if call.name == ASK_USER_TOOL_NAME:
287
+ return ToolResult(
288
+ success=False,
289
+ error=(
290
+ "sub-agents cannot call ask_user directly; call "
291
+ f"{PARENT_INPUT_TOOL_NAME} so the parent agent can ask"
292
+ ),
293
+ needsFollowup=True,
294
+ )
295
+ if call.name != PARENT_INPUT_TOOL_NAME:
296
+ return await self._inner.execute(call, ctx)
297
+ intro = str(call.arguments.get("intro") or "").strip()
298
+ if not intro:
299
+ return ToolResult(
300
+ success=False,
301
+ error=f"{PARENT_INPUT_TOOL_NAME}: intro must be non-empty",
302
+ needsFollowup=True,
303
+ )
304
+ try:
305
+ questions = normalize_ask_user_questions(call.arguments.get("questions"))
306
+ except ToolDispatchError as exc:
307
+ return ToolResult(
308
+ success=False,
309
+ error=str(exc),
310
+ needsFollowup=True,
311
+ )
312
+ payload = {
313
+ "status": "needs_input",
314
+ "intro": intro,
315
+ "questions": questions,
316
+ **(
317
+ {"outro": str(call.arguments["outro"])}
318
+ if call.arguments.get("outro") is not None
319
+ else {}
320
+ ),
321
+ }
322
+ self._on_request(payload)
323
+ return ToolResult(
324
+ success=True,
325
+ terminal=True,
326
+ message=(
327
+ "The parent-input request was recorded. Stop this subtask "
328
+ "and return control to the parent agent."
329
+ ),
330
+ data={**payload, "terminal_status": "completed"},
331
+ )
332
+
333
+ def concurrency_safe(self, call: ToolCall) -> bool:
334
+ if call.name in {ASK_USER_TOOL_NAME, PARENT_INPUT_TOOL_NAME}:
335
+ return False
336
+ inner_safe = getattr(self._inner, "concurrency_safe", None)
337
+ return bool(inner_safe and inner_safe(call))
338
+
339
+
237
340
  def _schema_name(schema: dict[str, Any]) -> str:
238
341
  function = schema.get("function")
239
342
  if isinstance(function, dict) and function.get("name"):
@@ -288,6 +391,7 @@ class SubagentExecutor:
288
391
  self._parent_tools = list(tools or [])
289
392
  self._history_store = history_store
290
393
  self._record_id_prefix = record_id_prefix
394
+ self._parent_input_requests: dict[str, dict[str, Any]] = {}
291
395
  self._pool = pool or AgentPool(
292
396
  config=OrchestrationConfig(max_parallel=self._config.max_parallel),
293
397
  depth=0,
@@ -353,6 +457,18 @@ class SubagentExecutor:
353
457
  error=f"orchestration_budget_exceeded: {exc}",
354
458
  needsFollowup=True,
355
459
  )
460
+ parent_input = self._parent_input_requests.pop(outcome.child_id, None)
461
+ if parent_input is not None:
462
+ return ToolResult(
463
+ success=True,
464
+ message=json.dumps(parent_input, ensure_ascii=False),
465
+ data=parent_input,
466
+ needsFollowup=True,
467
+ nextAction=(
468
+ "Parent agent: call ask_user with these questions, then "
469
+ "delegate again with the answers in the task."
470
+ ),
471
+ )
356
472
  if outcome.status != "completed":
357
473
  return ToolResult(
358
474
  success=False,
@@ -377,14 +493,17 @@ class SubagentExecutor:
377
493
  # execute() fails closed before the spawn when a
378
494
  # model-bearing profile has no provider factory.
379
495
  provider = self._provider_factory(config.model)
380
- schemas: list[dict[str, Any]] | None = None
496
+ schemas: list[dict[str, Any]] = []
381
497
  if config.allow_tools:
382
498
  schemas = [
383
499
  schema
384
500
  for schema in self._parent_tools
385
501
  if _schema_name(schema) != self._config.tool_name
502
+ and _schema_name(schema) != ASK_USER_TOOL_NAME
503
+ and _schema_name(schema) != PARENT_INPUT_TOOL_NAME
386
504
  and (tool_filter is None or _schema_name(schema) in tool_filter)
387
- ] or None
505
+ ]
506
+ schemas.append(parent_input_tool_descriptor())
388
507
  # A child writes its own durable record when the host gave us a
389
508
  # store and a prefix, so the delegation's process can be read
390
509
  # back afterwards (`<parent record>:child:<lineage id>`). The
@@ -398,7 +517,12 @@ class SubagentExecutor:
398
517
  return (
399
518
  CoreLoop(
400
519
  provider,
401
- self._child_executor(config),
520
+ _ParentInputExecutor(
521
+ self._child_executor(config),
522
+ lambda payload: self._parent_input_requests.__setitem__(
523
+ child_id, payload
524
+ ),
525
+ ),
402
526
  LoopConfig(
403
527
  max_rounds=config.max_rounds,
404
528
  max_tool_errors=config.max_tool_errors,
@@ -424,6 +548,7 @@ class SubagentExecutor:
424
548
  hosts call this on stream teardown (a no-op twice, so a shared
425
549
  pool may also be shut down via the orchestration executor)."""
426
550
  await self._pool.shutdown()
551
+ self._parent_input_requests.clear()
427
552
 
428
553
  def concurrency_safe(self, call: ToolCall) -> bool:
429
554
  # Delegation spawns a full child loop — batched with siblings only
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.34
3
+ Version: 0.6.35
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
@@ -6,6 +6,7 @@ import asyncio
6
6
 
7
7
  import pytest
8
8
  from steerable_agent_runtime import (
9
+ PARENT_INPUT_TOOL_NAME,
9
10
  CoreLoop,
10
11
  LoopConfig,
11
12
  RouterToolExecutor,
@@ -13,15 +14,17 @@ from steerable_agent_runtime import (
13
14
  SubagentExecutor,
14
15
  SubagentRegistry,
15
16
  ToolRouter,
17
+ parent_input_tool_descriptor,
16
18
  subagent_tool_descriptor,
17
19
  )
18
20
  from steerable_agent_runtime.llm import LLMMessage, LLMStreamChunk
19
-
20
21
  from test_loop import collect
21
22
  from test_trace_recorder import make_provider, tc
22
23
 
23
24
 
24
- async def _run_parent(script, router: ToolRouter, *, config: SubagentConfig | None = None):
25
+ async def _run_parent(
26
+ script, router: ToolRouter, *, config: SubagentConfig | None = None
27
+ ):
25
28
  provider = make_provider(script)
26
29
  executor = SubagentExecutor(RouterToolExecutor(router), provider, config)
27
30
  loop = CoreLoop(provider, executor, LoopConfig())
@@ -80,6 +83,100 @@ async def test_child_writes_its_own_durable_record_and_publishes_the_id() -> Non
80
83
  assert "chat-1" not in history.records
81
84
 
82
85
 
86
+ @pytest.mark.asyncio
87
+ async def test_children_use_their_record_ids_as_isolated_chat_contexts() -> None:
88
+ """Session-scoped child tools such as todo_write must not share the empty
89
+ chat id or overwrite the parent's state."""
90
+ contexts: list[str] = []
91
+ router = ToolRouter()
92
+
93
+ async def capture_context(
94
+ label: str, context: dict | None = None
95
+ ) -> dict[str, str]:
96
+ chat_id = str((context or {}).get("chat_id") or "")
97
+ contexts.append(chat_id)
98
+ return {"label": label, "chatId": chat_id}
99
+
100
+ router.register(capture_context)
101
+ provider = make_provider(
102
+ [
103
+ {"tool_calls": [tc("delegate_subagent", {"task": "first"})]},
104
+ {"tool_calls": [tc("capture_context", {"label": "first"})]},
105
+ {"content": "first done"},
106
+ {"tool_calls": [tc("delegate_subagent", {"task": "second"})]},
107
+ {"tool_calls": [tc("capture_context", {"label": "second"})]},
108
+ {"content": "second done"},
109
+ {"content": "parent done"},
110
+ ]
111
+ )
112
+ history = _MemoryHistory()
113
+ capture_schema = {
114
+ "type": "function",
115
+ "function": {
116
+ "name": "capture_context",
117
+ "parameters": {
118
+ "type": "object",
119
+ "properties": {"label": {"type": "string"}},
120
+ "required": ["label"],
121
+ },
122
+ },
123
+ }
124
+ executor = SubagentExecutor(
125
+ RouterToolExecutor(router),
126
+ provider,
127
+ tools=[capture_schema],
128
+ history_store=history,
129
+ record_id_prefix="chat-1",
130
+ )
131
+ loop = CoreLoop(provider, executor, LoopConfig())
132
+ [e async for e in loop.run([LLMMessage.text_of("user", "go")])]
133
+ await executor.shutdown()
134
+
135
+ assert contexts == ["chat-1:child:0.1", "chat-1:child:0.2"]
136
+
137
+
138
+ @pytest.mark.asyncio
139
+ async def test_children_without_records_use_distinct_pool_local_chat_contexts() -> None:
140
+ contexts: list[str] = []
141
+ router = ToolRouter()
142
+
143
+ async def capture_context(context: dict | None = None) -> str:
144
+ chat_id = str((context or {}).get("chat_id") or "")
145
+ contexts.append(chat_id)
146
+ return chat_id
147
+
148
+ router.register(capture_context)
149
+ capture_schema = {
150
+ "type": "function",
151
+ "function": {"name": "capture_context", "parameters": {"type": "object"}},
152
+ }
153
+
154
+ async def run_one() -> None:
155
+ provider = make_provider(
156
+ [
157
+ {"tool_calls": [tc("delegate_subagent", {"task": "capture"})]},
158
+ {"tool_calls": [tc("capture_context")]},
159
+ {"content": "child done"},
160
+ {"content": "parent done"},
161
+ ]
162
+ )
163
+ executor = SubagentExecutor(
164
+ RouterToolExecutor(router),
165
+ provider,
166
+ tools=[capture_schema],
167
+ )
168
+ loop = CoreLoop(provider, executor, LoopConfig())
169
+ [e async for e in loop.run([LLMMessage.text_of("user", "go")])]
170
+ await executor.shutdown()
171
+
172
+ await run_one()
173
+ await run_one()
174
+
175
+ assert len(contexts) == 2
176
+ assert contexts[0] != contexts[1]
177
+ assert all(context.startswith("subagent:") for context in contexts)
178
+
179
+
83
180
  @pytest.mark.asyncio
84
181
  async def test_child_stays_storage_free_without_a_prefix() -> None:
85
182
  provider = make_provider(
@@ -313,7 +410,9 @@ def test_descriptor_is_openai_tool_schema() -> None:
313
410
 
314
411
  def test_descriptor_with_registry_advertises_subagent_type_enum() -> None:
315
412
  registry = SubagentRegistry()
316
- registry.register("researcher", SubagentConfig(tool_filter=frozenset({"read_file"})))
413
+ registry.register(
414
+ "researcher", SubagentConfig(tool_filter=frozenset({"read_file"}))
415
+ )
317
416
  registry.register("writer", SubagentConfig())
318
417
  d = subagent_tool_descriptor(registry=registry)
319
418
  prop = d["function"]["parameters"]["properties"]["subagent_type"]
@@ -351,9 +450,7 @@ async def test_registered_profile_governs_the_child() -> None:
351
450
  {"content": "parent done"},
352
451
  ]
353
452
  )
354
- executor = SubagentExecutor(
355
- RouterToolExecutor(router), provider, registry=registry
356
- )
453
+ executor = SubagentExecutor(RouterToolExecutor(router), provider, registry=registry)
357
454
  loop = CoreLoop(provider, executor, LoopConfig())
358
455
  events = [e async for e in loop.run([LLMMessage.text_of("user", "go")])]
359
456
 
@@ -720,9 +817,7 @@ async def test_attach_pool_shares_the_orchestration_pool() -> None:
720
817
  ]
721
818
  )
722
819
  subagent = SubagentExecutor(RouterToolExecutor(ToolRouter()), provider)
723
- orchestration = OrchestrationExecutor(
724
- subagent, provider, OrchestrationConfig()
725
- )
820
+ orchestration = OrchestrationExecutor(subagent, provider, OrchestrationConfig())
726
821
  subagent.attach_pool(orchestration.pool)
727
822
  loop = CoreLoop(provider, orchestration, LoopConfig())
728
823
  events = [
@@ -745,9 +840,8 @@ async def test_attach_pool_shares_the_orchestration_pool() -> None:
745
840
 
746
841
  @pytest.mark.asyncio
747
842
  async def test_child_advertises_the_delegated_tool_surface() -> None:
748
- """Children see the parent's advertised schemas minus the delegation
749
- tool itself, intersected with the profile's tool filter — a child
750
- never re-delegates (depth-1 by construction, advertised honestly)."""
843
+ """Children see the filtered parent tools plus parent-input coordination,
844
+ but never delegation itself (depth-1 by construction)."""
751
845
  seen_tools: list[list[str] | None] = []
752
846
  script = iter(
753
847
  [
@@ -765,9 +859,7 @@ async def test_child_advertises_the_delegated_tool_surface() -> None:
765
859
  raise NotImplementedError
766
860
 
767
861
  def stream(self, messages, *, tools=None, **kw):
768
- seen_tools.append(
769
- [t["function"]["name"] for t in tools] if tools else None
770
- )
862
+ seen_tools.append([t["function"]["name"] for t in tools] if tools else None)
771
863
  entry = next(script)
772
864
 
773
865
  async def _gen():
@@ -800,7 +892,131 @@ async def test_child_advertises_the_delegated_tool_surface() -> None:
800
892
  # Round 1 is the parent's request (full surface); round 2 is the
801
893
  # child's — filtered to the delegated domain.
802
894
  assert seen_tools[0] == ["read_file", "write_file", "delegate_subagent"]
803
- assert seen_tools[1] == ["read_file"]
895
+ assert seen_tools[1] == ["read_file", PARENT_INPUT_TOOL_NAME]
896
+
897
+
898
+ @pytest.mark.asyncio
899
+ async def test_child_uses_parent_input_protocol_instead_of_ask_user() -> None:
900
+ """A child never receives the interactive ``ask_user`` tool. It always
901
+ receives the child-only parent-input tool, even when the profile narrows
902
+ the delegated host-tool domain."""
903
+ seen_tools: list[list[str] | None] = []
904
+ script = iter(
905
+ [
906
+ {"tool_calls": [tc("delegate_subagent", {"task": "prepare"})]},
907
+ {"content": "child done"},
908
+ {"content": "parent done"},
909
+ ]
910
+ )
911
+
912
+ class _CapturingProvider:
913
+ name = "fake"
914
+ model = "fake-model"
915
+
916
+ async def complete(self, messages, *, tools=None, **kw):
917
+ raise NotImplementedError
918
+
919
+ def stream(self, messages, *, tools=None, **kw):
920
+ seen_tools.append([t["function"]["name"] for t in tools] if tools else None)
921
+ entry = next(script)
922
+
923
+ async def _gen():
924
+ if entry.get("content"):
925
+ yield LLMStreamChunk(content_delta=entry["content"])
926
+ for call in entry.get("tool_calls", []):
927
+ yield LLMStreamChunk(tool_call_delta=call)
928
+ yield LLMStreamChunk(
929
+ finish_reason="tool_calls" if entry.get("tool_calls") else "stop"
930
+ )
931
+
932
+ return _gen()
933
+
934
+ parent_tools = [
935
+ {"type": "function", "function": {"name": "read_file", "parameters": {}}},
936
+ {"type": "function", "function": {"name": "ask_user", "parameters": {}}},
937
+ subagent_tool_descriptor(),
938
+ ]
939
+ provider = _CapturingProvider()
940
+ executor = SubagentExecutor(
941
+ RouterToolExecutor(ToolRouter()),
942
+ provider,
943
+ SubagentConfig(allow_tools=False),
944
+ tools=parent_tools,
945
+ )
946
+ loop = CoreLoop(provider, executor, LoopConfig())
947
+ [e async for e in loop.run([LLMMessage.text_of("user", "go")], tools=parent_tools)]
948
+ await executor.shutdown()
949
+
950
+ assert seen_tools[0] == ["read_file", "ask_user", "delegate_subagent"]
951
+ assert seen_tools[1] == [PARENT_INPUT_TOOL_NAME]
952
+
953
+
954
+ @pytest.mark.asyncio
955
+ async def test_parent_input_request_becomes_structured_delegation_result() -> None:
956
+ direct_ask_calls = 0
957
+
958
+ async def ask_user(**kwargs) -> dict:
959
+ nonlocal direct_ask_calls
960
+ direct_ask_calls += 1
961
+ return kwargs
962
+
963
+ router = ToolRouter()
964
+ router.register(ask_user)
965
+ questions = [
966
+ {
967
+ "id": "template",
968
+ "text": "选择哪种模板?",
969
+ "header": "模板",
970
+ "type": "select",
971
+ "options": ["标准", "精简"],
972
+ "multiSelect": False,
973
+ }
974
+ ]
975
+ provider = make_provider(
976
+ [
977
+ {"tool_calls": [tc("delegate_subagent", {"task": "prepare"})]},
978
+ {
979
+ "tool_calls": [
980
+ tc(
981
+ "ask_user",
982
+ {"intro": "wrong path", "questions": questions},
983
+ )
984
+ ]
985
+ },
986
+ {
987
+ "tool_calls": [
988
+ tc(
989
+ PARENT_INPUT_TOOL_NAME,
990
+ {"intro": "missing questions"},
991
+ )
992
+ ]
993
+ },
994
+ {
995
+ "tool_calls": [
996
+ tc(
997
+ PARENT_INPUT_TOOL_NAME,
998
+ {"intro": "需要补充信息", "questions": questions},
999
+ )
1000
+ ]
1001
+ },
1002
+ {"content": "parent will ask"},
1003
+ ]
1004
+ )
1005
+ executor = SubagentExecutor(
1006
+ RouterToolExecutor(router),
1007
+ provider,
1008
+ tools=[parent_input_tool_descriptor()],
1009
+ )
1010
+ loop = CoreLoop(provider, executor, LoopConfig())
1011
+ events = [e async for e in loop.run([LLMMessage.text_of("user", "go")])]
1012
+ await executor.shutdown()
1013
+
1014
+ result = _tool_results(events)[0]
1015
+ assert result["success"] is True
1016
+ assert '"status": "needs_input"' in result["resultPreview"]
1017
+ assert "选择哪种模板?" in result["resultPreview"]
1018
+ assert provider._idx == 5
1019
+ assert direct_ask_calls == 0
804
1020
 
805
1021
 
806
1022
  @pytest.mark.asyncio