steerable-agent-runtime 0.6.72__tar.gz → 0.6.74__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.74}/PKG-INFO +1 -1
  2. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/pyproject.toml +1 -1
  3. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/__init__.py +2 -0
  4. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/antihallucination.py +13 -0
  5. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/ask_user.py +34 -13
  6. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/history.py +11 -7
  7. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/hooks.py +32 -0
  8. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/native_bridge.py +3 -1
  9. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/pseudo.py +319 -3
  10. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/skills.py +76 -22
  11. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
  12. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_antihallucination.py +58 -0
  13. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_ask_user.py +53 -10
  14. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_fragment_bounds.py +17 -2
  15. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_pseudo.py +64 -0
  16. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_skills.py +139 -8
  17. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_stream_strip.py +118 -0
  18. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/README.md +0 -0
  19. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/setup.cfg +0 -0
  20. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/approval.py +0 -0
  21. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/approval_policy.py +0 -0
  22. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/branch.py +0 -0
  23. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/cache_control.py +0 -0
  24. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/calibration.py +0 -0
  25. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/compaction.py +0 -0
  26. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/compaction_policy.py +0 -0
  27. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/config.py +0 -0
  28. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/default.harness.json +0 -0
  29. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  30. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/delegation_gate.py +0 -0
  31. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/errors.py +0 -0
  32. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
  33. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/handoff.py +0 -0
  34. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/harness.py +0 -0
  35. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/harness_spec.py +0 -0
  36. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  37. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  38. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/llm/compat.py +0 -0
  39. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/llm/errors.py +0 -0
  40. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
  41. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
  42. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
  43. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/llm/parts.py +0 -0
  44. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/llm/presets.py +0 -0
  45. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  46. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/loop.py +0 -0
  47. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/maintenance.py +0 -0
  48. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/mcp.py +0 -0
  49. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/mcp_server.py +0 -0
  50. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/model_catalog.py +0 -0
  51. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/model_info.py +0 -0
  52. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/model_resolve.py +0 -0
  53. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/observation_aging.py +0 -0
  54. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/orchestration.py +0 -0
  55. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/otel.py +0 -0
  56. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/plugins.py +0 -0
  57. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/pool.py +0 -0
  58. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/pricing.py +0 -0
  59. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/recording.py +0 -0
  60. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/reminders.py +0 -0
  61. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/replay.py +0 -0
  62. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/resume.py +0 -0
  63. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/retry.py +0 -0
  64. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/sandboxed.py +0 -0
  65. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/spill.py +0 -0
  66. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  67. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  68. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  69. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  70. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  71. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/subagent.py +0 -0
  72. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/todo.py +0 -0
  73. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/tokens.py +0 -0
  74. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/tool_presentation.py +0 -0
  75. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/tool_schema.py +0 -0
  76. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/tool_search.py +0 -0
  77. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/tools.py +0 -0
  78. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/tracing.py +0 -0
  79. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  80. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  81. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  82. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime/world_state.py +0 -0
  83. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
  84. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  85. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
  86. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  87. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_approval.py +0 -0
  88. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_approval_policy.py +0 -0
  89. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_branch.py +0 -0
  90. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_cache_control.py +0 -0
  91. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_cache_instrumentation.py +0 -0
  92. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_calibration.py +0 -0
  93. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_compaction.py +0 -0
  94. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_compaction_policy.py +0 -0
  95. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_compaction_strategy.py +0 -0
  96. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_config.py +0 -0
  97. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_content_parts.py +0 -0
  98. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_delegation_gate.py +0 -0
  99. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_error_taxonomy.py +0 -0
  100. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_gateway_catalog.py +0 -0
  101. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_golden.py +0 -0
  102. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_handoff.py +0 -0
  103. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_harness.py +0 -0
  104. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_harness_spec.py +0 -0
  105. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_history.py +0 -0
  106. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_history_persistence.py +0 -0
  107. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_hooks.py +0 -0
  108. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_in_memory_storage.py +0 -0
  109. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_llm_gemini_wire.py +0 -0
  110. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_llm_responses_wire.py +0 -0
  111. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_llm_stream_e2e.py +0 -0
  112. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_llm_wire_helpers.py +0 -0
  113. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_long_session.py +0 -0
  114. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_loop.py +0 -0
  115. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_loop_cancellation.py +0 -0
  116. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_loop_replay.py +0 -0
  117. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_loop_sandbox_event.py +0 -0
  118. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_loop_terminal.py +0 -0
  119. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_maintenance.py +0 -0
  120. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_mcp.py +0 -0
  121. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_mcp_server.py +0 -0
  122. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_model_catalog.py +0 -0
  123. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_model_equivalence.py +0 -0
  124. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_model_info.py +0 -0
  125. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_model_resolve.py +0 -0
  126. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_native_default.py +0 -0
  127. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_observation_aging.py +0 -0
  128. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_orchestration.py +0 -0
  129. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_otel.py +0 -0
  130. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_parallel_tools.py +0 -0
  131. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_plugins.py +0 -0
  132. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_provider_compat.py +0 -0
  133. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_provider_presets.py +0 -0
  134. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_pyo3_api_surface.py +0 -0
  135. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_recording.py +0 -0
  136. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_reminders.py +0 -0
  137. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_replay_crosslang.py +0 -0
  138. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_resume.py +0 -0
  139. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_retry_hooks.py +0 -0
  140. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_safety_gate.py +0 -0
  141. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_sandboxed.py +0 -0
  142. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_soft_timeout.py +0 -0
  143. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_spill.py +0 -0
  144. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_sqlite_storage.py +0 -0
  145. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_steer.py +0 -0
  146. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_storage_contract.py +0 -0
  147. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_subagent.py +0 -0
  148. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_system_proxy.py +0 -0
  149. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_todo.py +0 -0
  150. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_todo_gate.py +0 -0
  151. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_tokens.py +0 -0
  152. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_tool_exposure.py +0 -0
  153. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_tool_hygiene.py +0 -0
  154. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_tool_presentation.py +0 -0
  155. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_tool_router.py +0 -0
  156. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_tool_schema.py +0 -0
  157. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_tool_timeout.py +0 -0
  158. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_trace_recorder.py +0 -0
  159. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_transport_jsonrpc.py +0 -0
  160. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_transport_sse.py +0 -0
  161. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_usage_attribution.py +0 -0
  162. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/tests/test_world_state.py +0 -0
  163. {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.74}/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.74
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.74"
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
@@ -33,6 +33,7 @@ from steerable_agent_runtime.loop import (
33
33
  from steerable_agent_runtime.pseudo import (
34
34
  PseudoStreamStripper,
35
35
  extract_inline_tool_calls,
36
+ reply_text_from_call,
36
37
  split_trailing_high_surrogate,
37
38
  )
38
39
  from steerable_agent_runtime.replay import build_step_decision_event
@@ -316,7 +317,8 @@ async def _collect_stream(
316
317
  send("chunk", chunk={"finish_reason": cut_finish_reason})
317
318
  elif not tool_seen:
318
319
  recovered, _ = extract_inline_tool_calls("".join(raw_content))
319
- for index, call in enumerate(recovered):
320
+ actionable = [call for call in recovered if reply_text_from_call(call) is None]
321
+ for index, call in enumerate(actionable):
320
322
  send(
321
323
  "chunk",
322
324
  chunk={
@@ -7,6 +7,9 @@ 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.
12
+ ``{"name": "text", "arguments": {"value": "..."}}`` is a reply, not a tool.
10
13
 
11
14
  If the loop treated that text as a final answer it would end the turn having
12
15
  never run the tool — the model "meant" to act but the loop stopped. This module
@@ -53,6 +56,27 @@ _PSEUDO_TOOL_CALL_HEADER_RE = re.compile(
53
56
  re.IGNORECASE,
54
57
  )
55
58
 
59
+ # --- JSON tool objects (Qwen / Ollama) --------------------------------------
60
+ #
61
+ # Small local models often answer with a markdown fence (or a <tool_call>
62
+ # wrapper, or a message that is only the object) instead of a structured
63
+ # tool_calls block:
64
+ #
65
+ # ```json
66
+ # {"name": "ask_user", "arguments": {"intro": "..."}}
67
+ # ```
68
+ #
69
+ # Only a payload whose every element is {name, arguments} is a call. A fenced
70
+ # config or a JSON object mentioned inside a sentence stays text.
71
+
72
+ _TOOL_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
73
+ _FENCE_RE = re.compile(r"```[^\n`]*\n?(.*?)```", re.DOTALL)
74
+ _TOOL_CALL_BLOCK_RE = re.compile(
75
+ r"<tool_call>\s*(.*?)\s*</tool_call>",
76
+ re.DOTALL | re.IGNORECASE,
77
+ )
78
+ _FENCE_HOLD_CAP = 8192
79
+
56
80
 
57
81
  def _scan_balanced_json_object(text: str, start: int) -> tuple[str, int] | None:
58
82
  """Find the next ``{...}`` JSON object starting at/after ``start``.
@@ -151,6 +175,9 @@ def extract_inline_tool_calls(text: str) -> tuple[list[dict[str, Any]], str]:
151
175
  cursor = end_idx
152
176
 
153
177
  if not results:
178
+ recovered = _extract_json_tool_calls(text)
179
+ if recovered is not None:
180
+ return recovered
154
181
  return [], text
155
182
 
156
183
  cleaned_segments.append(text[cursor:])
@@ -159,6 +186,128 @@ def extract_inline_tool_calls(text: str) -> tuple[list[dict[str, Any]], str]:
159
186
  return results, cleaned_text
160
187
 
161
188
 
189
+ def _coerce_arguments(raw: Any) -> dict[str, Any] | None:
190
+ """Return a dict of tool arguments, parsing a JSON string when needed."""
191
+ if raw is None:
192
+ return {}
193
+ if isinstance(raw, str):
194
+ stripped = raw.strip()
195
+ if not stripped:
196
+ return {}
197
+ try:
198
+ raw = json.loads(stripped)
199
+ except json.JSONDecodeError:
200
+ return None
201
+ if isinstance(raw, dict):
202
+ return raw
203
+ return None
204
+
205
+
206
+ _REPLY_TOOL_NAMES = frozenset({"text"})
207
+ _REPLY_ARG_KEYS = ("value", "content", "text", "message")
208
+
209
+
210
+ def reply_text_from_call(call: dict[str, Any]) -> str | None:
211
+ """Return the visible reply when a model wrapped plain text as a tool.
212
+
213
+ Some local models emit ``{"name": "text", "arguments": {"value": "..."}}``
214
+ instead of a message. ``None`` means the call is a real tool.
215
+
216
+ @param call: one ``{"name", "arguments"}`` object from inline recovery.
217
+ """
218
+ name = call.get("name")
219
+ if not isinstance(name, str) or name.strip().lower() not in _REPLY_TOOL_NAMES:
220
+ return None
221
+ arguments = call.get("arguments") or {}
222
+ if not isinstance(arguments, dict):
223
+ return ""
224
+ for key in _REPLY_ARG_KEYS:
225
+ value = arguments.get(key)
226
+ if isinstance(value, str):
227
+ return value
228
+ return ""
229
+
230
+
231
+ def _calls_from_payload(payload: Any) -> list[dict[str, Any]] | None:
232
+ """Accept one tool-call object or a list of them. Anything else is text."""
233
+ items = payload if isinstance(payload, list) else [payload]
234
+ if not items:
235
+ return None
236
+ calls: list[dict[str, Any]] = []
237
+ for item in items:
238
+ if not isinstance(item, dict):
239
+ return None
240
+ name = item.get("name")
241
+ if not isinstance(name, str) or _TOOL_NAME_RE.fullmatch(name.strip()) is None:
242
+ return None
243
+ arguments = _coerce_arguments(item.get("arguments", {}))
244
+ if arguments is None:
245
+ return None
246
+ calls.append({"name": name.strip(), "arguments": arguments})
247
+ return calls
248
+
249
+
250
+ def _parse_tool_call_blob(blob: str) -> list[dict[str, Any]] | None:
251
+ """Parse a tool-call JSON blob, unwrapping one surrounding fence."""
252
+ blob = blob.strip()
253
+ if not blob:
254
+ return None
255
+ fenced = _FENCE_RE.fullmatch(blob)
256
+ if fenced is not None:
257
+ blob = fenced.group(1).strip()
258
+ if not blob:
259
+ return None
260
+ try:
261
+ payload = json.loads(blob)
262
+ except json.JSONDecodeError:
263
+ return None
264
+ return _calls_from_payload(payload)
265
+
266
+
267
+ def _extract_json_tool_calls(text: str) -> tuple[list[dict[str, Any]], str] | None:
268
+ """Recover fenced / ``<tool_call>`` / whole-message JSON tool calls.
269
+
270
+ Returns ``None`` when the text is not this format, so the caller can
271
+ leave it untouched.
272
+ """
273
+ results: list[dict[str, Any]] = []
274
+ spans: list[tuple[int, int]] = []
275
+
276
+ def take(match: re.Match[str], blob: str) -> None:
277
+ if any(match.start() < end and match.end() > start for start, end in spans):
278
+ return
279
+ calls = _parse_tool_call_blob(blob)
280
+ if not calls:
281
+ return
282
+ results.extend(calls)
283
+ spans.append(match.span())
284
+
285
+ for match in _TOOL_CALL_BLOCK_RE.finditer(text):
286
+ take(match, match.group(1))
287
+ for match in _FENCE_RE.finditer(text):
288
+ take(match, match.group(1))
289
+ if not results:
290
+ unclosed = re.search(r"```[^\n`]*\n(.*)\Z", text, re.DOTALL)
291
+ if unclosed is not None:
292
+ calls = _parse_tool_call_blob(unclosed.group(1))
293
+ if calls:
294
+ results.extend(calls)
295
+ spans.append(unclosed.span())
296
+ if not results:
297
+ stripped = text.strip()
298
+ if stripped[:1] in "{[":
299
+ calls = _parse_tool_call_blob(stripped)
300
+ if calls:
301
+ return calls, ""
302
+ return None
303
+
304
+ cleaned = text
305
+ for start, end in sorted(spans, reverse=True):
306
+ cleaned = cleaned[:start] + cleaned[end:]
307
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip()
308
+ return results, cleaned
309
+
310
+
162
311
  # ---------------------------------------------------------------------------
163
312
  # Streaming-time stripping (display path)
164
313
  # ---------------------------------------------------------------------------
@@ -378,8 +527,171 @@ class _MdCallFilter:
378
527
  return tail
379
528
 
380
529
 
530
+ def _split_partial_backticks(buf: str) -> tuple[str, str]:
531
+ """Hold a trailing 1-2 backticks that may still become a fence opener."""
532
+ hold = 0
533
+ for char in reversed(buf):
534
+ if char != "`" or hold == 2:
535
+ break
536
+ hold += 1
537
+ if hold == 0:
538
+ return buf, ""
539
+ return buf[:-hold], buf[-hold:]
540
+
541
+
542
+ def _opened_fence_body(held: str) -> str | None:
543
+ """Body of a span that starts with an opening fence.
544
+
545
+ ``held`` includes the closing fence when the span is complete. An
546
+ unclosed span returns the text after the language line.
547
+ """
548
+ if not held.startswith("```"):
549
+ return None
550
+ if held.endswith("```") and len(held) >= 6:
551
+ match = _FENCE_RE.fullmatch(held)
552
+ if match is None:
553
+ return None
554
+ return match.group(1)
555
+ rest = held[3:]
556
+ newline = rest.find("\n")
557
+ if newline < 0:
558
+ return rest
559
+ return rest[newline + 1 :]
560
+
561
+
562
+ class _JsonFenceFilter:
563
+ """Hold a ``` fence and drop it only when the body is a tool-call object.
564
+
565
+ A code sample or a JSON config is released unchanged, including when the
566
+ fence never closes. A fence larger than ``_FENCE_HOLD_CAP`` is released
567
+ so a long code block is not stuck in the filter.
568
+ """
569
+
570
+ def __init__(self) -> None:
571
+ self._buf = ""
572
+ self._open = False
573
+ self._held = ""
574
+
575
+ def feed(self, chunk: str) -> str:
576
+ buf = self._buf + (chunk or "")
577
+ self._buf = ""
578
+ out: list[str] = []
579
+ while True:
580
+ if self._open:
581
+ close = buf.find("```")
582
+ if close < 0:
583
+ self._held += buf
584
+ if len(self._held) > _FENCE_HOLD_CAP:
585
+ out.append(self._held)
586
+ self._held = ""
587
+ self._open = False
588
+ break
589
+ self._held += buf[: close + 3]
590
+ buf = buf[close + 3 :]
591
+ released = self._release_held()
592
+ if released:
593
+ out.append(released)
594
+ continue
595
+ opener = buf.find("```")
596
+ if opener >= 0:
597
+ if opener:
598
+ out.append(buf[:opener])
599
+ buf = buf[opener + 3 :]
600
+ self._open = True
601
+ self._held = "```"
602
+ continue
603
+ emit, hold = _split_partial_backticks(buf)
604
+ if emit:
605
+ out.append(emit)
606
+ self._buf = hold
607
+ break
608
+ return "".join(out)
609
+
610
+ def _release_held(self) -> str:
611
+ held = self._held
612
+ self._held = ""
613
+ self._open = False
614
+ body = _opened_fence_body(held)
615
+ if body is not None and _parse_tool_call_blob(body):
616
+ return ""
617
+ return held
618
+
619
+ def flush(self) -> str:
620
+ if self._open:
621
+ self._held += self._buf
622
+ self._buf = ""
623
+ return self._release_held()
624
+ tail = self._buf
625
+ self._buf = ""
626
+ return tail
627
+
628
+
629
+ class _BareToolJsonFilter:
630
+ """Hold a message that is only a tool-call JSON object.
631
+
632
+ A real tool object is dropped so recovery can execute it. A ``text``
633
+ reply object is replaced by its value. Any other leading ``{`` or ``[``
634
+ is released once it is clearly not that object.
635
+ """
636
+
637
+ def __init__(self) -> None:
638
+ self._buf = ""
639
+ self._passthrough = False
640
+
641
+ def feed(self, chunk: str) -> str:
642
+ if self._passthrough:
643
+ return chunk or ""
644
+ self._buf += chunk or ""
645
+ return self._decide(final=False)
646
+
647
+ def flush(self) -> str:
648
+ if self._passthrough:
649
+ return ""
650
+ return self._decide(final=True)
651
+
652
+ def _release(self, text: str) -> str:
653
+ self._passthrough = True
654
+ self._buf = ""
655
+ return text
656
+
657
+ def _decide(self, final: bool) -> str:
658
+ text = self._buf
659
+ stripped = text.lstrip()
660
+ if not stripped:
661
+ if final:
662
+ return self._release(text)
663
+ return ""
664
+ if stripped[0] not in "{[":
665
+ return self._release(text)
666
+ if len(text) > _FENCE_HOLD_CAP and not final:
667
+ return self._release(text)
668
+ try:
669
+ payload, end = json.JSONDecoder().raw_decode(stripped)
670
+ except json.JSONDecodeError as exc:
671
+ # An error at the end, or an open string, can still become JSON.
672
+ # `{not` and `[Tool call:` cannot, so they must stream immediately.
673
+ incomplete = exc.pos >= len(stripped) or "Unterminated" in exc.msg
674
+ if incomplete and not final and len(text) <= _FENCE_HOLD_CAP:
675
+ return ""
676
+ return self._release(text)
677
+ rest = stripped[end:]
678
+ if rest.strip():
679
+ return self._release(text)
680
+ calls = _calls_from_payload(payload)
681
+ if not calls:
682
+ return self._release(text)
683
+ if not final:
684
+ return ""
685
+ self._passthrough = True
686
+ self._buf = ""
687
+ replies = [reply_text_from_call(call) for call in calls]
688
+ if all(reply is not None for reply in replies):
689
+ return "".join(replies)
690
+ return ""
691
+
692
+
381
693
  class PseudoStreamStripper:
382
- """Display-path filter chaining the fn-echo and markdown-call filters.
694
+ """Display-path filter chaining the fn-echo, JSON, and markdown filters.
383
695
 
384
696
  Usage: ``feed`` each raw content delta, emit what comes back; at stream
385
697
  end, ``flush`` once and emit the tail. Text held back to detect markers
@@ -388,10 +700,14 @@ class PseudoStreamStripper:
388
700
 
389
701
  def __init__(self) -> None:
390
702
  self._fn = _FnEchoFilter()
703
+ self._fence = _JsonFenceFilter()
704
+ self._bare = _BareToolJsonFilter()
391
705
  self._md = _MdCallFilter()
392
706
 
393
707
  def feed(self, chunk: str) -> str:
394
- return self._md.feed(self._fn.feed(chunk))
708
+ return self._md.feed(self._bare.feed(self._fence.feed(self._fn.feed(chunk))))
395
709
 
396
710
  def flush(self) -> str:
397
- return self._md.feed(self._fn.flush()) + self._md.flush()
711
+ fenced = self._fence.feed(self._fn.flush()) + self._fence.flush()
712
+ bare = self._bare.feed(fenced) + self._bare.flush()
713
+ return self._md.feed(bare) + self._md.flush()