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.
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/PKG-INFO +1 -1
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/pyproject.toml +1 -1
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/__init__.py +2 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/antihallucination.py +13 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/ask_user.py +34 -13
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/history.py +11 -7
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/hooks.py +32 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/pseudo.py +227 -3
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/skills.py +76 -22
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_antihallucination.py +58 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_ask_user.py +53 -10
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_fragment_bounds.py +17 -2
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_pseudo.py +64 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_skills.py +139 -8
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_stream_strip.py +53 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/README.md +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/setup.cfg +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/approval.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/branch.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/cache_control.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/calibration.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/compaction.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/config.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/default.harness.json +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/default.harness.yaml +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/delegation_gate.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/errors.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/handoff.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/harness.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/__init__.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/compat.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/errors.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/parts.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/presets.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/loop.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/maintenance.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/mcp.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/model_info.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/native_bridge.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/orchestration.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/otel.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/plugins.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/pool.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/pricing.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/recording.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/reminders.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/replay.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/resume.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/retry.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/spill.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/storage/__init__.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/subagent.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/todo.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tokens.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tool_presentation.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tool_search.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tools.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/tracing.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/transport/__init__.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime/world_state.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_approval.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_branch.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_cache_control.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_cache_instrumentation.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_calibration.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_compaction.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_compaction_strategy.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_config.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_content_parts.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_delegation_gate.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_error_taxonomy.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_golden.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_handoff.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_harness.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_history.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_history_persistence.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_hooks.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_in_memory_storage.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_llm_gemini_wire.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_llm_responses_wire.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_llm_stream_e2e.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_llm_wire_helpers.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_long_session.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_loop.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_loop_cancellation.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_loop_replay.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_loop_sandbox_event.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_loop_terminal.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_maintenance.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_mcp.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_model_equivalence.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_model_info.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_native_default.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_orchestration.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_otel.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_parallel_tools.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_plugins.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_provider_compat.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_provider_presets.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_pyo3_api_surface.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_recording.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_reminders.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_replay_crosslang.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_resume.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_retry_hooks.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_safety_gate.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_soft_timeout.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_spill.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_sqlite_storage.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_steer.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_storage_contract.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_subagent.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_todo.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_todo_gate.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tokens.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_exposure.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_hygiene.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_presentation.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_router.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_tool_timeout.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_trace_recorder.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_transport_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_transport_sse.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_usage_attribution.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_world_state.py +0 -0
- {steerable_agent_runtime-0.6.72 → steerable_agent_runtime-0.6.73}/tests/test_write_lease.py +0 -0
|
@@ -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, ``
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
170
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
376
|
-
"environment + tool reality check)
|
|
377
|
-
"
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
"
|
|
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
|
|
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
|
-
|
|
620
|
+
mid = self._fence.feed(self._fn.flush()) + self._fence.flush()
|
|
621
|
+
return self._md.feed(mid) + self._md.flush()
|