steerable-agent-runtime 0.6.8__tar.gz → 0.6.10__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 (152) hide show
  1. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/PKG-INFO +1 -1
  2. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/pyproject.toml +1 -1
  3. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/loop.py +20 -3
  4. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/model_resolve.py +77 -1
  5. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
  6. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_antihallucination.py +37 -0
  7. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_gateway_catalog.py +6 -0
  8. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_loop.py +1 -1
  9. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_model_resolve.py +41 -0
  10. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/README.md +0 -0
  11. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/setup.cfg +0 -0
  12. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/__init__.py +0 -0
  13. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/antihallucination.py +0 -0
  14. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/approval.py +0 -0
  15. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/approval_policy.py +0 -0
  16. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/ask_user.py +0 -0
  17. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/branch.py +0 -0
  18. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/cache_control.py +0 -0
  19. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/calibration.py +0 -0
  20. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/compaction.py +0 -0
  21. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/config.py +0 -0
  22. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/default.harness.json +0 -0
  23. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  24. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/errors.py +0 -0
  25. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  26. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/handoff.py +0 -0
  27. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/harness.py +0 -0
  28. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/harness_spec.py +0 -0
  29. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/history.py +0 -0
  30. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/hooks.py +0 -0
  31. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  32. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  33. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/llm/compat.py +0 -0
  34. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/llm/errors.py +0 -0
  35. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  36. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  37. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  38. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/llm/parts.py +0 -0
  39. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/llm/presets.py +0 -0
  40. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  41. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/maintenance.py +0 -0
  42. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/mcp.py +0 -0
  43. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/mcp_server.py +0 -0
  44. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/model_catalog.py +0 -0
  45. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/model_info.py +0 -0
  46. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/observation_aging.py +0 -0
  47. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/orchestration.py +0 -0
  48. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/otel.py +0 -0
  49. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/plugins.py +0 -0
  50. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/pool.py +0 -0
  51. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/pricing.py +0 -0
  52. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/pseudo.py +0 -0
  53. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/recording.py +0 -0
  54. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/reminders.py +0 -0
  55. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/replay.py +0 -0
  56. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/resume.py +0 -0
  57. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/retry.py +0 -0
  58. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/sandboxed.py +0 -0
  59. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/skills.py +0 -0
  60. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/spill.py +0 -0
  61. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  62. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  63. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  64. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  65. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  66. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/subagent.py +0 -0
  67. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/todo.py +0 -0
  68. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/tokens.py +0 -0
  69. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/tool_schema.py +0 -0
  70. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/tool_search.py +0 -0
  71. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/tools.py +0 -0
  72. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/tracing.py +0 -0
  73. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  74. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  75. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  76. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime/world_state.py +0 -0
  77. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
  78. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  79. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
  80. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  81. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_approval.py +0 -0
  82. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_approval_policy.py +0 -0
  83. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_ask_user.py +0 -0
  84. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_branch.py +0 -0
  85. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_cache_control.py +0 -0
  86. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_cache_instrumentation.py +0 -0
  87. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_calibration.py +0 -0
  88. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_compaction.py +0 -0
  89. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_config.py +0 -0
  90. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_content_parts.py +0 -0
  91. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_error_taxonomy.py +0 -0
  92. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_fragment_bounds.py +0 -0
  93. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_golden.py +0 -0
  94. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_handoff.py +0 -0
  95. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_harness.py +0 -0
  96. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_harness_spec.py +0 -0
  97. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_history.py +0 -0
  98. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_history_persistence.py +0 -0
  99. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_hooks.py +0 -0
  100. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_in_memory_storage.py +0 -0
  101. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_llm_gemini_wire.py +0 -0
  102. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_llm_responses_wire.py +0 -0
  103. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_llm_stream_e2e.py +0 -0
  104. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_llm_wire_helpers.py +0 -0
  105. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_long_session.py +0 -0
  106. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_loop_cancellation.py +0 -0
  107. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_loop_replay.py +0 -0
  108. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_loop_sandbox_event.py +0 -0
  109. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_maintenance.py +0 -0
  110. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_mcp.py +0 -0
  111. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_mcp_server.py +0 -0
  112. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_model_catalog.py +0 -0
  113. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_model_equivalence.py +0 -0
  114. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_model_info.py +0 -0
  115. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_observation_aging.py +0 -0
  116. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_orchestration.py +0 -0
  117. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_otel.py +0 -0
  118. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_parallel_tools.py +0 -0
  119. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_plugins.py +0 -0
  120. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_provider_compat.py +0 -0
  121. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_provider_presets.py +0 -0
  122. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_pseudo.py +0 -0
  123. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_recording.py +0 -0
  124. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_reminders.py +0 -0
  125. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_replay_crosslang.py +0 -0
  126. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_resume.py +0 -0
  127. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_retry_hooks.py +0 -0
  128. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_safety_gate.py +0 -0
  129. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_sandboxed.py +0 -0
  130. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_skills.py +0 -0
  131. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_soft_timeout.py +0 -0
  132. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_spill.py +0 -0
  133. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_sqlite_storage.py +0 -0
  134. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_steer.py +0 -0
  135. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_storage_contract.py +0 -0
  136. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_stream_strip.py +0 -0
  137. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_subagent.py +0 -0
  138. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_system_proxy.py +0 -0
  139. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_todo.py +0 -0
  140. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_todo_gate.py +0 -0
  141. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_tokens.py +0 -0
  142. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_tool_exposure.py +0 -0
  143. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_tool_hygiene.py +0 -0
  144. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_tool_router.py +0 -0
  145. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_tool_schema.py +0 -0
  146. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_tool_timeout.py +0 -0
  147. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_trace_recorder.py +0 -0
  148. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_transport_jsonrpc.py +0 -0
  149. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_transport_sse.py +0 -0
  150. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_usage_attribution.py +0 -0
  151. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/tests/test_world_state.py +0 -0
  152. {steerable_agent_runtime-0.6.8 → steerable_agent_runtime-0.6.10}/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.8
3
+ Version: 0.6.10
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.8"
3
+ version = "0.6.10"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -877,6 +877,13 @@ class CoreLoop:
877
877
  else None
878
878
  )
879
879
  wrap_up = False
880
+ # Terminal that triggered the current wrap-up when a budget wall
881
+ # (maxRounds) offered narration. The wrap-up summary is an interim
882
+ # report, not task completion: the no-tools terminal below restores
883
+ # this decision so the host's auto-continue sees budget_exhausted and
884
+ # resumes with a fresh budget instead of mistaking the summary for a
885
+ # finished task.
886
+ wrap_up_terminal: CompletionDecision | None = None
880
887
  wrap_up_tool_rounds_used = 0
881
888
  idle_cut_count = 0
882
889
 
@@ -1051,7 +1058,10 @@ class CoreLoop:
1051
1058
  )
1052
1059
  yield LoopEvent(
1053
1060
  "budget_exhausted",
1054
- {"kind": "rounds", "rounds": self._config.max_rounds},
1061
+ # "budget" not "kind": the sidecar wraps loop events as
1062
+ # {"kind": <event kind>, **data} — a data["kind"] would
1063
+ # clobber the envelope kind hosts dispatch on.
1064
+ {"budget": "rounds", "rounds": self._config.max_rounds},
1055
1065
  )
1056
1066
  narrate = await self._offer_narration(
1057
1067
  "", round_index, had_tool_calls=True, ctx=ctx,
@@ -1060,6 +1070,7 @@ class CoreLoop:
1060
1070
  if narrate is not None:
1061
1071
  prompt, reason = narrate
1062
1072
  completion_redos += 1
1073
+ wrap_up_terminal = decision
1063
1074
  mark_wrap_up()
1064
1075
  manager.append_fragment(NarrationRequest(prompt))
1065
1076
  yield LoopEvent(
@@ -1362,7 +1373,7 @@ class CoreLoop:
1362
1373
  if exhausted:
1363
1374
  yield LoopEvent(
1364
1375
  "budget_exhausted",
1365
- {"kind": "tokens", "used": budget_state.tokens_used},
1376
+ {"budget": "tokens", "used": budget_state.tokens_used},
1366
1377
  )
1367
1378
  decision = CompletionDecision(
1368
1379
  status="budget_exhausted", reason="token budget exceeded"
@@ -1586,7 +1597,13 @@ class CoreLoop:
1586
1597
 
1587
1598
  # ── decide: no tool calls → terminal (unless a hook vetoes) ──
1588
1599
  if not tool_calls:
1589
- if content.strip():
1600
+ if wrap_up_terminal is not None:
1601
+ # Budget-wall wrap-up: the narration summary is an interim
1602
+ # report, not a finished task. Restore the wall's terminal
1603
+ # (budget_exhausted) so the host auto-continues with a
1604
+ # fresh budget instead of stopping on a half-done task.
1605
+ decision = wrap_up_terminal
1606
+ elif content.strip():
1590
1607
  decision = CompletionDecision(
1591
1608
  status="completed",
1592
1609
  reason="assistant produced final response with no pending tools",
@@ -19,7 +19,7 @@ silent defaults are exactly how the drift stayed invisible.
19
19
  from __future__ import annotations
20
20
 
21
21
  from dataclasses import dataclass
22
- from typing import Literal
22
+ from typing import Any, Literal
23
23
 
24
24
  from .model_catalog import MODEL_ENTRIES, PROVIDER_ENTRIES
25
25
 
@@ -173,6 +173,82 @@ def provider_endpoint(provider: str) -> ProviderEndpoint | None:
173
173
  return ProviderEndpoint(provider=provider, api_base_url=api, env_vars=env)
174
174
 
175
175
 
176
+ #: Sidecar factory kinds the host settings picker maps onto. Catalog ids
177
+ #: not in this table (and whose models are not all Anthropic-format) speak
178
+ #: OpenAI-compatible chat completions.
179
+ _WIRE_KIND_BY_PROVIDER: dict[str, str] = {
180
+ "anthropic": "anthropic",
181
+ "google": "google",
182
+ "google-vertex": "google",
183
+ "google-vertex-anthropic": "anthropic",
184
+ "xai": "openai-responses",
185
+ }
186
+
187
+ #: models.dev omits ``api`` for first-party SDKs. Host pickers still need a
188
+ #: fillable URL so the user is not dropped into a blank field for OpenAI /
189
+ #: Anthropic / Gemini / Groq / xAI.
190
+ _SDK_DEFAULT_BASE_URLS: dict[str, str] = {
191
+ "openai": "https://api.openai.com/v1",
192
+ "anthropic": "https://api.anthropic.com",
193
+ "google": "https://generativelanguage.googleapis.com",
194
+ "groq": "https://api.groq.com/openai/v1",
195
+ "xai": "https://api.x.ai/v1",
196
+ }
197
+
198
+
199
+ def _wire_kind(provider_id: str, api: str | None, formats: set[str]) -> str:
200
+ mapped = _WIRE_KIND_BY_PROVIDER.get(provider_id)
201
+ if mapped:
202
+ return mapped
203
+ if api and "/anthropic" in api.lower():
204
+ return "anthropic"
205
+ if formats and formats <= {"anthropic"}:
206
+ return "anthropic"
207
+ return "openai_compat"
208
+
209
+
210
+ def describe_catalog_providers() -> list[dict[str, Any]]:
211
+ """Host settings pickers: one row per catalogued serving provider.
212
+
213
+ Same service pattern as ``compat.describe`` / ``presets.describe``: the
214
+ bundled catalog is the source of truth for default URLs and chat-capable
215
+ model ids. ``wireKind`` is the sidecar factory kind. ``apiBaseUrl`` is
216
+ None when neither the catalog nor the first-party SDK default knows an
217
+ endpoint — the host then collects one by hand. Embedding / TTS / video
218
+ ids (``tool_format == "none"``) are omitted so the model list is what a
219
+ chat agent can actually call.
220
+ """
221
+ models_by_provider: dict[str, list[str]] = {}
222
+ formats_by_provider: dict[str, set[str]] = {}
223
+ for key, row in MODEL_ENTRIES.items():
224
+ provider_id, _, model_id = key.partition("/")
225
+ if not provider_id or not model_id:
226
+ continue
227
+ _context, _modalities, tool_format, _reasoning = row
228
+ if tool_format == "none":
229
+ continue
230
+ models_by_provider.setdefault(provider_id, []).append(model_id)
231
+ formats_by_provider.setdefault(provider_id, set()).add(tool_format)
232
+
233
+ described: list[dict[str, Any]] = []
234
+ for provider_id in sorted(PROVIDER_ENTRIES):
235
+ api, env_vars = PROVIDER_ENTRIES[provider_id]
236
+ models = models_by_provider.get(provider_id, [])
237
+ if not models:
238
+ continue
239
+ formats = formats_by_provider.get(provider_id, set())
240
+ described.append(
241
+ {
242
+ "id": provider_id,
243
+ "apiBaseUrl": api or _SDK_DEFAULT_BASE_URLS.get(provider_id),
244
+ "envVars": list(env_vars),
245
+ "wireKind": _wire_kind(provider_id, api, formats),
246
+ "models": sorted(models),
247
+ }
248
+ )
249
+ return described
250
+
251
+
176
252
  def catalog_provider_for_base_url(base_url: str | None) -> str | None:
177
253
  """The catalog provider serving ``base_url``, matched by URL host.
178
254
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.8
3
+ Version: 0.6.10
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
@@ -17,6 +17,7 @@ from steerable_agent_runtime import (
17
17
  AntiHallucinationConfig,
18
18
  AntiHallucinationHooks,
19
19
  CoreLoop,
20
+ LoopConfig,
20
21
  LoopEvent,
21
22
  RouterToolExecutor,
22
23
  ToolRouter,
@@ -521,6 +522,42 @@ async def test_narration_round_on_empty_terminal() -> None:
521
522
  )
522
523
 
523
524
 
525
+ @pytest.mark.asyncio
526
+ async def test_narration_at_max_rounds_wall_stays_budget_exhausted() -> None:
527
+ """The maxRounds wall interrupts work; the narration summary it triggers
528
+ is an interim report, not task completion. The terminal must keep
529
+ budget_exhausted so the host's auto-continue resumes with a fresh budget
530
+ (a completed status would silently stop a half-done task)."""
531
+ provider = make_provider(
532
+ [
533
+ {"content": "", "tool_calls": [tc("get_data", call_id="c1")]},
534
+ {"content": "", "tool_calls": [tc("get_data", {"source": "a"}, "c2")]},
535
+ {"content": "", "tool_calls": [tc("get_data", {"source": "b"}, "c3")]},
536
+ # narration wrap-up round (tools withheld): interim summary
537
+ {"content": "阶段性总结:已取数 3 次。"},
538
+ ]
539
+ )
540
+ hooks = AntiHallucinationHooks(provider, AntiHallucinationConfig())
541
+ loop = CoreLoop(
542
+ provider,
543
+ RouterToolExecutor(make_router_with_tool()),
544
+ LoopConfig(max_rounds=3),
545
+ hooks=hooks,
546
+ )
547
+ events = await collect(loop.run([LLMMessage.text_of("user", "取数")], tools=TOOLS))
548
+
549
+ final = final_completion(events)
550
+ assert final["status"] == "budget_exhausted"
551
+ assert "maxRounds" in final["reason"]
552
+ # The interim summary still rides the completion and the transcript.
553
+ assert final["textLength"] == len("阶段性总结:已取数 3 次。")
554
+ narrations = [
555
+ e for e in events
556
+ if e.kind == "hook_action" and e.data.get("action") == "narrate"
557
+ ]
558
+ assert len(narrations) == 1
559
+
560
+
524
561
  @pytest.mark.asyncio
525
562
  async def test_narration_second_chance_on_empty_wrap_up() -> None:
526
563
  """Real-data replay finding: the wrap-up round itself can come back empty
@@ -179,6 +179,12 @@ async def test_fetch_caches_within_ttl(fake_httpx) -> None:
179
179
  assert second.entries == first.entries
180
180
 
181
181
 
182
+ async def test_fetch_ttl_zero_bypasses_cache(fake_httpx) -> None:
183
+ await fetch_gateway_models("http://gw.test/v1")
184
+ await fetch_gateway_models("http://gw.test/v1", ttl_sec=0)
185
+ assert fake_httpx.calls == 2
186
+
187
+
182
188
  async def test_fetch_serves_stale_on_refresh_failure(fake_httpx) -> None:
183
189
  await fetch_gateway_models("http://gw.test/v1")
184
190
  fake_httpx.payload = ConnectionError("gateway down")
@@ -410,7 +410,7 @@ async def test_token_budget_exhausted() -> None:
410
410
  events = await collect(loop.run([LLMMessage.text_of("user", "hi")]))
411
411
  decision = final_completion(events)
412
412
  assert decision["status"] == "budget_exhausted"
413
- assert any(e.kind == "budget_exhausted" and e.data["kind"] == "tokens" for e in events)
413
+ assert any(e.kind == "budget_exhausted" and e.data["budget"] == "tokens" for e in events)
414
414
 
415
415
 
416
416
  @pytest.mark.asyncio
@@ -84,3 +84,44 @@ def test_provider_endpoint_none_for_first_party_default() -> None:
84
84
 
85
85
  def test_provider_endpoint_unknown_provider() -> None:
86
86
  assert provider_endpoint("no-such-provider") is None
87
+
88
+
89
+ def test_describe_catalog_providers_covers_featured_vendors() -> None:
90
+ from steerable_agent_runtime.model_resolve import describe_catalog_providers
91
+
92
+ described = describe_catalog_providers()
93
+ by_id = {row["id"]: row for row in described}
94
+ assert "deepseek" in by_id
95
+ assert by_id["deepseek"]["apiBaseUrl"] == "https://api.deepseek.com"
96
+ assert by_id["deepseek"]["wireKind"] == "openai_compat"
97
+ assert "deepseek-v4-flash" in by_id["deepseek"]["models"]
98
+
99
+ assert by_id["anthropic"]["wireKind"] == "anthropic"
100
+ assert by_id["anthropic"]["apiBaseUrl"] == "https://api.anthropic.com"
101
+ assert any(m.startswith("claude-") for m in by_id["anthropic"]["models"])
102
+
103
+ assert by_id["google"]["wireKind"] == "google"
104
+ assert by_id["google"]["apiBaseUrl"] == "https://generativelanguage.googleapis.com"
105
+ assert "gemini-embedding-001" not in by_id["google"]["models"]
106
+
107
+ assert by_id["xai"]["wireKind"] == "openai-responses"
108
+ assert by_id["minimax"]["wireKind"] == "anthropic"
109
+ assert by_id["minimax"]["apiBaseUrl"] and "/anthropic" in by_id["minimax"]["apiBaseUrl"]
110
+
111
+ # First-party openai has no catalog api field; the describe row still
112
+ # fills the SDK default so a host picker can auto-set the URL.
113
+ assert by_id["openai"]["apiBaseUrl"] == "https://api.openai.com/v1"
114
+
115
+
116
+ def test_describe_catalog_providers_skips_providers_without_chat_models() -> None:
117
+ from steerable_agent_runtime.model_resolve import describe_catalog_providers
118
+
119
+ described = describe_catalog_providers()
120
+ for row in described:
121
+ assert row["models"], row["id"]
122
+ assert row["wireKind"] in {
123
+ "openai_compat",
124
+ "anthropic",
125
+ "google",
126
+ "openai-responses",
127
+ }