steerable-agent-runtime 0.6.72__tar.gz → 0.6.73__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/PKG-INFO +1 -1
  2. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/pyproject.toml +1 -1
  3. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/__init__.py +2 -0
  4. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/antihallucination.py +13 -0
  5. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/ask_user.py +34 -13
  6. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/history.py +11 -7
  7. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/hooks.py +32 -0
  8. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/pseudo.py +227 -3
  9. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/skills.py +76 -22
  10. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
  11. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_antihallucination.py +58 -0
  12. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_ask_user.py +53 -10
  13. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_fragment_bounds.py +17 -2
  14. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_pseudo.py +64 -0
  15. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_skills.py +139 -8
  16. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_stream_strip.py +53 -0
  17. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/README.md +0 -0
  18. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/setup.cfg +0 -0
  19. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/approval.py +0 -0
  20. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/approval_policy.py +0 -0
  21. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/branch.py +0 -0
  22. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/cache_control.py +0 -0
  23. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/calibration.py +0 -0
  24. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/compaction.py +0 -0
  25. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/compaction_policy.py +0 -0
  26. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/config.py +0 -0
  27. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/default.harness.json +0 -0
  28. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  29. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/delegation_gate.py +0 -0
  30. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/errors.py +0 -0
  31. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  32. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/handoff.py +0 -0
  33. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/harness.py +0 -0
  34. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/harness_spec.py +0 -0
  35. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  36. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  37. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/compat.py +0 -0
  38. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/errors.py +0 -0
  39. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  40. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  41. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  42. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/parts.py +0 -0
  43. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/presets.py +0 -0
  44. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  45. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/loop.py +0 -0
  46. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/maintenance.py +0 -0
  47. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/mcp.py +0 -0
  48. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/mcp_server.py +0 -0
  49. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/model_catalog.py +0 -0
  50. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/model_info.py +0 -0
  51. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/model_resolve.py +0 -0
  52. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/native_bridge.py +0 -0
  53. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/observation_aging.py +0 -0
  54. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/orchestration.py +0 -0
  55. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/otel.py +0 -0
  56. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/plugins.py +0 -0
  57. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/pool.py +0 -0
  58. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/pricing.py +0 -0
  59. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/recording.py +0 -0
  60. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/reminders.py +0 -0
  61. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/replay.py +0 -0
  62. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/resume.py +0 -0
  63. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/retry.py +0 -0
  64. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/sandboxed.py +0 -0
  65. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/spill.py +0 -0
  66. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  67. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  68. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  69. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  70. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  71. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/subagent.py +0 -0
  72. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/todo.py +0 -0
  73. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tokens.py +0 -0
  74. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tool_presentation.py +0 -0
  75. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tool_schema.py +0 -0
  76. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tool_search.py +0 -0
  77. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tools.py +0 -0
  78. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tracing.py +0 -0
  79. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  80. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  81. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  82. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/world_state.py +0 -0
  83. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
  84. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  85. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
  86. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  87. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_approval.py +0 -0
  88. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_approval_policy.py +0 -0
  89. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_branch.py +0 -0
  90. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_cache_control.py +0 -0
  91. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_cache_instrumentation.py +0 -0
  92. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_calibration.py +0 -0
  93. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_compaction.py +0 -0
  94. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_compaction_policy.py +0 -0
  95. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_compaction_strategy.py +0 -0
  96. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_config.py +0 -0
  97. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_content_parts.py +0 -0
  98. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_delegation_gate.py +0 -0
  99. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_error_taxonomy.py +0 -0
  100. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_gateway_catalog.py +0 -0
  101. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_golden.py +0 -0
  102. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_handoff.py +0 -0
  103. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_harness.py +0 -0
  104. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_harness_spec.py +0 -0
  105. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_history.py +0 -0
  106. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_history_persistence.py +0 -0
  107. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_hooks.py +0 -0
  108. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_in_memory_storage.py +0 -0
  109. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_llm_gemini_wire.py +0 -0
  110. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_llm_responses_wire.py +0 -0
  111. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_llm_stream_e2e.py +0 -0
  112. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_llm_wire_helpers.py +0 -0
  113. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_long_session.py +0 -0
  114. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_loop.py +0 -0
  115. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_loop_cancellation.py +0 -0
  116. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_loop_replay.py +0 -0
  117. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_loop_sandbox_event.py +0 -0
  118. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_loop_terminal.py +0 -0
  119. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_maintenance.py +0 -0
  120. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_mcp.py +0 -0
  121. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_mcp_server.py +0 -0
  122. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_model_catalog.py +0 -0
  123. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_model_equivalence.py +0 -0
  124. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_model_info.py +0 -0
  125. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_model_resolve.py +0 -0
  126. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_native_default.py +0 -0
  127. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_observation_aging.py +0 -0
  128. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_orchestration.py +0 -0
  129. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_otel.py +0 -0
  130. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_parallel_tools.py +0 -0
  131. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_plugins.py +0 -0
  132. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_provider_compat.py +0 -0
  133. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_provider_presets.py +0 -0
  134. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_pyo3_api_surface.py +0 -0
  135. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_recording.py +0 -0
  136. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_reminders.py +0 -0
  137. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_replay_crosslang.py +0 -0
  138. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_resume.py +0 -0
  139. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_retry_hooks.py +0 -0
  140. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_safety_gate.py +0 -0
  141. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_sandboxed.py +0 -0
  142. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_soft_timeout.py +0 -0
  143. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_spill.py +0 -0
  144. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_sqlite_storage.py +0 -0
  145. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_steer.py +0 -0
  146. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_storage_contract.py +0 -0
  147. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_subagent.py +0 -0
  148. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_system_proxy.py +0 -0
  149. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_todo.py +0 -0
  150. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_todo_gate.py +0 -0
  151. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tokens.py +0 -0
  152. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_exposure.py +0 -0
  153. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_hygiene.py +0 -0
  154. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_presentation.py +0 -0
  155. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_router.py +0 -0
  156. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_schema.py +0 -0
  157. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_timeout.py +0 -0
  158. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_trace_recorder.py +0 -0
  159. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_transport_jsonrpc.py +0 -0
  160. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_transport_sse.py +0 -0
  161. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_usage_attribution.py +0 -0
  162. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_world_state.py +0 -0
  163. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/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.72
3
+ Version: 0.6.73
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.72"
3
+ version = "0.6.73"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  license = { text = "Apache-2.0" }
6
6
  readme = "README.md"
@@ -246,6 +246,7 @@ from .skills import (
246
246
  matches_conditions,
247
247
  render_skill_catalog,
248
248
  select_catalog,
249
+ select_pinned_loadable,
249
250
  select_skills,
250
251
  skill_to_dict,
251
252
  skill_tool_descriptor,
@@ -503,6 +504,7 @@ __all__ = [
503
504
  "resolve_model_info",
504
505
  "rule_from_amendment",
505
506
  "select_catalog",
507
+ "select_pinned_loadable",
506
508
  "select_skills",
507
509
  "should_run_grounding_judge",
508
510
  "skill_to_dict",
@@ -459,6 +459,14 @@ class AntiHallucinationHooks(NoopHooks):
459
459
  self._route: DataNeedRoute | None = None
460
460
  self._retries = 0
461
461
  self._narrations = 0
462
+ #: The wall a narration round swallowed (first one wins). The loop
463
+ #: reports ``completed`` once the narration round answers, which the
464
+ #: host would otherwise read as "the model decided it was done".
465
+ self._wall_status: str | None = None
466
+
467
+ def wall_status(self) -> str | None:
468
+ """Pre-wrap-up terminal status, for the host's completion mapping."""
469
+ return self._wall_status
462
470
 
463
471
  # -- pre_step: data-need routing → tool_choice on the first round ------
464
472
 
@@ -535,6 +543,11 @@ class AntiHallucinationHooks(NoopHooks):
535
543
  and draft.tool_calls_used > 0
536
544
  ):
537
545
  self._narrations += 1
546
+ # Remember the wall the narration is papering over — the loop
547
+ # reports ``completed`` after this round, and the host needs the
548
+ # original status to know the task did not actually finish.
549
+ if self._wall_status is None:
550
+ self._wall_status = draft.status
538
551
  return CompletionAction(
539
552
  kind="narrate",
540
553
  message=(
@@ -131,11 +131,12 @@ def normalize_ask_user_questions(
131
131
  ) -> list[dict[str, Any]]:
132
132
  """Map known alias keys onto the canonical payload fields, enforce the
133
133
  Claude Code ``AskUserQuestion`` bounds (1-4 questions, 2-4 options per
134
- select, ``header`` <=12 chars, ``multiSelect`` explicit), and require the
135
- two fields the host card cannot render without (``id`` to key the answer,
136
- ``text`` to label the control). A violation raises ``ToolDispatchError`` —
137
- the router wraps it as the tool result, so the model sees exactly what to
138
- fix and retries within the same turn."""
134
+ select, ``multiSelect`` explicit), and require the two fields the host
135
+ card cannot render without (``id`` to key the answer, ``text`` to label
136
+ the control). An over-long ``header`` is clipped to 12 characters so a
137
+ local model that ignores the cap still gets a card. Any other violation
138
+ raises ``ToolDispatchError`` — the router wraps it as the tool result, so
139
+ the model sees exactly what to fix and retries within the same turn."""
139
140
  if not isinstance(questions, list):
140
141
  raise ToolDispatchError(
141
142
  f"ask_user: questions must be an array, got {type(questions).__name__}"
@@ -166,8 +167,8 @@ def normalize_ask_user_questions(
166
167
  f"ask_user: questions[{index}] is missing a non-empty string "
167
168
  '"text" (the question label shown to the user).'
168
169
  )
169
- # header: optional on the wire; validate length when present, derive
170
- # from text when absent so the card always has a chip label.
170
+ # header: optional on the wire. Derive from text when absent, and
171
+ # clip an over-long label so the chip still fits.
171
172
  header = q.get("header")
172
173
  if header is None:
173
174
  q["header"] = _derive_header(q["text"])
@@ -176,10 +177,7 @@ def normalize_ask_user_questions(
176
177
  f"ask_user: questions[{index}].header must be a non-empty string."
177
178
  )
178
179
  elif len(header) > _MAX_HEADER_LEN:
179
- raise ToolDispatchError(
180
- f"ask_user: questions[{index}].header is {len(header)} chars; "
181
- f"the chip label must be <= {_MAX_HEADER_LEN}."
182
- )
180
+ q["header"] = _derive_header(header)
183
181
  # multiSelect: CC requires the model to commit to single vs multi —
184
182
  # the field is required, so an omission is a model error to fix, not
185
183
  # a default to assume.
@@ -233,6 +231,23 @@ class AskUserHandler(Protocol):
233
231
  ) -> Awaitable[dict[str, Any]]: ...
234
232
 
235
233
 
234
+ def _resolve_intro(intro: str | None, extra: dict[str, Any]) -> str:
235
+ """Return the framing line, accepting local-model key spellings.
236
+
237
+ Qwen often emits ``"intro questions"`` (a space, or an underscore) instead
238
+ of ``intro``. The published schema still requires ``intro``; this only
239
+ recovers a call that already arrived with the framing text under another
240
+ key.
241
+ """
242
+ if isinstance(intro, str) and intro.strip():
243
+ return intro.strip()
244
+ for key, value in extra.items():
245
+ folded = key.replace(" ", "_").replace("-", "_").lower()
246
+ if folded.startswith("intro") and isinstance(value, str) and value.strip():
247
+ return value.strip()
248
+ raise ToolDispatchError("ask_user: intro must be a non-empty string")
249
+
250
+
236
251
  def make_ask_user_tool(handler: AskUserHandler) -> Callable[..., Awaitable[dict[str, Any]]]:
237
252
  """Build the ``ask_user`` tool handler bound to ``handler``.
238
253
 
@@ -244,11 +259,17 @@ def make_ask_user_tool(handler: AskUserHandler) -> Callable[..., Awaitable[dict[
244
259
  """
245
260
 
246
261
  async def ask_user(
247
- intro: str,
248
- questions: list[dict[str, Any]],
262
+ intro: str | None = None,
263
+ questions: list[dict[str, Any]] | None = None,
249
264
  outro: str | None = None,
250
265
  context: dict[str, Any] | None = None,
266
+ **extra: Any,
251
267
  ) -> dict[str, Any]:
268
+ intro = _resolve_intro(intro, extra)
269
+ if not isinstance(questions, list):
270
+ raise ToolDispatchError(
271
+ "ask_user: questions must be an array of question objects."
272
+ )
252
273
  normalized = normalize_ask_user_questions(questions)
253
274
  answers = await _call_handler(handler, intro, normalized, context)
254
275
  return {
@@ -370,14 +370,18 @@ class SystemPromptFragment(ContextFragment):
370
370
 
371
371
  role: LLMRole = "system"
372
372
  content_kind: str = "system_prompt"
373
- max_tokens: int | None = 4096
373
+ #: Align the system-prompt backstop with the framework's absolute
374
+ #: per-fragment ceiling. Desktop prompts with pinned/eager skills (e.g.
375
+ #: word-master + ppt-master) legitimately reach ~6-8k tokens; the old
376
+ #: 4k cap silently truncated the very skill body the agent bound.
377
+ max_tokens: int | None = FRAGMENT_TOKEN_CEILING
374
378
  review_note: str | None = (
375
- "Desktop production prompts (persona + eager skill layer + runtime "
376
- "environment + tool reality check) measure 1-3k tokens; 4k is the "
377
- "headroom for larger skill stacks, not the target. Crossings "
378
- "degrade with a visible marker so an over-budget assembly is "
379
- "discoverable in the transcript instead of silently inflating every "
380
- "request."
379
+ "Desktop production prompts (persona + pinned/eager skill stack + "
380
+ "runtime environment + tool reality check) legitimately reach 6-8k "
381
+ "tokens when an agent pins large skills. Reusing the absolute "
382
+ "per-fragment ceiling (10k) keeps a bounded backstop without "
383
+ "truncating the bound skill body; crossings still degrade with a "
384
+ "visible marker."
381
385
  )
382
386
 
383
387
  def __init__(self, prompt: str) -> None:
@@ -340,6 +340,22 @@ class NoopHooks:
340
340
  """
341
341
  return result.success
342
342
 
343
+ def wall_status(self) -> str | None:
344
+ """Terminal status a ``before_completion`` wrap-up swallowed, or None.
345
+
346
+ ``before_completion`` may grant a *narration* round after the turn
347
+ already hit a wall (``failed`` / ``budget_exhausted``). That round
348
+ produces a natural-language summary, so the loop's own terminal
349
+ status becomes ``completed`` even though the task never finished —
350
+ and the host's auto-continue, which only resumes on
351
+ ``budget_exhausted``, reads ``completed`` as "the model decided it
352
+ was done" and stops. Returning the pre-wrap-up status lets the host
353
+ report (and resume) the wall instead of the laundered completion.
354
+
355
+ Default None: no wrap-up disguised a wall.
356
+ """
357
+ return None
358
+
343
359
 
344
360
  class ChainHooks:
345
361
  """Compose several hooks into one ``LoopHooks``.
@@ -507,3 +523,19 @@ class ChainHooks:
507
523
  if callable(judge) and not judge(result, call):
508
524
  return False
509
525
  return True
526
+
527
+ def wall_status(self) -> str | None:
528
+ """First hook that recorded a wrap-up-swallowed wall wins.
529
+
530
+ AntiHallucinationHooks is the recorder today. Probed with
531
+ ``getattr`` like the other optional capabilities, so a chain of
532
+ product hooks that never wraps up answers None without needing the
533
+ method.
534
+ """
535
+ for hook in self._hooks:
536
+ probe = getattr(hook, "wall_status", None)
537
+ if callable(probe):
538
+ status = probe()
539
+ if status is not None:
540
+ return status
541
+ return None
@@ -7,6 +7,8 @@ regress into prose) emit tool *intent* as text instead of a structured
7
7
  - MiniMax XML: ``<invoke name="tool"><parameter name="k">v</parameter></invoke>``
8
8
  - DeepSeek XML: ``<function=tool><parameter=k>v</parameter></function>``
9
9
  - Markdown pseudo: ``[Tool call: NAME]\\n{"k": "v"}``
10
+ - JSON tool object: a fenced or ``<tool_call>`` ``{"name", "arguments"}``
11
+ object (Qwen / Ollama), or a message that is only that object
10
12
 
11
13
  If the loop treated that text as a final answer it would end the turn having
12
14
  never run the tool — the model "meant" to act but the loop stopped. This module
@@ -53,6 +55,27 @@ _PSEUDO_TOOL_CALL_HEADER_RE = re.compile(
53
55
  re.IGNORECASE,
54
56
  )
55
57
 
58
+ # --- JSON tool objects (Qwen / Ollama) --------------------------------------
59
+ #
60
+ # Small local models often answer with a markdown fence (or a <tool_call>
61
+ # wrapper, or a message that is only the object) instead of a structured
62
+ # tool_calls block:
63
+ #
64
+ # ```json
65
+ # {"name": "ask_user", "arguments": {"intro": "..."}}
66
+ # ```
67
+ #
68
+ # Only a payload whose every element is {name, arguments} is a call. A fenced
69
+ # config or a JSON object mentioned inside a sentence stays text.
70
+
71
+ _TOOL_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
72
+ _FENCE_RE = re.compile(r"```[^\n`]*\n?(.*?)```", re.DOTALL)
73
+ _TOOL_CALL_BLOCK_RE = re.compile(
74
+ r"<tool_call>\s*(.*?)\s*</tool_call>",
75
+ re.DOTALL | re.IGNORECASE,
76
+ )
77
+ _FENCE_HOLD_CAP = 8192
78
+
56
79
 
57
80
  def _scan_balanced_json_object(text: str, start: int) -> tuple[str, int] | None:
58
81
  """Find the next ``{...}`` JSON object starting at/after ``start``.
@@ -151,6 +174,9 @@ def extract_inline_tool_calls(text: str) -> tuple[list[dict[str, Any]], str]:
151
174
  cursor = end_idx
152
175
 
153
176
  if not results:
177
+ recovered = _extract_json_tool_calls(text)
178
+ if recovered is not None:
179
+ return recovered
154
180
  return [], text
155
181
 
156
182
  cleaned_segments.append(text[cursor:])
@@ -159,6 +185,103 @@ def extract_inline_tool_calls(text: str) -> tuple[list[dict[str, Any]], str]:
159
185
  return results, cleaned_text
160
186
 
161
187
 
188
+ def _coerce_arguments(raw: Any) -> dict[str, Any] | None:
189
+ """Return a dict of tool arguments, parsing a JSON string when needed."""
190
+ if raw is None:
191
+ return {}
192
+ if isinstance(raw, str):
193
+ stripped = raw.strip()
194
+ if not stripped:
195
+ return {}
196
+ try:
197
+ raw = json.loads(stripped)
198
+ except json.JSONDecodeError:
199
+ return None
200
+ if isinstance(raw, dict):
201
+ return raw
202
+ return None
203
+
204
+
205
+ def _calls_from_payload(payload: Any) -> list[dict[str, Any]] | None:
206
+ """Accept one tool-call object or a list of them. Anything else is text."""
207
+ items = payload if isinstance(payload, list) else [payload]
208
+ if not items:
209
+ return None
210
+ calls: list[dict[str, Any]] = []
211
+ for item in items:
212
+ if not isinstance(item, dict):
213
+ return None
214
+ name = item.get("name")
215
+ if not isinstance(name, str) or _TOOL_NAME_RE.fullmatch(name.strip()) is None:
216
+ return None
217
+ arguments = _coerce_arguments(item.get("arguments", {}))
218
+ if arguments is None:
219
+ return None
220
+ calls.append({"name": name.strip(), "arguments": arguments})
221
+ return calls
222
+
223
+
224
+ def _parse_tool_call_blob(blob: str) -> list[dict[str, Any]] | None:
225
+ """Parse a tool-call JSON blob, unwrapping one surrounding fence."""
226
+ blob = blob.strip()
227
+ if not blob:
228
+ return None
229
+ fenced = _FENCE_RE.fullmatch(blob)
230
+ if fenced is not None:
231
+ blob = fenced.group(1).strip()
232
+ if not blob:
233
+ return None
234
+ try:
235
+ payload = json.loads(blob)
236
+ except json.JSONDecodeError:
237
+ return None
238
+ return _calls_from_payload(payload)
239
+
240
+
241
+ def _extract_json_tool_calls(text: str) -> tuple[list[dict[str, Any]], str] | None:
242
+ """Recover fenced / ``<tool_call>`` / whole-message JSON tool calls.
243
+
244
+ Returns ``None`` when the text is not this format, so the caller can
245
+ leave it untouched.
246
+ """
247
+ results: list[dict[str, Any]] = []
248
+ spans: list[tuple[int, int]] = []
249
+
250
+ def take(match: re.Match[str], blob: str) -> None:
251
+ if any(match.start() < end and match.end() > start for start, end in spans):
252
+ return
253
+ calls = _parse_tool_call_blob(blob)
254
+ if not calls:
255
+ return
256
+ results.extend(calls)
257
+ spans.append(match.span())
258
+
259
+ for match in _TOOL_CALL_BLOCK_RE.finditer(text):
260
+ take(match, match.group(1))
261
+ for match in _FENCE_RE.finditer(text):
262
+ take(match, match.group(1))
263
+ if not results:
264
+ unclosed = re.search(r"```[^\n`]*\n(.*)\Z", text, re.DOTALL)
265
+ if unclosed is not None:
266
+ calls = _parse_tool_call_blob(unclosed.group(1))
267
+ if calls:
268
+ results.extend(calls)
269
+ spans.append(unclosed.span())
270
+ if not results:
271
+ stripped = text.strip()
272
+ if stripped[:1] in "{[":
273
+ calls = _parse_tool_call_blob(stripped)
274
+ if calls:
275
+ return calls, ""
276
+ return None
277
+
278
+ cleaned = text
279
+ for start, end in sorted(spans, reverse=True):
280
+ cleaned = cleaned[:start] + cleaned[end:]
281
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip()
282
+ return results, cleaned
283
+
284
+
162
285
  # ---------------------------------------------------------------------------
163
286
  # Streaming-time stripping (display path)
164
287
  # ---------------------------------------------------------------------------
@@ -378,8 +501,107 @@ class _MdCallFilter:
378
501
  return tail
379
502
 
380
503
 
504
+ def _split_partial_backticks(buf: str) -> tuple[str, str]:
505
+ """Hold a trailing 1-2 backticks that may still become a fence opener."""
506
+ hold = 0
507
+ for char in reversed(buf):
508
+ if char != "`" or hold == 2:
509
+ break
510
+ hold += 1
511
+ if hold == 0:
512
+ return buf, ""
513
+ return buf[:-hold], buf[-hold:]
514
+
515
+
516
+ def _opened_fence_body(held: str) -> str | None:
517
+ """Body of a span that starts with an opening fence.
518
+
519
+ ``held`` includes the closing fence when the span is complete. An
520
+ unclosed span returns the text after the language line.
521
+ """
522
+ if not held.startswith("```"):
523
+ return None
524
+ if held.endswith("```") and len(held) >= 6:
525
+ match = _FENCE_RE.fullmatch(held)
526
+ if match is None:
527
+ return None
528
+ return match.group(1)
529
+ rest = held[3:]
530
+ newline = rest.find("\n")
531
+ if newline < 0:
532
+ return rest
533
+ return rest[newline + 1 :]
534
+
535
+
536
+ class _JsonFenceFilter:
537
+ """Hold a ``` fence and drop it only when the body is a tool-call object.
538
+
539
+ A code sample or a JSON config is released unchanged, including when the
540
+ fence never closes. A fence larger than ``_FENCE_HOLD_CAP`` is released
541
+ so a long code block is not stuck in the filter.
542
+ """
543
+
544
+ def __init__(self) -> None:
545
+ self._buf = ""
546
+ self._open = False
547
+ self._held = ""
548
+
549
+ def feed(self, chunk: str) -> str:
550
+ buf = self._buf + (chunk or "")
551
+ self._buf = ""
552
+ out: list[str] = []
553
+ while True:
554
+ if self._open:
555
+ close = buf.find("```")
556
+ if close < 0:
557
+ self._held += buf
558
+ if len(self._held) > _FENCE_HOLD_CAP:
559
+ out.append(self._held)
560
+ self._held = ""
561
+ self._open = False
562
+ break
563
+ self._held += buf[: close + 3]
564
+ buf = buf[close + 3 :]
565
+ released = self._release_held()
566
+ if released:
567
+ out.append(released)
568
+ continue
569
+ opener = buf.find("```")
570
+ if opener >= 0:
571
+ if opener:
572
+ out.append(buf[:opener])
573
+ buf = buf[opener + 3 :]
574
+ self._open = True
575
+ self._held = "```"
576
+ continue
577
+ emit, hold = _split_partial_backticks(buf)
578
+ if emit:
579
+ out.append(emit)
580
+ self._buf = hold
581
+ break
582
+ return "".join(out)
583
+
584
+ def _release_held(self) -> str:
585
+ held = self._held
586
+ self._held = ""
587
+ self._open = False
588
+ body = _opened_fence_body(held)
589
+ if body is not None and _parse_tool_call_blob(body):
590
+ return ""
591
+ return held
592
+
593
+ def flush(self) -> str:
594
+ if self._open:
595
+ self._held += self._buf
596
+ self._buf = ""
597
+ return self._release_held()
598
+ tail = self._buf
599
+ self._buf = ""
600
+ return tail
601
+
602
+
381
603
  class PseudoStreamStripper:
382
- """Display-path filter chaining the fn-echo and markdown-call filters.
604
+ """Display-path filter chaining the fn-echo, JSON-fence, and markdown filters.
383
605
 
384
606
  Usage: ``feed`` each raw content delta, emit what comes back; at stream
385
607
  end, ``flush`` once and emit the tail. Text held back to detect markers
@@ -388,10 +610,12 @@ class PseudoStreamStripper:
388
610
 
389
611
  def __init__(self) -> None:
390
612
  self._fn = _FnEchoFilter()
613
+ self._fence = _JsonFenceFilter()
391
614
  self._md = _MdCallFilter()
392
615
 
393
616
  def feed(self, chunk: str) -> str:
394
- return self._md.feed(self._fn.feed(chunk))
617
+ return self._md.feed(self._fence.feed(self._fn.feed(chunk)))
395
618
 
396
619
  def flush(self) -> str:
397
- return self._md.feed(self._fn.flush()) + self._md.flush()
620
+ mid = self._fence.feed(self._fn.flush()) + self._fence.flush()
621
+ return self._md.feed(mid) + self._md.flush()