steerable-agent-runtime 0.6.32__tar.gz → 0.6.34__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.32 → steerable_agent_runtime-0.6.34}/PKG-INFO +1 -1
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/pyproject.toml +1 -1
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/__init__.py +2 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/antihallucination.py +18 -2
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/hooks.py +34 -2
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/llm/openai_compat.py +20 -10
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/loop.py +38 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/native_bridge.py +61 -10
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_antihallucination.py +25 -1
- steerable_agent_runtime-0.6.34/tests/test_native_default.py +124 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_provider_compat.py +21 -0
- steerable_agent_runtime-0.6.32/tests/test_native_default.py +0 -44
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/README.md +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/setup.cfg +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/approval.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/ask_user.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/branch.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/cache_control.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/calibration.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/compaction.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/config.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/default.harness.json +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/default.harness.yaml +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/delegation_gate.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/errors.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/handoff.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/harness.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/history.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/llm/__init__.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/llm/compat.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/llm/errors.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/llm/parts.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/llm/presets.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/maintenance.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/mcp.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/model_info.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/orchestration.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/otel.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/plugins.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/pool.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/pricing.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/pseudo.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/recording.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/reminders.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/replay.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/resume.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/retry.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/skills.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/spill.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/storage/__init__.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/subagent.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/todo.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/tokens.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/tool_search.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/tools.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/tracing.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/transport/__init__.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime/world_state.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_approval.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_ask_user.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_branch.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_cache_control.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_cache_instrumentation.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_calibration.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_compaction.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_compaction_strategy.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_config.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_content_parts.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_delegation_gate.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_error_taxonomy.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_fragment_bounds.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_golden.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_handoff.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_harness.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_history.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_history_persistence.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_hooks.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_in_memory_storage.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_llm_gemini_wire.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_llm_responses_wire.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_llm_stream_e2e.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_llm_wire_helpers.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_long_session.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_loop.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_loop_cancellation.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_loop_replay.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_loop_sandbox_event.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_loop_terminal.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_maintenance.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_mcp.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_model_equivalence.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_model_info.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_orchestration.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_otel.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_parallel_tools.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_plugins.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_provider_presets.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_pseudo.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_pyo3_api_surface.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_recording.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_reminders.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_replay_crosslang.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_resume.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_retry_hooks.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_safety_gate.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_skills.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_soft_timeout.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_spill.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_sqlite_storage.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_steer.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_storage_contract.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_stream_strip.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_subagent.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_todo.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_todo_gate.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_tokens.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_tool_exposure.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_tool_hygiene.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_tool_router.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_tool_timeout.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_trace_recorder.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_transport_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_transport_sse.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_usage_attribution.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_world_state.py +0 -0
- {steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_write_lease.py +0 -0
|
@@ -131,6 +131,7 @@ from .hooks import (
|
|
|
131
131
|
ChainHooks,
|
|
132
132
|
CompletionAction,
|
|
133
133
|
CompletionDraft,
|
|
134
|
+
DecisionNote,
|
|
134
135
|
LoopHooks,
|
|
135
136
|
NoopHooks,
|
|
136
137
|
PreStepAction,
|
|
@@ -339,6 +340,7 @@ __all__ = [
|
|
|
339
340
|
"ContextFragment",
|
|
340
341
|
"ContextManager",
|
|
341
342
|
"CoreLoop",
|
|
343
|
+
"DecisionNote",
|
|
342
344
|
"ExecutionBudget",
|
|
343
345
|
"FamilyTree",
|
|
344
346
|
"FamilyTreeNode",
|
|
@@ -34,7 +34,13 @@ import re
|
|
|
34
34
|
from dataclasses import dataclass
|
|
35
35
|
from typing import TYPE_CHECKING, Any, Literal, Sequence
|
|
36
36
|
|
|
37
|
-
from .hooks import
|
|
37
|
+
from .hooks import (
|
|
38
|
+
CompletionAction,
|
|
39
|
+
CompletionDraft,
|
|
40
|
+
DecisionNote,
|
|
41
|
+
NoopHooks,
|
|
42
|
+
PreStepAction,
|
|
43
|
+
)
|
|
38
44
|
|
|
39
45
|
if TYPE_CHECKING:
|
|
40
46
|
from .llm import LLMMessage, LLMProvider
|
|
@@ -473,12 +479,22 @@ class AntiHallucinationHooks(NoopHooks):
|
|
|
473
479
|
):
|
|
474
480
|
return PreStepAction(kind="proceed")
|
|
475
481
|
self._route = await self._classify_route()
|
|
482
|
+
# Both branches record the verdict: the allow_no_tool turns are the
|
|
483
|
+
# denominator for how often routing forces a tool at all, and a run
|
|
484
|
+
# with no note at all means routing was skipped (plan mode, no tools,
|
|
485
|
+
# forcing disabled) rather than that it allowed a free turn.
|
|
486
|
+
note = DecisionNote(
|
|
487
|
+
action="data_need_route",
|
|
488
|
+
reason="anti-hallucination data-need routing",
|
|
489
|
+
value=self._route,
|
|
490
|
+
)
|
|
476
491
|
if self._route == "require_tool":
|
|
477
492
|
return PreStepAction(
|
|
478
493
|
kind="proceed",
|
|
479
494
|
tool_choice=self._config.force_tool_choice,
|
|
495
|
+
notes=(note,),
|
|
480
496
|
)
|
|
481
|
-
return PreStepAction(kind="proceed")
|
|
497
|
+
return PreStepAction(kind="proceed", notes=(note,))
|
|
482
498
|
|
|
483
499
|
async def _classify_route(self) -> DataNeedRoute:
|
|
484
500
|
from .llm import LLMMessage
|
|
@@ -32,7 +32,7 @@ existing behavior.
|
|
|
32
32
|
from __future__ import annotations
|
|
33
33
|
|
|
34
34
|
import logging
|
|
35
|
-
from dataclasses import dataclass
|
|
35
|
+
from dataclasses import dataclass, replace
|
|
36
36
|
from typing import TYPE_CHECKING, Literal, Protocol, runtime_checkable
|
|
37
37
|
|
|
38
38
|
from steerable_agent_protocol.generated import ToolCall, ToolResult
|
|
@@ -118,6 +118,26 @@ class RewriteRequest:
|
|
|
118
118
|
bracket: CompactionBracket | None = None
|
|
119
119
|
|
|
120
120
|
|
|
121
|
+
@dataclass(frozen=True, slots=True)
|
|
122
|
+
class DecisionNote:
|
|
123
|
+
"""A hook verdict the loop should record even though it changed nothing.
|
|
124
|
+
|
|
125
|
+
Verdicts that *act* are already visible as ``hook_action`` (a rewrite, an
|
|
126
|
+
append, a ``tool_choice``). The verdict that declined to act leaves no
|
|
127
|
+
trace, so a trajectory cannot distinguish "the data-need router allowed a
|
|
128
|
+
no-tool turn" from "routing never ran" — and that is exactly the
|
|
129
|
+
denominator a routing-variance measurement needs.
|
|
130
|
+
|
|
131
|
+
``value`` carries the verdict itself (a route name, a chosen option) and
|
|
132
|
+
``probability`` the backend's confidence when it reports one.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
action: str
|
|
136
|
+
reason: str
|
|
137
|
+
value: str | None = None
|
|
138
|
+
probability: float | None = None
|
|
139
|
+
|
|
140
|
+
|
|
121
141
|
@dataclass(slots=True)
|
|
122
142
|
class PreStepAction:
|
|
123
143
|
"""Outcome of a ``pre_step`` hook.
|
|
@@ -131,6 +151,9 @@ class PreStepAction:
|
|
|
131
151
|
this step's LLM call — the data-need router uses it to force a tool call
|
|
132
152
|
on the first round of a data-seeking turn. Pass-through value, forwarded
|
|
133
153
|
as a provider kwarg; providers that cannot honor it ignore it.
|
|
154
|
+
|
|
155
|
+
``notes`` records verdicts that took no action, so a hook's decision rate
|
|
156
|
+
is measurable and not just its intervention rate.
|
|
134
157
|
"""
|
|
135
158
|
|
|
136
159
|
kind: Literal["proceed", "reject"] = "proceed"
|
|
@@ -138,6 +161,7 @@ class PreStepAction:
|
|
|
138
161
|
rewrite: RewriteRequest | None = None
|
|
139
162
|
reason: str | None = None
|
|
140
163
|
tool_choice: str | None = None
|
|
164
|
+
notes: tuple[DecisionNote, ...] = ()
|
|
141
165
|
#: Label for the ``hook_action`` event when this action appended context
|
|
142
166
|
#: (a rewrite is labelled by its own ``RewriteRequest.action``). Skill
|
|
143
167
|
#: catalog injection owns ``"skill_catalog"``; the generic default is
|
|
@@ -327,6 +351,9 @@ class ChainHooks:
|
|
|
327
351
|
after a rewrite fold into that rewrite — "hook1 rewrites, hook2
|
|
328
352
|
appends" is exactly ``replace_all`` then ``append``. The first
|
|
329
353
|
``reject`` wins; the first non-``None`` ``tool_choice`` wins.
|
|
354
|
+
``notes`` accumulate from every hook in order: a note describes a
|
|
355
|
+
decision instead of declaring one, so the hook that loses the
|
|
356
|
+
``tool_choice`` must still have its verdict recorded.
|
|
330
357
|
- ``post_tool_result``: the result threads through each hook in order.
|
|
331
358
|
- ``on_request_error``: the first ``retry`` decision wins; if every hook
|
|
332
359
|
says ``fail``, the first failure reason is surfaced.
|
|
@@ -356,10 +383,14 @@ class ChainHooks:
|
|
|
356
383
|
appends: list[TranscriptAppend] = []
|
|
357
384
|
append_action: str | None = None
|
|
358
385
|
reason: str | None = None
|
|
386
|
+
notes: list[DecisionNote] = []
|
|
359
387
|
for hook in self._hooks:
|
|
360
388
|
action = await hook.pre_step(current, ctx)
|
|
389
|
+
notes.extend(action.notes)
|
|
361
390
|
if action.kind == "reject":
|
|
362
|
-
|
|
391
|
+
# A reject ends the turn, but the verdicts that preceded it
|
|
392
|
+
# still describe how the turn got there.
|
|
393
|
+
return replace(action, notes=tuple(notes))
|
|
363
394
|
if action.rewrite is not None:
|
|
364
395
|
# The rewriter computed against the current projection, so
|
|
365
396
|
# its message list already contains any earlier appends.
|
|
@@ -391,6 +422,7 @@ class ChainHooks:
|
|
|
391
422
|
reason=reason,
|
|
392
423
|
tool_choice=tool_choice,
|
|
393
424
|
append_action=append_action,
|
|
425
|
+
notes=tuple(notes),
|
|
394
426
|
)
|
|
395
427
|
|
|
396
428
|
async def compact_now(
|
|
@@ -162,6 +162,19 @@ class OpenAICompatProvider:
|
|
|
162
162
|
# Public API
|
|
163
163
|
# ------------------------------------------------------------------
|
|
164
164
|
|
|
165
|
+
def honors_forced_tool_choice(self) -> bool:
|
|
166
|
+
"""Whether ``tool_choice="required"`` reaches the wire unchanged.
|
|
167
|
+
|
|
168
|
+
``_build_body`` downgrades it to ``auto`` for vendors that reject the
|
|
169
|
+
forced value, which makes a caller's gate a silent no-op. Callers that
|
|
170
|
+
record the gate (the loop's ``hook_action``) ask first so the trace
|
|
171
|
+
distinguishes a gate that held from one the vendor dropped.
|
|
172
|
+
"""
|
|
173
|
+
compat = self.compat or OpenAICompatFlags()
|
|
174
|
+
return compat.supports_forced_tool_choice and not (
|
|
175
|
+
_thinking_rejects_forced_tool_choice(self.model, self.base_url)
|
|
176
|
+
)
|
|
177
|
+
|
|
165
178
|
async def complete(
|
|
166
179
|
self,
|
|
167
180
|
messages: Sequence[LLMMessage],
|
|
@@ -385,16 +398,13 @@ class OpenAICompatProvider:
|
|
|
385
398
|
for key, value in preset.extra_body.items():
|
|
386
399
|
if key not in body:
|
|
387
400
|
body[key] = value
|
|
388
|
-
# ``tool_choice=required`` downgrade
|
|
389
|
-
#
|
|
390
|
-
#
|
|
391
|
-
#
|
|
392
|
-
#
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if body.get("tool_choice") == "required" and (
|
|
396
|
-
not compat.supports_forced_tool_choice
|
|
397
|
-
or _thinking_rejects_forced_tool_choice(self.model, self.base_url)
|
|
401
|
+
# ``tool_choice=required`` downgrade (see honors_forced_tool_choice):
|
|
402
|
+
# the wire must send auto for these vendors or the trial dies on round
|
|
403
|
+
# 0 (failed-prev 33335200327, cheap-12 Qwen×steerable 34439098611).
|
|
404
|
+
# The caller's ``hook_action`` carries ``honored: false`` here, so a
|
|
405
|
+
# forced turn that the vendor dropped stays distinguishable in traces.
|
|
406
|
+
if body.get("tool_choice") == "required" and not (
|
|
407
|
+
self.honors_forced_tool_choice()
|
|
398
408
|
):
|
|
399
409
|
body["tool_choice"] = "auto"
|
|
400
410
|
# W6-8: clamp the requested reasoning effort to a level the model
|
|
@@ -1184,6 +1184,22 @@ class CoreLoop:
|
|
|
1184
1184
|
"round": round_index,
|
|
1185
1185
|
},
|
|
1186
1186
|
)
|
|
1187
|
+
# Verdicts that took no action. Without these a trace shows only
|
|
1188
|
+
# the rounds a hook intervened in, which is a numerator with no
|
|
1189
|
+
# denominator — "routing allowed a no-tool turn" and "routing
|
|
1190
|
+
# never ran" look identical.
|
|
1191
|
+
for note in pre.notes:
|
|
1192
|
+
yield LoopEvent(
|
|
1193
|
+
"hook_action",
|
|
1194
|
+
{
|
|
1195
|
+
"hook": "pre_step",
|
|
1196
|
+
"action": note.action,
|
|
1197
|
+
"reason": note.reason,
|
|
1198
|
+
"value": note.value,
|
|
1199
|
+
"probability": note.probability,
|
|
1200
|
+
"round": round_index,
|
|
1201
|
+
},
|
|
1202
|
+
)
|
|
1187
1203
|
# ── manual compact: host-requested (agent.chat.compact, CC ────
|
|
1188
1204
|
# ── /compact parity) ──
|
|
1189
1205
|
# Consumed AFTER the regular pre_step pass, on the projection
|
|
@@ -1227,6 +1243,14 @@ class CoreLoop:
|
|
|
1227
1243
|
"hook": "pre_step",
|
|
1228
1244
|
"action": "tool_choice",
|
|
1229
1245
|
"value": step_tool_choice,
|
|
1246
|
+
# Several vendors downgrade ``required`` to ``auto`` on
|
|
1247
|
+
# the wire (Z.AI GLM, Qwen/DeepSeek thinking). Logging
|
|
1248
|
+
# the hook without this reads as a gate that held, so
|
|
1249
|
+
# every measurement over forced turns silently mixes
|
|
1250
|
+
# the models where the gate is a no-op.
|
|
1251
|
+
"honored": _honors_forced_tool_choice(
|
|
1252
|
+
self._provider, step_tool_choice
|
|
1253
|
+
),
|
|
1230
1254
|
"round": round_index,
|
|
1231
1255
|
},
|
|
1232
1256
|
)
|
|
@@ -2459,6 +2483,20 @@ async def _safe_aclose(stream: Any) -> None:
|
|
|
2459
2483
|
pass
|
|
2460
2484
|
|
|
2461
2485
|
|
|
2486
|
+
def _honors_forced_tool_choice(provider: LLMProvider, tool_choice: str) -> bool:
|
|
2487
|
+
"""Whether ``tool_choice`` survives to the wire on this provider.
|
|
2488
|
+
|
|
2489
|
+
Only ``required`` is ever downgraded, so any other value is honored by
|
|
2490
|
+
definition. The capability is duck-typed like ``on_stream_chunk``: a
|
|
2491
|
+
provider that does not publish ``honors_forced_tool_choice`` is taken at
|
|
2492
|
+
its word, since the downgrade is a vendor quirk the adapter owns.
|
|
2493
|
+
"""
|
|
2494
|
+
if tool_choice != "required":
|
|
2495
|
+
return True
|
|
2496
|
+
honors = getattr(provider, "honors_forced_tool_choice", None)
|
|
2497
|
+
return bool(honors()) if callable(honors) else True
|
|
2498
|
+
|
|
2499
|
+
|
|
2462
2500
|
def _apply_rewrite(manager: ContextManager, rewrite: RewriteRequest) -> None:
|
|
2463
2501
|
"""Apply one declared rewrite to the record.
|
|
2464
2502
|
|
|
@@ -9,6 +9,7 @@ module is importable; ``STEERABLE_RUST_CORELOOP=0`` forces Python and
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
|
+
import contextvars
|
|
12
13
|
import json
|
|
13
14
|
import logging
|
|
14
15
|
import os
|
|
@@ -24,7 +25,13 @@ from steerable_agent_runtime.history import ContextManager
|
|
|
24
25
|
from steerable_agent_runtime.llm import LLMMessage
|
|
25
26
|
from steerable_agent_runtime.llm.errors import LLMError
|
|
26
27
|
from steerable_agent_runtime.llm.parts import ImagePart, TextPart
|
|
27
|
-
from steerable_agent_runtime.loop import
|
|
28
|
+
from steerable_agent_runtime.loop import (
|
|
29
|
+
CoreLoop,
|
|
30
|
+
LoopContext,
|
|
31
|
+
LoopEvent,
|
|
32
|
+
_apply_rewrite,
|
|
33
|
+
_honors_forced_tool_choice,
|
|
34
|
+
)
|
|
28
35
|
from steerable_agent_runtime.pseudo import (
|
|
29
36
|
PseudoStreamStripper,
|
|
30
37
|
extract_inline_tool_calls,
|
|
@@ -403,6 +410,22 @@ async def _pre_step(
|
|
|
403
410
|
else action.append_action or "append"
|
|
404
411
|
),
|
|
405
412
|
"tool_choice": action.tool_choice,
|
|
413
|
+
# The Rust engine emits the tool_choice hook_action, but only the
|
|
414
|
+
# Python provider knows whether the vendor downgrades the forced value.
|
|
415
|
+
"tool_choice_honored": (
|
|
416
|
+
_honors_forced_tool_choice(loop._provider, action.tool_choice)
|
|
417
|
+
if action.tool_choice
|
|
418
|
+
else None
|
|
419
|
+
),
|
|
420
|
+
"notes": [
|
|
421
|
+
{
|
|
422
|
+
"action": note.action,
|
|
423
|
+
"reason": note.reason,
|
|
424
|
+
"value": note.value,
|
|
425
|
+
"probability": note.probability,
|
|
426
|
+
}
|
|
427
|
+
for note in action.notes
|
|
428
|
+
],
|
|
406
429
|
"pre_tokens": (
|
|
407
430
|
rewrite.pre_tokens if rewrite is not None else None
|
|
408
431
|
),
|
|
@@ -535,8 +558,26 @@ async def _sync_history(
|
|
|
535
558
|
return json.dumps({"ok": True})
|
|
536
559
|
|
|
537
560
|
|
|
538
|
-
def _await_on_loop(
|
|
539
|
-
|
|
561
|
+
def _await_on_loop(
|
|
562
|
+
coro: Any,
|
|
563
|
+
aio_loop: asyncio.AbstractEventLoop,
|
|
564
|
+
caller_context: contextvars.Context | None = None,
|
|
565
|
+
) -> str:
|
|
566
|
+
"""Schedule ``coro`` on the running asyncio loop; block without holding the GIL.
|
|
567
|
+
|
|
568
|
+
The callback runs under ``caller_context`` so it sees the ContextVars the
|
|
569
|
+
turn was started with. Rust drives these callbacks from its own thread,
|
|
570
|
+
whose context knows nothing the process set up earlier: without this the
|
|
571
|
+
task would copy that empty context and every ContextVar read would fall
|
|
572
|
+
back to its default, while the pure-Python loop — awaiting in the caller's
|
|
573
|
+
own task — reads the value. ``run_code`` binds its nested-tool router this
|
|
574
|
+
way, so the divergence surfaced as "no tool executor bound for nested
|
|
575
|
+
calls" on the native path only.
|
|
576
|
+
|
|
577
|
+
``Task`` takes no ``context`` argument before 3.11, so the context is
|
|
578
|
+
applied one level up: ``call_soon_threadsafe`` runs ``start`` inside it and
|
|
579
|
+
``create_task`` copies from there.
|
|
580
|
+
"""
|
|
540
581
|
box: queue.Queue[str] = queue.Queue()
|
|
541
582
|
|
|
542
583
|
def start() -> None:
|
|
@@ -551,7 +592,7 @@ def _await_on_loop(coro: Any, aio_loop: asyncio.AbstractEventLoop) -> str:
|
|
|
551
592
|
|
|
552
593
|
task.add_done_callback(done)
|
|
553
594
|
|
|
554
|
-
aio_loop.call_soon_threadsafe(start)
|
|
595
|
+
aio_loop.call_soon_threadsafe(start, context=caller_context)
|
|
555
596
|
return box.get()
|
|
556
597
|
|
|
557
598
|
|
|
@@ -566,6 +607,9 @@ async def run_native(
|
|
|
566
607
|
import steerable_agent_runtime_native as native
|
|
567
608
|
|
|
568
609
|
aio_loop = asyncio.get_running_loop()
|
|
610
|
+
# Every Python callback below runs under this snapshot, so the Rust-driven
|
|
611
|
+
# path reads the same ContextVars the pure-Python loop would.
|
|
612
|
+
caller_context = contextvars.copy_context()
|
|
569
613
|
out: asyncio.Queue[tuple[LoopEvent, threading.Event] | None] = asyncio.Queue()
|
|
570
614
|
loop._run_context = LoopContext(chat_id=chat_id)
|
|
571
615
|
loop._wrap_up_active = False
|
|
@@ -735,7 +779,7 @@ async def run_native(
|
|
|
735
779
|
|
|
736
780
|
task.add_done_callback(done)
|
|
737
781
|
|
|
738
|
-
aio_loop.call_soon_threadsafe(start)
|
|
782
|
+
aio_loop.call_soon_threadsafe(start, context=caller_context)
|
|
739
783
|
first_frame = output.get()
|
|
740
784
|
first = json.loads(first_frame)
|
|
741
785
|
if first.get("kind") == "error":
|
|
@@ -783,7 +827,7 @@ async def run_native(
|
|
|
783
827
|
|
|
784
828
|
task.add_done_callback(done)
|
|
785
829
|
|
|
786
|
-
aio_loop.call_soon_threadsafe(start)
|
|
830
|
+
aio_loop.call_soon_threadsafe(start, context=caller_context)
|
|
787
831
|
return box.get()
|
|
788
832
|
|
|
789
833
|
def tool_concurrency_safe(call_json: str) -> bool:
|
|
@@ -810,6 +854,7 @@ async def run_native(
|
|
|
810
854
|
return _await_on_loop(
|
|
811
855
|
_sync_history(loop, manager, history_json, chat_id),
|
|
812
856
|
aio_loop,
|
|
857
|
+
caller_context,
|
|
813
858
|
)
|
|
814
859
|
|
|
815
860
|
def sync_context(context_json: str) -> str:
|
|
@@ -848,17 +893,23 @@ async def run_native(
|
|
|
848
893
|
return json.dumps({"ok": True})
|
|
849
894
|
|
|
850
895
|
def before_completion(draft_json: str) -> str:
|
|
851
|
-
return _await_on_loop(
|
|
896
|
+
return _await_on_loop(
|
|
897
|
+
_before_completion(loop, draft_json), aio_loop, caller_context
|
|
898
|
+
)
|
|
852
899
|
|
|
853
900
|
def post_tool_result(payload_json: str) -> str:
|
|
854
|
-
return _await_on_loop(
|
|
901
|
+
return _await_on_loop(
|
|
902
|
+
_post_tool_result(loop, payload_json), aio_loop, caller_context
|
|
903
|
+
)
|
|
855
904
|
|
|
856
905
|
def pre_step(transcript_json: str) -> str:
|
|
857
|
-
return _await_on_loop(
|
|
906
|
+
return _await_on_loop(
|
|
907
|
+
_pre_step(loop, manager, transcript_json), aio_loop, caller_context
|
|
908
|
+
)
|
|
858
909
|
|
|
859
910
|
def on_request_error(payload_json: str) -> str:
|
|
860
911
|
return _await_on_loop(
|
|
861
|
-
_on_request_error(loop, manager, payload_json), aio_loop
|
|
912
|
+
_on_request_error(loop, manager, payload_json), aio_loop, caller_context
|
|
862
913
|
)
|
|
863
914
|
|
|
864
915
|
def wrap_up_may_drop_tools() -> bool:
|
{steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_antihallucination.py
RENAMED
|
@@ -107,6 +107,15 @@ def final_completion(events: list[LoopEvent]) -> dict[str, Any]:
|
|
|
107
107
|
return completions[-1].data
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
def route_notes(events: list[LoopEvent]) -> list[tuple[str, str]]:
|
|
111
|
+
"""``(action, value)`` of every recorded data-need routing verdict."""
|
|
112
|
+
return [
|
|
113
|
+
(e.data["action"], e.data["value"])
|
|
114
|
+
for e in events
|
|
115
|
+
if e.kind == "hook_action" and e.data.get("action") == "data_need_route"
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
|
|
110
119
|
def make_router_with_tool() -> ToolRouter:
|
|
111
120
|
router = ToolRouter()
|
|
112
121
|
|
|
@@ -325,6 +334,13 @@ async def test_routing_require_tool_forces_tool_choice_on_first_call() -> None:
|
|
|
325
334
|
# 第二轮起不再强制
|
|
326
335
|
assert provider.stream_kwargs[1].get("tool_choice") is None
|
|
327
336
|
assert final_completion(events)["status"] == "completed"
|
|
337
|
+
assert route_notes(events) == [("data_need_route", "require_tool")]
|
|
338
|
+
# 这个 fake provider 不声明 honors_forced_tool_choice,按"照实发送"处理。
|
|
339
|
+
forced = [
|
|
340
|
+
e for e in events
|
|
341
|
+
if e.kind == "hook_action" and e.data.get("action") == "tool_choice"
|
|
342
|
+
]
|
|
343
|
+
assert [e.data["honored"] for e in forced] == [True]
|
|
328
344
|
|
|
329
345
|
|
|
330
346
|
@pytest.mark.asyncio
|
|
@@ -341,6 +357,9 @@ async def test_routing_allow_no_tool_passes_through() -> None:
|
|
|
341
357
|
|
|
342
358
|
assert provider.stream_kwargs[0].get("tool_choice") is None
|
|
343
359
|
assert final_completion(events)["status"] == "completed"
|
|
360
|
+
# allow_no_tool 不改变这一步,但必须留痕:没有它,"路由放行"和"路由没跑"
|
|
361
|
+
# 在轨迹里长得一样,强制率就没有分母。
|
|
362
|
+
assert route_notes(events) == [("data_need_route", "allow_no_tool")]
|
|
344
363
|
|
|
345
364
|
|
|
346
365
|
@pytest.mark.asyncio
|
|
@@ -353,8 +372,13 @@ async def test_routing_classification_error_falls_back_to_require_tool() -> None
|
|
|
353
372
|
provider, AntiHallucinationConfig(user_question="查一下数值")
|
|
354
373
|
)
|
|
355
374
|
loop = CoreLoop(provider, RouterToolExecutor(make_router_with_tool()), hooks=hooks)
|
|
356
|
-
await collect(
|
|
375
|
+
events = await collect(
|
|
376
|
+
loop.run([LLMMessage.text_of("user", "查一下数值")], tools=TOOLS)
|
|
377
|
+
)
|
|
357
378
|
assert provider.stream_kwargs[0].get("tool_choice") == "required"
|
|
379
|
+
# 分类失败走保守回退,留痕与真判 require_tool 无法区分——这是有意的:
|
|
380
|
+
# 记录的是生效的裁决,不是它的来源。
|
|
381
|
+
assert route_notes(events) == [("data_need_route", "require_tool")]
|
|
358
382
|
|
|
359
383
|
|
|
360
384
|
@pytest.mark.asyncio
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Rust CoreLoop is the default when the native binding is importable."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextvars
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from steerable_agent_protocol.generated import ToolCall
|
|
11
|
+
from steerable_agent_runtime import CoreLoop, RouterToolExecutor, ToolRouter, tool
|
|
12
|
+
from steerable_agent_runtime.llm import LLMMessage, LLMStreamChunk
|
|
13
|
+
from steerable_agent_runtime.native_bridge import native_available, uses_native_coreloop
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_uses_native_coreloop_follows_availability_when_unset(
|
|
17
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
18
|
+
) -> None:
|
|
19
|
+
monkeypatch.delenv("STEERABLE_RUST_CORELOOP", raising=False)
|
|
20
|
+
monkeypatch.setattr(
|
|
21
|
+
"steerable_agent_runtime.native_bridge.native_available",
|
|
22
|
+
lambda: True,
|
|
23
|
+
)
|
|
24
|
+
assert uses_native_coreloop() is True
|
|
25
|
+
monkeypatch.setattr(
|
|
26
|
+
"steerable_agent_runtime.native_bridge.native_available",
|
|
27
|
+
lambda: False,
|
|
28
|
+
)
|
|
29
|
+
assert uses_native_coreloop() is False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_uses_native_coreloop_zero_forces_python(
|
|
33
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
34
|
+
) -> None:
|
|
35
|
+
monkeypatch.setenv("STEERABLE_RUST_CORELOOP", "0")
|
|
36
|
+
monkeypatch.setattr(
|
|
37
|
+
"steerable_agent_runtime.native_bridge.native_available",
|
|
38
|
+
lambda: True,
|
|
39
|
+
)
|
|
40
|
+
assert uses_native_coreloop() is False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_uses_native_coreloop_one_forces_rust(
|
|
44
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
45
|
+
) -> None:
|
|
46
|
+
monkeypatch.setenv("STEERABLE_RUST_CORELOOP", "1")
|
|
47
|
+
monkeypatch.setattr(
|
|
48
|
+
"steerable_agent_runtime.native_bridge.native_available",
|
|
49
|
+
lambda: False,
|
|
50
|
+
)
|
|
51
|
+
assert uses_native_coreloop() is True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
#: Stand-in for the process-lifetime ContextVars callers bind around a turn —
|
|
55
|
+
#: ``run_code`` keeps its nested-tool router in one.
|
|
56
|
+
_ambient: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
|
57
|
+
"test_ambient", default=None
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _provider_calling(name: str) -> Any:
|
|
62
|
+
class _FakeProvider:
|
|
63
|
+
name = "fake"
|
|
64
|
+
model = "fake-model"
|
|
65
|
+
|
|
66
|
+
def __init__(self) -> None:
|
|
67
|
+
self._idx = 0
|
|
68
|
+
|
|
69
|
+
async def complete(self, messages, *, tools=None, **kw): # pragma: no cover
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
def stream(self, messages, *, tools=None, **kw) -> AsyncIterator[LLMStreamChunk]:
|
|
73
|
+
first = self._idx == 0
|
|
74
|
+
self._idx += 1
|
|
75
|
+
|
|
76
|
+
async def _gen() -> AsyncIterator[LLMStreamChunk]:
|
|
77
|
+
if first:
|
|
78
|
+
yield LLMStreamChunk(
|
|
79
|
+
tool_call_delta=ToolCall(id="c1", name=name, arguments={})
|
|
80
|
+
)
|
|
81
|
+
yield LLMStreamChunk(finish_reason="tool_calls")
|
|
82
|
+
else:
|
|
83
|
+
yield LLMStreamChunk(content_delta="done")
|
|
84
|
+
yield LLMStreamChunk(finish_reason="stop")
|
|
85
|
+
|
|
86
|
+
return _gen()
|
|
87
|
+
|
|
88
|
+
return _FakeProvider()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.skipif(
|
|
92
|
+
not native_available(), reason="the context bridge only applies to the Rust loop"
|
|
93
|
+
)
|
|
94
|
+
@pytest.mark.asyncio
|
|
95
|
+
async def test_native_tool_execution_sees_the_callers_context_vars() -> None:
|
|
96
|
+
"""Rust drives the Python callbacks from its own thread, so a task created
|
|
97
|
+
for them copies that thread's context, not the turn's. A tool that reads an
|
|
98
|
+
ambient ContextVar would then see the default while the pure-Python loop
|
|
99
|
+
sees the value — which is how ``run_code`` lost its nested-tool router.
|
|
100
|
+
"""
|
|
101
|
+
router = ToolRouter()
|
|
102
|
+
seen: list[str | None] = []
|
|
103
|
+
|
|
104
|
+
@tool(router=router, name="peek", description="read the ambient var", mode="read")
|
|
105
|
+
async def peek() -> dict[str, Any]:
|
|
106
|
+
seen.append(_ambient.get())
|
|
107
|
+
return {"value": seen[-1]}
|
|
108
|
+
|
|
109
|
+
_ambient.set("bound-by-caller")
|
|
110
|
+
loop = CoreLoop(_provider_calling("peek"), RouterToolExecutor(router))
|
|
111
|
+
tools = [
|
|
112
|
+
{
|
|
113
|
+
"type": "function",
|
|
114
|
+
"function": {
|
|
115
|
+
"name": "peek",
|
|
116
|
+
"description": "read",
|
|
117
|
+
"parameters": {"type": "object", "properties": {}},
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
async for _event in loop.run([LLMMessage.text_of("user", "go")], tools=tools):
|
|
122
|
+
pass
|
|
123
|
+
|
|
124
|
+
assert seen == ["bound-by-caller"]
|
{steerable_agent_runtime-0.6.32 → steerable_agent_runtime-0.6.34}/tests/test_provider_compat.py
RENAMED
|
@@ -114,6 +114,27 @@ def test_forced_tool_choice_downgraded_when_flagged_off() -> None:
|
|
|
114
114
|
assert body["tool_choice"] == "auto"
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
def test_honors_forced_tool_choice_matches_the_downgrade() -> None:
|
|
118
|
+
"""The probe callers record must agree with what the body actually sends.
|
|
119
|
+
|
|
120
|
+
Two sources downgrade: the compat flag and the model-name paths host
|
|
121
|
+
flags cannot cover (Z.AI GLM, Qwen/DeepSeek thinking over OpenRouter).
|
|
122
|
+
"""
|
|
123
|
+
assert _provider().honors_forced_tool_choice() is True
|
|
124
|
+
assert (
|
|
125
|
+
_provider(
|
|
126
|
+
compat=OpenAICompatFlags(supports_forced_tool_choice=False)
|
|
127
|
+
).honors_forced_tool_choice()
|
|
128
|
+
is False
|
|
129
|
+
)
|
|
130
|
+
assert (
|
|
131
|
+
_provider(
|
|
132
|
+
model="z-ai/glm-5", base_url="https://openrouter.ai/api/v1"
|
|
133
|
+
).honors_forced_tool_choice()
|
|
134
|
+
is False
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
117
138
|
def test_forced_tool_choice_kept_by_default() -> None:
|
|
118
139
|
from steerable_agent_runtime.llm import LLMMessage
|
|
119
140
|
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
"""Rust CoreLoop is the default when the native binding is importable."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
from steerable_agent_runtime.native_bridge import uses_native_coreloop
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def test_uses_native_coreloop_follows_availability_when_unset(
|
|
10
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
11
|
-
) -> None:
|
|
12
|
-
monkeypatch.delenv("STEERABLE_RUST_CORELOOP", raising=False)
|
|
13
|
-
monkeypatch.setattr(
|
|
14
|
-
"steerable_agent_runtime.native_bridge.native_available",
|
|
15
|
-
lambda: True,
|
|
16
|
-
)
|
|
17
|
-
assert uses_native_coreloop() is True
|
|
18
|
-
monkeypatch.setattr(
|
|
19
|
-
"steerable_agent_runtime.native_bridge.native_available",
|
|
20
|
-
lambda: False,
|
|
21
|
-
)
|
|
22
|
-
assert uses_native_coreloop() is False
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_uses_native_coreloop_zero_forces_python(
|
|
26
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
27
|
-
) -> None:
|
|
28
|
-
monkeypatch.setenv("STEERABLE_RUST_CORELOOP", "0")
|
|
29
|
-
monkeypatch.setattr(
|
|
30
|
-
"steerable_agent_runtime.native_bridge.native_available",
|
|
31
|
-
lambda: True,
|
|
32
|
-
)
|
|
33
|
-
assert uses_native_coreloop() is False
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_uses_native_coreloop_one_forces_rust(
|
|
37
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
38
|
-
) -> None:
|
|
39
|
-
monkeypatch.setenv("STEERABLE_RUST_CORELOOP", "1")
|
|
40
|
-
monkeypatch.setattr(
|
|
41
|
-
"steerable_agent_runtime.native_bridge.native_available",
|
|
42
|
-
lambda: False,
|
|
43
|
-
)
|
|
44
|
-
assert uses_native_coreloop() is True
|
|
File without changes
|
|
File without changes
|