steerable-agent-runtime 0.6.51__tar.gz → 0.6.53__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.51 → steerable_agent_runtime-0.6.53}/PKG-INFO +3 -3
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/pyproject.toml +7 -7
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/__init__.py +6 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/approval.py +86 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/default.harness.json +0 -2
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/default.harness.yaml +4 -2
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/mcp.py +242 -7
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/orchestration.py +69 -10
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pool.py +32 -5
- steerable_agent_runtime-0.6.53/src/steerable_agent_runtime/tool_presentation.py +110 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tools.py +7 -1
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/PKG-INFO +3 -3
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/SOURCES.txt +2 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/requires.txt +2 -2
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_approval.py +101 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_mcp.py +155 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_orchestration.py +246 -0
- steerable_agent_runtime-0.6.53/tests/test_tool_presentation.py +113 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/README.md +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/setup.cfg +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/antihallucination.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/ask_user.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/branch.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/cache_control.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/calibration.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/compaction.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/config.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/delegation_gate.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/errors.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/handoff.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/harness.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/history.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/hooks.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/__init__.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/compat.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/errors.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/parts.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/presets.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/loop.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/maintenance.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_info.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/native_bridge.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/otel.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/plugins.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pricing.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/pseudo.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/recording.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/reminders.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/replay.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/resume.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/retry.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/skills.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/spill.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/__init__.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/subagent.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/todo.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tokens.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tool_search.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/tracing.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/__init__.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/world_state.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_antihallucination.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_ask_user.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_branch.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_cache_control.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_cache_instrumentation.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_calibration.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_compaction.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_compaction_strategy.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_config.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_content_parts.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_delegation_gate.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_error_taxonomy.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_fragment_bounds.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_golden.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_handoff.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_harness.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_history.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_history_persistence.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_hooks.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_in_memory_storage.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_llm_gemini_wire.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_llm_responses_wire.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_llm_stream_e2e.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_llm_wire_helpers.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_long_session.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_loop.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_loop_cancellation.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_loop_replay.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_loop_sandbox_event.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_loop_terminal.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_maintenance.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_model_equivalence.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_model_info.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_native_default.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_otel.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_parallel_tools.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_plugins.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_provider_compat.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_provider_presets.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_pseudo.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_pyo3_api_surface.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_recording.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_reminders.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_replay_crosslang.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_resume.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_retry_hooks.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_safety_gate.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_skills.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_soft_timeout.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_spill.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_sqlite_storage.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_steer.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_storage_contract.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_stream_strip.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_subagent.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_todo.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_todo_gate.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tokens.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tool_exposure.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tool_hygiene.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tool_router.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_tool_timeout.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_trace_recorder.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_transport_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_transport_sse.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_usage_attribution.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_world_state.py +0 -0
- {steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/tests/test_write_lease.py +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steerable-agent-runtime
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.53
|
|
4
4
|
Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: httpx[socks]>=0.27
|
|
9
|
+
Requires-Dist: mcp<2.0.0,>=1.13.0
|
|
8
10
|
Requires-Dist: pydantic>=2.10.0
|
|
9
11
|
Requires-Dist: steerable-agent-protocol<1.0.0,>=0.1.0
|
|
10
12
|
Requires-Dist: steerable-agent-harness<1.0.0,>=0.1.0
|
|
@@ -15,14 +17,12 @@ Provides-Extra: fastapi
|
|
|
15
17
|
Requires-Dist: fastapi>=0.110; extra == "fastapi"
|
|
16
18
|
Requires-Dist: starlette>=0.37; extra == "fastapi"
|
|
17
19
|
Provides-Extra: openai
|
|
18
|
-
Requires-Dist: httpx[socks]>=0.27; extra == "openai"
|
|
19
20
|
Provides-Extra: anthropic
|
|
20
21
|
Requires-Dist: anthropic>=0.40; extra == "anthropic"
|
|
21
22
|
Provides-Extra: all
|
|
22
23
|
Requires-Dist: sqlalchemy>=2.0; extra == "all"
|
|
23
24
|
Requires-Dist: fastapi>=0.110; extra == "all"
|
|
24
25
|
Requires-Dist: starlette>=0.37; extra == "all"
|
|
25
|
-
Requires-Dist: httpx[socks]>=0.27; extra == "all"
|
|
26
26
|
Requires-Dist: anthropic>=0.40; extra == "all"
|
|
27
27
|
|
|
28
28
|
# steerable-agent-runtime
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "steerable-agent-runtime"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.53"
|
|
4
4
|
description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
|
|
5
5
|
license = { text = "Apache-2.0" }
|
|
6
6
|
readme = "README.md"
|
|
@@ -13,6 +13,11 @@ requires-python = ">=3.10"
|
|
|
13
13
|
# CoreLoop execution is the Rust engine. The matching native wheel
|
|
14
14
|
# is a hard dependency: install or startup fails if it is missing.
|
|
15
15
|
dependencies = [
|
|
16
|
+
# MCP Streamable HTTP and OpenAI-compatible providers both construct httpx
|
|
17
|
+
# clients. Keep SOCKS support in the base install because exported proxy
|
|
18
|
+
# variables are evaluated during client construction.
|
|
19
|
+
"httpx[socks]>=0.27",
|
|
20
|
+
"mcp>=1.13.0,<2.0.0",
|
|
16
21
|
"pydantic>=2.10.0",
|
|
17
22
|
"steerable-agent-protocol>=0.1.0,<1.0.0",
|
|
18
23
|
"steerable-agent-harness>=0.1.0,<1.0.0",
|
|
@@ -22,17 +27,12 @@ dependencies = [
|
|
|
22
27
|
[project.optional-dependencies]
|
|
23
28
|
sqlalchemy = ["sqlalchemy>=2.0"]
|
|
24
29
|
fastapi = ["fastapi>=0.110", "starlette>=0.37"]
|
|
25
|
-
|
|
26
|
-
# every proxy the environment declares while the client is constructed, so a
|
|
27
|
-
# host exporting `all_proxy=socks5://…` — the common shell proxy snippet — makes
|
|
28
|
-
# construction raise without it, failing provider calls that never reach a URL.
|
|
29
|
-
openai = ["httpx[socks]>=0.27"]
|
|
30
|
+
openai = []
|
|
30
31
|
anthropic = ["anthropic>=0.40"]
|
|
31
32
|
all = [
|
|
32
33
|
"sqlalchemy>=2.0",
|
|
33
34
|
"fastapi>=0.110",
|
|
34
35
|
"starlette>=0.37",
|
|
35
|
-
"httpx[socks]>=0.27",
|
|
36
36
|
"anthropic>=0.40",
|
|
37
37
|
]
|
|
38
38
|
|
|
@@ -163,9 +163,12 @@ from .mcp import (
|
|
|
163
163
|
DEFAULT_MAX_SERVER_TOOLS,
|
|
164
164
|
McpCallResult,
|
|
165
165
|
McpCatalogError,
|
|
166
|
+
McpClient,
|
|
166
167
|
McpError,
|
|
168
|
+
McpStreamableHttpClient,
|
|
167
169
|
McpStdioClient,
|
|
168
170
|
McpToolInfo,
|
|
171
|
+
create_mcp_client,
|
|
169
172
|
mcp_invoker,
|
|
170
173
|
parse_mcp_name,
|
|
171
174
|
qualify_mcp_name,
|
|
@@ -374,7 +377,9 @@ __all__ = [
|
|
|
374
377
|
"LoopHooks",
|
|
375
378
|
"McpCallResult",
|
|
376
379
|
"McpCatalogError",
|
|
380
|
+
"McpClient",
|
|
377
381
|
"McpError",
|
|
382
|
+
"McpStreamableHttpClient",
|
|
378
383
|
"McpStdioClient",
|
|
379
384
|
"McpToolInfo",
|
|
380
385
|
"ModelCalibration",
|
|
@@ -472,6 +477,7 @@ __all__ = [
|
|
|
472
477
|
"make_todo_write_tool",
|
|
473
478
|
"parent_input_tool_descriptor",
|
|
474
479
|
"matches_conditions",
|
|
480
|
+
"create_mcp_client",
|
|
475
481
|
"mcp_invoker",
|
|
476
482
|
"merge_patch",
|
|
477
483
|
"message_from_dict",
|
|
@@ -53,6 +53,7 @@ __all__ = [
|
|
|
53
53
|
"ApprovalStore",
|
|
54
54
|
"Approver",
|
|
55
55
|
"AutoApprover",
|
|
56
|
+
"WorkspaceAutoApprover",
|
|
56
57
|
"InMemoryApprovalStore",
|
|
57
58
|
"JsonApprovalStore",
|
|
58
59
|
"SessionApprovalCache",
|
|
@@ -130,6 +131,91 @@ class Approver(Protocol):
|
|
|
130
131
|
async def approve(self, request: ApprovalRequest) -> ApprovalDecision: ...
|
|
131
132
|
|
|
132
133
|
|
|
134
|
+
_PATH_ARG_TOOLS = {
|
|
135
|
+
"local_read_file": "path",
|
|
136
|
+
"local_write_file": "path",
|
|
137
|
+
"local_edit_file": "path",
|
|
138
|
+
"view_image": "path",
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def declared_target_paths(request: ApprovalRequest) -> list[str] | None:
|
|
143
|
+
"""Filesystem targets of a path-scoped tool, or None when the call is not one.
|
|
144
|
+
|
|
145
|
+
``None`` means the approver cannot prove the call stays inside the
|
|
146
|
+
project, so the interactive prompt still runs. Shell commands stay in
|
|
147
|
+
that bucket: the command text can name paths the cwd does not.
|
|
148
|
+
"""
|
|
149
|
+
if request.tool_name == "present_files":
|
|
150
|
+
files = request.arguments.get("files")
|
|
151
|
+
if not isinstance(files, list) or not files:
|
|
152
|
+
return None
|
|
153
|
+
paths: list[str] = []
|
|
154
|
+
for item in files:
|
|
155
|
+
if not isinstance(item, dict):
|
|
156
|
+
return None
|
|
157
|
+
raw = item.get("path")
|
|
158
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
159
|
+
return None
|
|
160
|
+
paths.append(raw.strip())
|
|
161
|
+
return paths
|
|
162
|
+
key = _PATH_ARG_TOOLS.get(request.tool_name)
|
|
163
|
+
if key is None:
|
|
164
|
+
return None
|
|
165
|
+
raw = request.arguments.get(key)
|
|
166
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
167
|
+
return None
|
|
168
|
+
return [raw.strip()]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def path_inside_writable_roots(raw: str, roots: list[str]) -> bool:
|
|
172
|
+
"""True when ``raw`` is the configured directory or one of its subdirectories.
|
|
173
|
+
|
|
174
|
+
Relative paths are project-relative. A ``..`` segment can leave the
|
|
175
|
+
project, so those still go to the prompt. Absolute paths must sit under
|
|
176
|
+
one of ``roots``.
|
|
177
|
+
"""
|
|
178
|
+
expanded = os.path.expanduser(raw.strip())
|
|
179
|
+
if not os.path.isabs(expanded):
|
|
180
|
+
normalized = os.path.normpath(expanded)
|
|
181
|
+
return normalized != ".." and not normalized.startswith(".." + os.sep)
|
|
182
|
+
target = os.path.abspath(expanded)
|
|
183
|
+
for root in roots:
|
|
184
|
+
if not isinstance(root, str) or not root.strip():
|
|
185
|
+
continue
|
|
186
|
+
root_abs = os.path.abspath(os.path.expanduser(root.strip()))
|
|
187
|
+
try:
|
|
188
|
+
if os.path.commonpath([target, root_abs]) == root_abs:
|
|
189
|
+
return True
|
|
190
|
+
except ValueError:
|
|
191
|
+
continue
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class WorkspaceAutoApprover:
|
|
196
|
+
"""Skip the host prompt when every declared path is already inside a writable root.
|
|
197
|
+
|
|
198
|
+
The allow is request-scoped. The next call is judged again, so an
|
|
199
|
+
in-project ``present_files`` does not grant the same tool outside the
|
|
200
|
+
project. Calls with no path we can check, or any path outside the roots,
|
|
201
|
+
fall through to the inner approver.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self, inner: Approver, writable_roots: list[str]) -> None:
|
|
205
|
+
self._inner = inner
|
|
206
|
+
self._roots = [root for root in writable_roots if isinstance(root, str) and root.strip()]
|
|
207
|
+
|
|
208
|
+
async def approve(self, request: ApprovalRequest) -> ApprovalDecision:
|
|
209
|
+
paths = declared_target_paths(request)
|
|
210
|
+
if (
|
|
211
|
+
self._roots
|
|
212
|
+
and paths is not None
|
|
213
|
+
and all(path_inside_writable_roots(path, self._roots) for path in paths)
|
|
214
|
+
):
|
|
215
|
+
return ApprovalDecision("allow_once", "path is inside a writable project root")
|
|
216
|
+
return await self._inner.approve(request)
|
|
217
|
+
|
|
218
|
+
|
|
133
219
|
class AutoApprover:
|
|
134
220
|
"""Headless approver: never blocks, decides by tool mode.
|
|
135
221
|
|
|
@@ -22,7 +22,9 @@ memory: stateless
|
|
|
22
22
|
orchestration: single
|
|
23
23
|
# Loop limits the harness pins (W3.4.2.4): entrypoints read these instead of
|
|
24
24
|
# hardcoding knobs. Absent fields fall back to the entrypoint's baseline.
|
|
25
|
+
# max_rounds and max_tool_errors are absent on purpose — a cap here ends a
|
|
26
|
+
# long task at budget_exhausted / consecutive tool errors. The baseline is
|
|
27
|
+
# no cap. A caller that wants a wall passes maxRounds / maxToolErrors, and
|
|
28
|
+
# Harbor passes --max-rounds 250.
|
|
25
29
|
loop:
|
|
26
|
-
max_rounds: 80
|
|
27
|
-
max_tool_errors: 16
|
|
28
30
|
tool_dedup: false
|
{steerable_agent_runtime-0.6.51 → steerable_agent_runtime-0.6.53}/src/steerable_agent_runtime/mcp.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""MCP client seam: qualified names, capped catalogs,
|
|
1
|
+
"""MCP client seam: qualified names, capped catalogs, transport clients.
|
|
2
2
|
|
|
3
3
|
MCP is the largest unbounded, third-party, mutable source of model-visible
|
|
4
4
|
context an agent system can add, so it lands on the Wave 2 foundation:
|
|
@@ -22,9 +22,9 @@ caps.
|
|
|
22
22
|
Architecture (recorded in docs/roadmap.md): in the desktop product, MCP
|
|
23
23
|
servers are launched host-side (Electron main) and reached through
|
|
24
24
|
`ToolRouter.register_remote` — a Seatbelt-confined sidecar never spawns
|
|
25
|
-
them. `McpStdioClient`
|
|
26
|
-
(CLI agents, tests, non-Electron hosts);
|
|
27
|
-
loop state.
|
|
25
|
+
them. `McpStdioClient` and `McpStreamableHttpClient` exist for hosts that
|
|
26
|
+
embed the runtime directly (CLI agents, tests, non-Electron hosts); both
|
|
27
|
+
are transport-only and hold no loop state.
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
30
|
from __future__ import annotations
|
|
@@ -34,8 +34,15 @@ import json
|
|
|
34
34
|
import logging
|
|
35
35
|
import os
|
|
36
36
|
from collections.abc import Awaitable, Callable, Mapping, Sequence
|
|
37
|
+
from contextlib import AsyncExitStack
|
|
37
38
|
from dataclasses import dataclass, field
|
|
38
|
-
from
|
|
39
|
+
from datetime import timedelta
|
|
40
|
+
from typing import Any, Protocol
|
|
41
|
+
from urllib.parse import urlparse
|
|
42
|
+
|
|
43
|
+
import httpx
|
|
44
|
+
from mcp import ClientSession
|
|
45
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
39
46
|
|
|
40
47
|
from .errors import RuntimeError as SteerableRuntimeError
|
|
41
48
|
from .tools import RegisteredTool, ToolExposure, ToolRouter
|
|
@@ -126,6 +133,26 @@ class McpCallResult:
|
|
|
126
133
|
structured: Any = None
|
|
127
134
|
|
|
128
135
|
|
|
136
|
+
class McpClient(Protocol):
|
|
137
|
+
"""Transport-independent client operations consumed by MCP mounting paths."""
|
|
138
|
+
|
|
139
|
+
server_info: dict[str, Any]
|
|
140
|
+
|
|
141
|
+
async def __aenter__(self) -> McpClient: ...
|
|
142
|
+
|
|
143
|
+
async def __aexit__(self, *exc: Any) -> None: ...
|
|
144
|
+
|
|
145
|
+
async def start(self) -> None: ...
|
|
146
|
+
|
|
147
|
+
async def aclose(self) -> None: ...
|
|
148
|
+
|
|
149
|
+
async def list_tools(self) -> list[McpToolInfo]: ...
|
|
150
|
+
|
|
151
|
+
async def call_tool(
|
|
152
|
+
self, name: str, arguments: dict[str, Any]
|
|
153
|
+
) -> McpCallResult: ...
|
|
154
|
+
|
|
155
|
+
|
|
129
156
|
def register_mcp_catalog(
|
|
130
157
|
router: ToolRouter,
|
|
131
158
|
*,
|
|
@@ -178,9 +205,9 @@ def register_mcp_catalog(
|
|
|
178
205
|
|
|
179
206
|
|
|
180
207
|
def mcp_invoker(
|
|
181
|
-
client:
|
|
208
|
+
client: McpClient,
|
|
182
209
|
) -> Callable[[str, dict[str, Any]], Awaitable[Any]]:
|
|
183
|
-
"""Adapt `
|
|
210
|
+
"""Adapt an MCP client's `call_tool` to the catalog invoker contract.
|
|
184
211
|
|
|
185
212
|
Maps ``isError`` results to failed `ToolResult`s (follow-up allowed —
|
|
186
213
|
the model sees the server's error text and can correct its arguments)
|
|
@@ -199,6 +226,214 @@ def mcp_invoker(
|
|
|
199
226
|
return _invoke
|
|
200
227
|
|
|
201
228
|
|
|
229
|
+
def create_mcp_client(
|
|
230
|
+
config: Mapping[str, Any],
|
|
231
|
+
*,
|
|
232
|
+
environ: Mapping[str, str] | None = None,
|
|
233
|
+
request_timeout: float = 30.0,
|
|
234
|
+
) -> McpClient:
|
|
235
|
+
"""Build a client from an explicit or legacy-discriminated config entry.
|
|
236
|
+
|
|
237
|
+
``transport`` accepts ``stdio`` and ``streamable-http``. For existing
|
|
238
|
+
callers, an omitted discriminator is inferred from exactly one of
|
|
239
|
+
``command`` or ``url``. Environment-sourced header values are resolved
|
|
240
|
+
only into the client and are redacted from transport failures.
|
|
241
|
+
"""
|
|
242
|
+
command = config.get("command")
|
|
243
|
+
url = config.get("url")
|
|
244
|
+
if command and url:
|
|
245
|
+
raise McpError("MCP config cannot contain both 'command' and 'url'")
|
|
246
|
+
|
|
247
|
+
transport = config.get("transport")
|
|
248
|
+
if transport is None:
|
|
249
|
+
if command:
|
|
250
|
+
transport = "stdio"
|
|
251
|
+
elif url:
|
|
252
|
+
transport = "streamable-http"
|
|
253
|
+
else:
|
|
254
|
+
raise McpError("MCP config requires a non-empty 'command' or 'url'")
|
|
255
|
+
|
|
256
|
+
if transport == "stdio":
|
|
257
|
+
if not command:
|
|
258
|
+
raise McpError("stdio MCP config requires a non-empty 'command'")
|
|
259
|
+
if url:
|
|
260
|
+
raise McpError("stdio MCP config cannot contain 'url'")
|
|
261
|
+
raw_env = config.get("env") or {}
|
|
262
|
+
if not isinstance(raw_env, Mapping):
|
|
263
|
+
raise McpError("stdio MCP config 'env' must be an object")
|
|
264
|
+
return McpStdioClient(
|
|
265
|
+
str(command),
|
|
266
|
+
[str(arg) for arg in config.get("args") or []],
|
|
267
|
+
env={str(key): str(value) for key, value in raw_env.items()} or None,
|
|
268
|
+
request_timeout=request_timeout,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if transport != "streamable-http":
|
|
272
|
+
raise McpError(f"unsupported MCP transport: {transport!r}")
|
|
273
|
+
if not url:
|
|
274
|
+
raise McpError("streamable-http MCP config requires a non-empty 'url'")
|
|
275
|
+
if command:
|
|
276
|
+
raise McpError("streamable-http MCP config cannot contain 'command'")
|
|
277
|
+
parsed = urlparse(str(url))
|
|
278
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
279
|
+
raise McpError("streamable-http MCP config 'url' must be an HTTP(S) URL")
|
|
280
|
+
if parsed.username or parsed.password:
|
|
281
|
+
raise McpError("streamable-http MCP config 'url' cannot contain credentials")
|
|
282
|
+
|
|
283
|
+
raw_headers = config.get("headers") or {}
|
|
284
|
+
raw_headers_from_env = config.get("headersFromEnv") or {}
|
|
285
|
+
if not isinstance(raw_headers, Mapping):
|
|
286
|
+
raise McpError("streamable-http MCP config 'headers' must be an object")
|
|
287
|
+
if not isinstance(raw_headers_from_env, Mapping):
|
|
288
|
+
raise McpError(
|
|
289
|
+
"streamable-http MCP config 'headersFromEnv' must be an object"
|
|
290
|
+
)
|
|
291
|
+
source = os.environ if environ is None else environ
|
|
292
|
+
headers = {str(name): str(value) for name, value in raw_headers.items()}
|
|
293
|
+
secret_values = [value for value in headers.values() if value]
|
|
294
|
+
for raw_name, raw_env_name in raw_headers_from_env.items():
|
|
295
|
+
env_name = str(raw_env_name)
|
|
296
|
+
value = source.get(env_name)
|
|
297
|
+
if not value:
|
|
298
|
+
raise McpError(
|
|
299
|
+
f"streamable-http MCP header environment variable "
|
|
300
|
+
f"{env_name!r} is not set"
|
|
301
|
+
)
|
|
302
|
+
headers[str(raw_name)] = value
|
|
303
|
+
secret_values.append(value)
|
|
304
|
+
bearer_env = config.get("bearerTokenEnvVar")
|
|
305
|
+
if bearer_env is not None:
|
|
306
|
+
env_name = str(bearer_env)
|
|
307
|
+
value = source.get(env_name)
|
|
308
|
+
if not value:
|
|
309
|
+
raise McpError(
|
|
310
|
+
f"streamable-http MCP bearer environment variable "
|
|
311
|
+
f"{env_name!r} is not set"
|
|
312
|
+
)
|
|
313
|
+
headers["Authorization"] = f"Bearer {value}"
|
|
314
|
+
secret_values.append(value)
|
|
315
|
+
|
|
316
|
+
return McpStreamableHttpClient(
|
|
317
|
+
str(url),
|
|
318
|
+
headers=headers,
|
|
319
|
+
secret_values=secret_values,
|
|
320
|
+
request_timeout=request_timeout,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class McpStreamableHttpClient:
|
|
325
|
+
"""MCP Streamable HTTP client backed by the official Python MCP SDK."""
|
|
326
|
+
|
|
327
|
+
def __init__(
|
|
328
|
+
self,
|
|
329
|
+
url: str,
|
|
330
|
+
*,
|
|
331
|
+
headers: Mapping[str, str] | None = None,
|
|
332
|
+
secret_values: Sequence[str] = (),
|
|
333
|
+
request_timeout: float = 30.0,
|
|
334
|
+
) -> None:
|
|
335
|
+
self._url = url
|
|
336
|
+
self._headers = dict(headers or {})
|
|
337
|
+
self._secret_values = tuple(value for value in secret_values if value)
|
|
338
|
+
self._request_timeout = request_timeout
|
|
339
|
+
self._stack: AsyncExitStack | None = None
|
|
340
|
+
self._session: ClientSession | None = None
|
|
341
|
+
self.server_info: dict[str, Any] = {}
|
|
342
|
+
|
|
343
|
+
async def __aenter__(self) -> McpStreamableHttpClient:
|
|
344
|
+
await self.start()
|
|
345
|
+
return self
|
|
346
|
+
|
|
347
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
348
|
+
await self.aclose()
|
|
349
|
+
|
|
350
|
+
async def start(self) -> None:
|
|
351
|
+
if self._stack is not None:
|
|
352
|
+
raise McpError("McpStreamableHttpClient already started")
|
|
353
|
+
stack = AsyncExitStack()
|
|
354
|
+
self._stack = stack
|
|
355
|
+
try:
|
|
356
|
+
http_client = await stack.enter_async_context(
|
|
357
|
+
httpx.AsyncClient(headers=self._headers)
|
|
358
|
+
)
|
|
359
|
+
read_stream, write_stream, _session_id = await stack.enter_async_context(
|
|
360
|
+
streamable_http_client(self._url, http_client=http_client)
|
|
361
|
+
)
|
|
362
|
+
session = await stack.enter_async_context(
|
|
363
|
+
ClientSession(
|
|
364
|
+
read_stream,
|
|
365
|
+
write_stream,
|
|
366
|
+
read_timeout_seconds=timedelta(seconds=self._request_timeout),
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
initialized = await session.initialize()
|
|
370
|
+
self._session = session
|
|
371
|
+
self.server_info = initialized.serverInfo.model_dump(
|
|
372
|
+
by_alias=True, exclude_none=True
|
|
373
|
+
)
|
|
374
|
+
except Exception as exc:
|
|
375
|
+
await self.aclose()
|
|
376
|
+
raise self._error("initialize", exc) from exc
|
|
377
|
+
|
|
378
|
+
async def aclose(self) -> None:
|
|
379
|
+
stack, self._stack = self._stack, None
|
|
380
|
+
self._session = None
|
|
381
|
+
if stack is not None:
|
|
382
|
+
await stack.aclose()
|
|
383
|
+
|
|
384
|
+
async def list_tools(self) -> list[McpToolInfo]:
|
|
385
|
+
session = self._require_session()
|
|
386
|
+
tools: list[McpToolInfo] = []
|
|
387
|
+
cursor: str | None = None
|
|
388
|
+
try:
|
|
389
|
+
while True:
|
|
390
|
+
result = await session.list_tools(cursor=cursor)
|
|
391
|
+
tools.extend(
|
|
392
|
+
McpToolInfo(
|
|
393
|
+
name=tool.name,
|
|
394
|
+
description=tool.description or "",
|
|
395
|
+
schema=dict(tool.inputSchema),
|
|
396
|
+
)
|
|
397
|
+
for tool in result.tools
|
|
398
|
+
)
|
|
399
|
+
cursor = result.nextCursor
|
|
400
|
+
if cursor is None:
|
|
401
|
+
return tools
|
|
402
|
+
except Exception as exc:
|
|
403
|
+
raise self._error("tools/list", exc) from exc
|
|
404
|
+
|
|
405
|
+
async def call_tool(
|
|
406
|
+
self, name: str, arguments: dict[str, Any]
|
|
407
|
+
) -> McpCallResult:
|
|
408
|
+
session = self._require_session()
|
|
409
|
+
try:
|
|
410
|
+
result = await session.call_tool(name, arguments)
|
|
411
|
+
except Exception as exc:
|
|
412
|
+
raise self._error("tools/call", exc) from exc
|
|
413
|
+
parts: list[str] = []
|
|
414
|
+
for content in result.content:
|
|
415
|
+
if content.type == "text":
|
|
416
|
+
parts.append(content.text)
|
|
417
|
+
else:
|
|
418
|
+
parts.append(f"[mcp {content.type} content]")
|
|
419
|
+
return McpCallResult(
|
|
420
|
+
text="\n".join(parts),
|
|
421
|
+
is_error=result.isError,
|
|
422
|
+
structured=result.structuredContent,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def _require_session(self) -> ClientSession:
|
|
426
|
+
if self._session is None:
|
|
427
|
+
raise McpError("MCP Streamable HTTP client is not running")
|
|
428
|
+
return self._session
|
|
429
|
+
|
|
430
|
+
def _error(self, operation: str, exc: Exception) -> McpError:
|
|
431
|
+
message = str(exc)
|
|
432
|
+
for secret in self._secret_values:
|
|
433
|
+
message = message.replace(secret, "[redacted]")
|
|
434
|
+
return McpError(f"MCP Streamable HTTP {operation} failed: {message}")
|
|
435
|
+
|
|
436
|
+
|
|
202
437
|
# ---------------------------------------------------------------------------
|
|
203
438
|
# Minimal stdio client (NDJSON JSON-RPC 2.0)
|
|
204
439
|
# ---------------------------------------------------------------------------
|
|
@@ -38,6 +38,7 @@ from typing import Any
|
|
|
38
38
|
|
|
39
39
|
from steerable_agent_protocol.generated import ToolCall, ToolResult
|
|
40
40
|
|
|
41
|
+
from .hooks import ChainHooks, CompletionAction, CompletionDraft, NoopHooks
|
|
41
42
|
from .llm import LLMProvider
|
|
42
43
|
from .loop import CoreLoop, LoopConfig, LoopContext, LoopHooks, ToolExecutor
|
|
43
44
|
from .pool import (
|
|
@@ -51,12 +52,61 @@ from .subagent import FilteredToolsExecutor, _schema_name
|
|
|
51
52
|
__all__ = [
|
|
52
53
|
"AgentPool",
|
|
53
54
|
"ChildOutcome",
|
|
55
|
+
"ChildBudgetWrapUp",
|
|
54
56
|
"OrchestrationBudgetExceeded",
|
|
55
57
|
"OrchestrationConfig",
|
|
56
58
|
"OrchestrationExecutor",
|
|
57
59
|
"orchestration_tool_descriptors",
|
|
58
60
|
]
|
|
59
61
|
|
|
62
|
+
#: Terminal status of a run stopped by ``max_rounds`` or the token budget.
|
|
63
|
+
_BUDGET_WALL_STATUS = "budget_exhausted"
|
|
64
|
+
|
|
65
|
+
_BUDGET_WRAP_UP_REQUEST = (
|
|
66
|
+
"[system notice] This run has used its round budget and tools are now "
|
|
67
|
+
"withheld. Answer the original task now from what you have already "
|
|
68
|
+
"gathered: give your concrete findings first, then list what you could "
|
|
69
|
+
"not finish or verify. Do not describe the steps you took."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
_RESUME_HINT = (
|
|
73
|
+
"The child stopped at its round budget; `answer` is its interim report. "
|
|
74
|
+
"To continue, agent_send this childId a message naming what is still "
|
|
75
|
+
"missing — it resumes with its prior context and a fresh budget. Do not "
|
|
76
|
+
"spawn a new child for the same task."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ChildBudgetWrapUp(NoopHooks):
|
|
81
|
+
"""``before_completion``: one no-tools answer round at a child's budget wall.
|
|
82
|
+
|
|
83
|
+
Without it a child stopped by ``max_rounds`` or the token budget returns
|
|
84
|
+
whatever text its last working round produced — usually a progress note,
|
|
85
|
+
not an answer. The parent then gets ``budget_exhausted`` with nothing
|
|
86
|
+
usable. The ``narrate`` action runs one round without tools and keeps the
|
|
87
|
+
``budget_exhausted`` status, so the parent can still tell the answer is
|
|
88
|
+
interim and resume the child with ``agent_send``.
|
|
89
|
+
|
|
90
|
+
One instance per child loop. A resumed child runs the same loop with a new
|
|
91
|
+
``LoopContext``, so the one-round limit applies per run.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self) -> None:
|
|
95
|
+
self._wrapped_run: LoopContext | None = None
|
|
96
|
+
|
|
97
|
+
async def before_completion(
|
|
98
|
+
self, draft: CompletionDraft, ctx: LoopContext
|
|
99
|
+
) -> CompletionAction:
|
|
100
|
+
"""Narrate once per run when the draft stops at the budget wall."""
|
|
101
|
+
if draft.status != _BUDGET_WALL_STATUS or self._wrapped_run is ctx:
|
|
102
|
+
return CompletionAction(kind="accept")
|
|
103
|
+
self._wrapped_run = ctx
|
|
104
|
+
return CompletionAction(
|
|
105
|
+
kind="narrate",
|
|
106
|
+
message=_BUDGET_WRAP_UP_REQUEST,
|
|
107
|
+
reason="child_budget_wrap_up",
|
|
108
|
+
)
|
|
109
|
+
|
|
60
110
|
|
|
61
111
|
def orchestration_tool_descriptors(
|
|
62
112
|
config: OrchestrationConfig | None = None,
|
|
@@ -119,7 +169,10 @@ def orchestration_tool_descriptors(
|
|
|
119
169
|
fn(
|
|
120
170
|
config.wait_tool,
|
|
121
171
|
"Wait for a sub-agent to finish and return its outcome. A child "
|
|
122
|
-
"still running at the timeout returns status 'running'."
|
|
172
|
+
"still running at the timeout returns status 'running'. A child "
|
|
173
|
+
"that hit its round budget returns status 'budget_exhausted' with "
|
|
174
|
+
"an interim answer and resumable: true — continue it with "
|
|
175
|
+
"agent_send instead of spawning a new child.",
|
|
123
176
|
{
|
|
124
177
|
"childId": {"type": "string"},
|
|
125
178
|
"timeoutMs": {"type": "integer"},
|
|
@@ -228,11 +281,15 @@ class OrchestrationExecutor:
|
|
|
228
281
|
_lineage=child_id,
|
|
229
282
|
)
|
|
230
283
|
schemas = [*schemas, *orchestration_tool_descriptors(self._config)]
|
|
284
|
+
wrap_up = ChildBudgetWrapUp()
|
|
231
285
|
loop = CoreLoop(
|
|
232
286
|
self._provider,
|
|
233
287
|
base,
|
|
234
|
-
LoopConfig(
|
|
235
|
-
|
|
288
|
+
LoopConfig(
|
|
289
|
+
max_rounds=self._config.child_max_rounds,
|
|
290
|
+
max_tool_errors=self._config.child_max_tool_errors,
|
|
291
|
+
),
|
|
292
|
+
hooks=wrap_up if self._hooks is None else ChainHooks(self._hooks, wrap_up),
|
|
236
293
|
)
|
|
237
294
|
return loop, schemas or None
|
|
238
295
|
|
|
@@ -343,15 +400,17 @@ class OrchestrationExecutor:
|
|
|
343
400
|
error=f"unknown_child: {child_id!r}",
|
|
344
401
|
needsFollowup=True,
|
|
345
402
|
)
|
|
403
|
+
payload: dict[str, Any] = {
|
|
404
|
+
"childId": outcome.child_id,
|
|
405
|
+
"status": outcome.status,
|
|
406
|
+
"answer": outcome.answer,
|
|
407
|
+
}
|
|
408
|
+
if self._pool.resumable(outcome.child_id):
|
|
409
|
+
payload["resumable"] = True
|
|
410
|
+
payload["hint"] = _RESUME_HINT
|
|
346
411
|
return ToolResult(
|
|
347
412
|
success=True,
|
|
348
|
-
message=json.dumps(
|
|
349
|
-
{
|
|
350
|
-
"childId": outcome.child_id,
|
|
351
|
-
"status": outcome.status,
|
|
352
|
-
"answer": outcome.answer,
|
|
353
|
-
}
|
|
354
|
-
),
|
|
413
|
+
message=json.dumps(payload),
|
|
355
414
|
data={"childId": outcome.child_id, "status": outcome.status},
|
|
356
415
|
)
|
|
357
416
|
|