steerable-agent-runtime 0.6.28__tar.gz → 0.6.30__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.28 → steerable_agent_runtime-0.6.30}/PKG-INFO +9 -1
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/README.md +8 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/pyproject.toml +1 -1
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/__init__.py +2 -0
- steerable_agent_runtime-0.6.30/src/steerable_agent_runtime/delegation_gate.py +100 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/loop.py +16 -5
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/native_bridge.py +206 -78
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/orchestration.py +4 -4
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/pool.py +3 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/subagent.py +34 -5
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime.egg-info/PKG-INFO +9 -1
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime.egg-info/SOURCES.txt +3 -0
- steerable_agent_runtime-0.6.30/tests/test_delegation_gate.py +108 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_loop.py +129 -1
- steerable_agent_runtime-0.6.30/tests/test_native_default.py +44 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_soft_timeout.py +73 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_subagent.py +74 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/setup.cfg +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/antihallucination.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/approval.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/ask_user.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/branch.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/cache_control.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/calibration.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/compaction.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/config.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/default.harness.json +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/default.harness.yaml +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/errors.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/handoff.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/harness.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/history.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/hooks.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/__init__.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/compat.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/errors.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/parts.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/presets.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/maintenance.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/mcp.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/model_info.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/otel.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/plugins.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/pricing.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/pseudo.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/recording.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/reminders.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/replay.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/resume.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/retry.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/skills.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/spill.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/storage/__init__.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/todo.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/tokens.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/tool_search.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/tools.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/tracing.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/transport/__init__.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime/world_state.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_antihallucination.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_approval.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_ask_user.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_branch.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_cache_control.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_cache_instrumentation.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_calibration.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_compaction.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_compaction_strategy.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_config.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_content_parts.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_error_taxonomy.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_fragment_bounds.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_golden.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_handoff.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_harness.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_history.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_history_persistence.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_hooks.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_in_memory_storage.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_llm_gemini_wire.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_llm_responses_wire.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_llm_stream_e2e.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_llm_wire_helpers.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_long_session.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_loop_cancellation.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_loop_replay.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_loop_sandbox_event.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_loop_terminal.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_maintenance.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_mcp.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_model_equivalence.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_model_info.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_orchestration.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_otel.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_parallel_tools.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_plugins.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_provider_compat.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_provider_presets.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_pseudo.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_pyo3_api_surface.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_recording.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_reminders.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_replay_crosslang.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_resume.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_retry_hooks.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_safety_gate.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_skills.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_spill.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_sqlite_storage.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_steer.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_storage_contract.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_stream_strip.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_todo.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_todo_gate.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tokens.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tool_exposure.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tool_hygiene.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tool_router.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_tool_timeout.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_trace_recorder.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_transport_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_transport_sse.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_usage_attribution.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_world_state.py +0 -0
- {steerable_agent_runtime-0.6.28 → steerable_agent_runtime-0.6.30}/tests/test_write_lease.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: steerable-agent-runtime
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.30
|
|
4
4
|
Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -59,3 +59,11 @@ pip install "steerable-agent-runtime[anthropic]"
|
|
|
59
59
|
pip install "steerable-agent-runtime[sqlalchemy]"
|
|
60
60
|
pip install "steerable-agent-runtime[fastapi]"
|
|
61
61
|
```
|
|
62
|
+
|
|
63
|
+
The CoreLoop defaults to the Rust binding when
|
|
64
|
+
`steerable-agent-runtime-native` is importable (`STEERABLE_RUST_CORELOOP=0`
|
|
65
|
+
keeps the Python loop):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install steerable-agent-runtime-native
|
|
69
|
+
```
|
|
@@ -34,3 +34,11 @@ pip install "steerable-agent-runtime[anthropic]"
|
|
|
34
34
|
pip install "steerable-agent-runtime[sqlalchemy]"
|
|
35
35
|
pip install "steerable-agent-runtime[fastapi]"
|
|
36
36
|
```
|
|
37
|
+
|
|
38
|
+
The CoreLoop defaults to the Rust binding when
|
|
39
|
+
`steerable-agent-runtime-native` is importable (`STEERABLE_RUST_CORELOOP=0`
|
|
40
|
+
keeps the Python loop):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install steerable-agent-runtime-native
|
|
44
|
+
```
|
|
@@ -51,6 +51,7 @@ from .todo import (
|
|
|
51
51
|
make_todo_write_tool,
|
|
52
52
|
todo_write_tool_descriptor,
|
|
53
53
|
)
|
|
54
|
+
from .delegation_gate import RequiredDelegationGate
|
|
54
55
|
from .config import (
|
|
55
56
|
DEFAULT_CONFIG_PATH,
|
|
56
57
|
ConfigError,
|
|
@@ -387,6 +388,7 @@ __all__ = [
|
|
|
387
388
|
"RecordingProvider",
|
|
388
389
|
"RegisteredTool",
|
|
389
390
|
"RequestSink",
|
|
391
|
+
"RequiredDelegationGate",
|
|
390
392
|
"RetryAction",
|
|
391
393
|
"RetryHooks",
|
|
392
394
|
"RewriteRequest",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Required-delegation completion gate.
|
|
2
|
+
|
|
3
|
+
A host that names sub-agents for a turn (the desktop's ``@`` mentions) can
|
|
4
|
+
only *ask* the model to delegate: the dispatch instruction is prompt text,
|
|
5
|
+
and every existing discipline guard lets a narrated hand-off through. The
|
|
6
|
+
claimed-execution check in ``antihallucination`` fires only on a round with
|
|
7
|
+
no tool calls at all, so a turn that ran unrelated tools and then wrote "I
|
|
8
|
+
launched the three agents" ends ``completed`` with zero delegations.
|
|
9
|
+
|
|
10
|
+
This gate closes that hole: the host declares which profiles must receive a
|
|
11
|
+
delegation, the gate watches ``delegate_subagent`` calls, and a ``completed``
|
|
12
|
+
draft that skipped one is converted into a retry naming the missing
|
|
13
|
+
profiles.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from collections.abc import Iterable
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from steerable_agent_protocol.generated import ToolCall, ToolResult
|
|
22
|
+
|
|
23
|
+
from .hooks import CompletionAction, CompletionDraft, NoopHooks
|
|
24
|
+
|
|
25
|
+
#: Retries this gate grants before accepting a turn that skipped a profile.
|
|
26
|
+
#: The loop's own completion-redo budget bounds the total; this keeps one
|
|
27
|
+
#: uncooperative model from spending all of it on delegation alone.
|
|
28
|
+
DEFAULT_MAX_RETRIES = 2
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class RequiredDelegationGate(NoopHooks):
|
|
32
|
+
"""``before_completion`` veto: every required profile must be delegated.
|
|
33
|
+
|
|
34
|
+
A profile counts as satisfied once a delegation was *attempted*, whether
|
|
35
|
+
the child succeeded or not — a child that failed (its own tool error, a
|
|
36
|
+
budget wall) is the parent's to report, and re-delegating to it would
|
|
37
|
+
spin. Only a profile the model never called at all blocks finishing.
|
|
38
|
+
|
|
39
|
+
One instance per run: it accumulates the turn's delegations.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
required_profiles: Iterable[str],
|
|
45
|
+
*,
|
|
46
|
+
tool_name: str = "delegate_subagent",
|
|
47
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""
|
|
50
|
+
@param required_profiles: profile names that must each receive a
|
|
51
|
+
delegation; duplicates and blanks are dropped.
|
|
52
|
+
@param tool_name: the delegation tool to watch.
|
|
53
|
+
@param max_retries: retries granted before accepting a skip.
|
|
54
|
+
"""
|
|
55
|
+
self._required = tuple(
|
|
56
|
+
dict.fromkeys(name.strip() for name in required_profiles if name.strip())
|
|
57
|
+
)
|
|
58
|
+
self._tool_name = tool_name
|
|
59
|
+
self._max_retries = max_retries
|
|
60
|
+
self._delegated: set[str] = set()
|
|
61
|
+
self._retries = 0
|
|
62
|
+
|
|
63
|
+
async def post_tool_result(
|
|
64
|
+
self, result: ToolResult, call: ToolCall, ctx: Any
|
|
65
|
+
) -> ToolResult:
|
|
66
|
+
"""Record the delegated profile, passing the result through."""
|
|
67
|
+
if call.name == self._tool_name:
|
|
68
|
+
profile = str(call.arguments.get("subagent_type") or "").strip()
|
|
69
|
+
if profile:
|
|
70
|
+
self._delegated.add(profile)
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
async def before_completion(
|
|
74
|
+
self, draft: CompletionDraft, ctx: Any
|
|
75
|
+
) -> CompletionAction:
|
|
76
|
+
"""Retry while a required profile has no delegation, else accept.
|
|
77
|
+
|
|
78
|
+
Only ``completed`` is gated: ``budget_exhausted`` and error stops have
|
|
79
|
+
their own continuation paths (the host's auto-continue resumes with a
|
|
80
|
+
fresh budget), and vetoing them here would fight those.
|
|
81
|
+
"""
|
|
82
|
+
if draft.status != "completed":
|
|
83
|
+
return CompletionAction(kind="accept")
|
|
84
|
+
missing = [name for name in self._required if name not in self._delegated]
|
|
85
|
+
if not missing or self._retries >= self._max_retries:
|
|
86
|
+
return CompletionAction(kind="accept")
|
|
87
|
+
self._retries += 1
|
|
88
|
+
listing = "、".join(missing)
|
|
89
|
+
return CompletionAction(
|
|
90
|
+
kind="retry",
|
|
91
|
+
message=(
|
|
92
|
+
f"用户点名的子代理还没拿到任务:{listing}。"
|
|
93
|
+
"现在就用 delegate_subagent 给每一个补上一份自包含的子任务"
|
|
94
|
+
"(subagent_type 填画像名),不要用文字描述代替调用。"
|
|
95
|
+
),
|
|
96
|
+
reason="required_delegation_missing",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = ["DEFAULT_MAX_RETRIES", "RequiredDelegationGate"]
|
|
@@ -545,6 +545,16 @@ class CoreLoop:
|
|
|
545
545
|
"""
|
|
546
546
|
self._compact_event.set()
|
|
547
547
|
|
|
548
|
+
@property
|
|
549
|
+
def record_id(self) -> str | None:
|
|
550
|
+
"""Durable-record id this loop appends to, when one was configured.
|
|
551
|
+
|
|
552
|
+
Hosts that want to read a run's transcript back (a pooled child's
|
|
553
|
+
process view) need the id the loop writes under, not the one they
|
|
554
|
+
guessed.
|
|
555
|
+
"""
|
|
556
|
+
return self._record_id
|
|
557
|
+
|
|
548
558
|
@property
|
|
549
559
|
def last_run_usage(self) -> LLMUsage | None:
|
|
550
560
|
"""Accumulated billable usage of the current/last run (W6-9).
|
|
@@ -848,12 +858,13 @@ class CoreLoop:
|
|
|
848
858
|
tools: list[dict[str, Any]] | None = None,
|
|
849
859
|
chat_id: str | None = None,
|
|
850
860
|
) -> AsyncIterator[LoopEvent]:
|
|
851
|
-
if os.environ.get("STEERABLE_RUST_CORELOOP")
|
|
852
|
-
from .native_bridge import run_native
|
|
861
|
+
if os.environ.get("STEERABLE_RUST_CORELOOP") != "0":
|
|
862
|
+
from .native_bridge import run_native, uses_native_coreloop
|
|
853
863
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
864
|
+
if uses_native_coreloop():
|
|
865
|
+
async for event in run_native(self, messages, tools=tools, chat_id=chat_id):
|
|
866
|
+
yield event
|
|
867
|
+
return
|
|
857
868
|
ctx = LoopContext(chat_id=chat_id)
|
|
858
869
|
self._run_context = ctx
|
|
859
870
|
# The transcript is a projection of the append-only record; nothing
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Drive the Rust CoreLoop from Python via `steerable_agent_runtime_native`.
|
|
2
2
|
|
|
3
3
|
Python keeps the provider, executor, and hooks (async, GIL-friendly). Rust
|
|
4
|
-
owns think → act → observe.
|
|
4
|
+
owns think → act → observe. The Rust binding is the default when the
|
|
5
|
+
module is importable; ``STEERABLE_RUST_CORELOOP=0`` forces Python and
|
|
6
|
+
``=1`` still forces Rust.
|
|
5
7
|
"""
|
|
6
8
|
|
|
7
9
|
from __future__ import annotations
|
|
@@ -9,6 +11,7 @@ from __future__ import annotations
|
|
|
9
11
|
import asyncio
|
|
10
12
|
import json
|
|
11
13
|
import logging
|
|
14
|
+
import os
|
|
12
15
|
import queue
|
|
13
16
|
import threading
|
|
14
17
|
import time
|
|
@@ -40,7 +43,25 @@ def native_available() -> bool:
|
|
|
40
43
|
return True
|
|
41
44
|
|
|
42
45
|
|
|
46
|
+
def uses_native_coreloop() -> bool:
|
|
47
|
+
"""Prefer Rust when the PyO3 module is importable.
|
|
48
|
+
|
|
49
|
+
``STEERABLE_RUST_CORELOOP=0`` keeps the Python loop. ``=1`` selects
|
|
50
|
+
Rust even if the caller has not probed availability first (the
|
|
51
|
+
subsequent import then fails loudly). Unset follows
|
|
52
|
+
:func:`native_available`.
|
|
53
|
+
"""
|
|
54
|
+
flag = os.environ.get("STEERABLE_RUST_CORELOOP")
|
|
55
|
+
if flag == "0":
|
|
56
|
+
return False
|
|
57
|
+
if flag == "1":
|
|
58
|
+
return True
|
|
59
|
+
return native_available()
|
|
60
|
+
|
|
61
|
+
|
|
43
62
|
def _config_json(loop: CoreLoop) -> str:
|
|
63
|
+
import steerable_agent_runtime.loop as loop_module
|
|
64
|
+
|
|
44
65
|
config = loop._config
|
|
45
66
|
payload: dict[str, Any] = {
|
|
46
67
|
"max_rounds": config.max_rounds,
|
|
@@ -54,6 +75,10 @@ def _config_json(loop: CoreLoop) -> str:
|
|
|
54
75
|
"wrap_up_max_tool_rounds": config.wrap_up_max_tool_rounds,
|
|
55
76
|
"wrap_up_tool_timeout_ms": config.wrap_up_tool_timeout_ms,
|
|
56
77
|
"wrap_up_hard_cap_ms": config.wrap_up_hard_cap_ms,
|
|
78
|
+
"idle_stream_timeout_ms": config.idle_stream_timeout_ms,
|
|
79
|
+
"idle_stream_max_chars": config.idle_stream_max_chars,
|
|
80
|
+
"reasoning_without_progress_chars": config.reasoning_without_progress_chars,
|
|
81
|
+
"idle_reasoning_gap_ms": int(loop_module._IDLE_REASONING_GAP_SEC * 1000),
|
|
57
82
|
"steer_mode": config.steer_mode,
|
|
58
83
|
}
|
|
59
84
|
if config.budget is not None:
|
|
@@ -167,7 +192,12 @@ async def _collect_stream(
|
|
|
167
192
|
cancel_flag: threading.Event,
|
|
168
193
|
run_started: float,
|
|
169
194
|
surrogate_mode: list[bool],
|
|
170
|
-
|
|
195
|
+
output: queue.Queue[str],
|
|
196
|
+
native_stream_id: str,
|
|
197
|
+
) -> None:
|
|
198
|
+
def send(kind: str, **payload: Any) -> None:
|
|
199
|
+
output.put(json.dumps({"kind": kind, **payload}))
|
|
200
|
+
|
|
171
201
|
request = json.loads(messages_json)
|
|
172
202
|
if isinstance(request, dict):
|
|
173
203
|
message_items = request.get("messages", [])
|
|
@@ -183,31 +213,40 @@ async def _collect_stream(
|
|
|
183
213
|
and message.content_text.startswith("[system notice] The time budget")
|
|
184
214
|
for message in messages
|
|
185
215
|
)
|
|
216
|
+
stream_kwargs: dict[str, Any] = {}
|
|
217
|
+
if tool_choice:
|
|
218
|
+
stream_kwargs["tool_choice"] = tool_choice
|
|
219
|
+
if loop._config.temperature is not None:
|
|
220
|
+
stream_kwargs["temperature"] = loop._config.temperature
|
|
221
|
+
if loop._config.max_tokens is not None:
|
|
222
|
+
stream_kwargs["max_tokens"] = loop._config.max_tokens
|
|
186
223
|
stream = loop._provider.stream(
|
|
187
224
|
messages,
|
|
188
225
|
tools=tools if tools_enabled else None,
|
|
189
|
-
**
|
|
226
|
+
**stream_kwargs,
|
|
190
227
|
)
|
|
191
|
-
chunks: list[dict[str, Any]] = []
|
|
192
228
|
stream_started = time.monotonic()
|
|
193
|
-
last_chunk_at = stream_started
|
|
194
|
-
active_ms = 0
|
|
195
|
-
stream_chars = 0
|
|
196
229
|
tool_seen = False
|
|
197
230
|
raw_content: list[str] = []
|
|
198
231
|
stripper = PseudoStreamStripper()
|
|
199
232
|
content_carry = ""
|
|
200
233
|
cut_finish_reason: str | None = None
|
|
201
|
-
import steerable_agent_runtime.loop as loop_module
|
|
202
|
-
|
|
203
234
|
try:
|
|
204
235
|
while True:
|
|
205
236
|
soft_timeout_ms = loop._config.soft_timeout_ms
|
|
206
237
|
wait_seconds = None
|
|
207
|
-
|
|
238
|
+
timeout_finish_reason = "__soft_timeout_cut__"
|
|
239
|
+
if wrap_up_request and loop._config.wrap_up_tool_timeout_ms is not None:
|
|
240
|
+
wait_seconds = max(
|
|
241
|
+
loop._config.wrap_up_tool_timeout_ms / 1000
|
|
242
|
+
- (time.monotonic() - stream_started),
|
|
243
|
+
0.05,
|
|
244
|
+
)
|
|
245
|
+
timeout_finish_reason = "__wrap_stream_cut__"
|
|
246
|
+
elif soft_timeout_ms is not None and not wrap_up_request:
|
|
208
247
|
wait_seconds = max(
|
|
209
248
|
soft_timeout_ms / 1000 - (time.monotonic() - run_started),
|
|
210
|
-
0.
|
|
249
|
+
0.05,
|
|
211
250
|
)
|
|
212
251
|
try:
|
|
213
252
|
if wait_seconds is None:
|
|
@@ -217,13 +256,14 @@ async def _collect_stream(
|
|
|
217
256
|
except StopAsyncIteration:
|
|
218
257
|
break
|
|
219
258
|
except TimeoutError:
|
|
220
|
-
cut_finish_reason =
|
|
259
|
+
cut_finish_reason = timeout_finish_reason
|
|
221
260
|
break
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
261
|
+
observe_chunk = getattr(loop._hooks, "on_stream_chunk", None)
|
|
262
|
+
if callable(observe_chunk):
|
|
263
|
+
try:
|
|
264
|
+
observe_chunk(chunk, loop._run_context)
|
|
265
|
+
except Exception: # noqa: BLE001 — observation cannot break streaming
|
|
266
|
+
logger.exception("native_bridge_on_stream_chunk_failed")
|
|
227
267
|
payload = _chunk_to_json(chunk)
|
|
228
268
|
if chunk.content_delta:
|
|
229
269
|
raw_content.append(chunk.content_delta)
|
|
@@ -235,71 +275,43 @@ async def _collect_stream(
|
|
|
235
275
|
):
|
|
236
276
|
surrogate_mode[0] = True
|
|
237
277
|
payload["content_delta"] = stripper.feed(emit_text) or None
|
|
238
|
-
|
|
278
|
+
send("chunk", chunk=payload)
|
|
239
279
|
if chunk.tool_call_delta is not None:
|
|
240
280
|
tool_seen = True
|
|
241
|
-
loop._run_context.reasoning_since_progress = 0
|
|
242
|
-
if not tool_seen:
|
|
243
|
-
delta_chars = len(chunk.reasoning_delta or "") + len(
|
|
244
|
-
chunk.content_delta or ""
|
|
245
|
-
)
|
|
246
|
-
stream_chars += delta_chars
|
|
247
|
-
loop._run_context.reasoning_since_progress += len(
|
|
248
|
-
chunk.reasoning_delta or ""
|
|
249
|
-
)
|
|
250
|
-
trigger = None
|
|
251
|
-
idle_ms = loop._config.idle_stream_timeout_ms
|
|
252
|
-
max_chars = loop._config.idle_stream_max_chars
|
|
253
|
-
stale_cap = loop._config.reasoning_without_progress_chars
|
|
254
|
-
if idle_ms is not None and active_ms >= idle_ms:
|
|
255
|
-
trigger = "active_ms"
|
|
256
|
-
elif max_chars is not None and stream_chars >= max_chars:
|
|
257
|
-
trigger = "chars"
|
|
258
|
-
elif (
|
|
259
|
-
stale_cap is not None
|
|
260
|
-
and loop._run_context.reasoning_since_progress >= stale_cap
|
|
261
|
-
):
|
|
262
|
-
trigger = "no_progress"
|
|
263
|
-
if trigger is not None:
|
|
264
|
-
stale_chars = loop._run_context.reasoning_since_progress
|
|
265
|
-
cut_finish_reason = (
|
|
266
|
-
f"__idle_stream_cut__:{trigger}:{stream_chars}:"
|
|
267
|
-
f"{stale_chars}"
|
|
268
|
-
)
|
|
269
|
-
if trigger == "no_progress":
|
|
270
|
-
loop._run_context.reasoning_since_progress = 0
|
|
271
|
-
break
|
|
272
281
|
if cancel_flag.is_set() or loop._cancel_event.is_set():
|
|
273
282
|
break
|
|
283
|
+
except asyncio.CancelledError:
|
|
284
|
+
send("error", error="cancelled", error_kind="transport")
|
|
285
|
+
return
|
|
274
286
|
except Exception as exc: # noqa: BLE001 — Rust applies on_request_error
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
"retry_after_ms": getattr(exc, "retry_after_ms", None),
|
|
283
|
-
}
|
|
287
|
+
send(
|
|
288
|
+
"error",
|
|
289
|
+
error=str(exc),
|
|
290
|
+
error_kind=getattr(exc, "kind", "unknown"),
|
|
291
|
+
status_code=getattr(exc, "status_code", None),
|
|
292
|
+
provider=getattr(exc, "provider", None),
|
|
293
|
+
retry_after_ms=getattr(exc, "retry_after_ms", None),
|
|
284
294
|
)
|
|
295
|
+
return
|
|
285
296
|
display_tail = stripper.feed(content_carry) + stripper.flush()
|
|
286
297
|
if display_tail:
|
|
287
|
-
|
|
298
|
+
send("chunk", chunk={"content_delta": display_tail})
|
|
288
299
|
if cut_finish_reason is not None:
|
|
289
|
-
|
|
300
|
+
send("chunk", chunk={"finish_reason": cut_finish_reason})
|
|
290
301
|
elif not tool_seen:
|
|
291
302
|
recovered, _ = extract_inline_tool_calls("".join(raw_content))
|
|
292
303
|
for index, call in enumerate(recovered):
|
|
293
|
-
|
|
294
|
-
|
|
304
|
+
send(
|
|
305
|
+
"chunk",
|
|
306
|
+
chunk={
|
|
295
307
|
"tool_call_delta": {
|
|
296
|
-
"id": f"recovered_{index}",
|
|
308
|
+
"id": f"recovered_{native_stream_id}_{index}",
|
|
297
309
|
"name": call["name"],
|
|
298
310
|
"arguments": call.get("arguments") or {},
|
|
299
311
|
}
|
|
300
|
-
}
|
|
312
|
+
},
|
|
301
313
|
)
|
|
302
|
-
|
|
314
|
+
send("done")
|
|
303
315
|
|
|
304
316
|
|
|
305
317
|
async def _exec_tool(loop: CoreLoop, call_json: str) -> str:
|
|
@@ -351,9 +363,12 @@ async def _post_tool_result(loop: CoreLoop, payload_json: str) -> str:
|
|
|
351
363
|
)
|
|
352
364
|
updated = await loop._hooks.post_tool_result(result, call, loop._run_context)
|
|
353
365
|
made_progress = getattr(loop._hooks, "tool_made_progress", None)
|
|
354
|
-
|
|
366
|
+
progressed = bool(callable(made_progress) and made_progress(updated, call))
|
|
367
|
+
if progressed:
|
|
355
368
|
loop._run_context.reasoning_since_progress = 0
|
|
356
|
-
return json.dumps(
|
|
369
|
+
return json.dumps(
|
|
370
|
+
{"result": _result_to_json(updated), "madeProgress": progressed}
|
|
371
|
+
)
|
|
357
372
|
|
|
358
373
|
|
|
359
374
|
async def _pre_step(
|
|
@@ -432,11 +447,34 @@ async def _on_request_error(
|
|
|
432
447
|
|
|
433
448
|
|
|
434
449
|
def _native_history_kind(message: LLMMessage) -> str | None:
|
|
450
|
+
if message.role == "user" and any(
|
|
451
|
+
isinstance(part, ImagePart) for part in message.content
|
|
452
|
+
):
|
|
453
|
+
return "tool.image"
|
|
435
454
|
if (
|
|
436
455
|
message.role == "user"
|
|
437
456
|
and message.content_text.startswith("[system notice] The time budget")
|
|
438
457
|
):
|
|
439
458
|
return "loop.soft_timeout_notice"
|
|
459
|
+
if (
|
|
460
|
+
message.role == "user"
|
|
461
|
+
and "was cut off mid-reasoning" in message.content_text
|
|
462
|
+
):
|
|
463
|
+
return "loop.stream_cut_notice"
|
|
464
|
+
if (
|
|
465
|
+
message.role == "user"
|
|
466
|
+
and message.content_text.startswith(
|
|
467
|
+
"[system notice] The task ended without a natural-language summary"
|
|
468
|
+
)
|
|
469
|
+
):
|
|
470
|
+
return "loop.narration_request"
|
|
471
|
+
if (
|
|
472
|
+
message.role == "user"
|
|
473
|
+
and message.content_text.startswith(
|
|
474
|
+
"[system notice] The previous reply described an intended action"
|
|
475
|
+
)
|
|
476
|
+
):
|
|
477
|
+
return "loop.discipline_retry_notice"
|
|
440
478
|
if (
|
|
441
479
|
message.role == "tool"
|
|
442
480
|
and "not executed" in message.content_text
|
|
@@ -455,6 +493,18 @@ def _native_history_kind(message: LLMMessage) -> str | None:
|
|
|
455
493
|
and "mid-turn message" in message.content_text
|
|
456
494
|
):
|
|
457
495
|
return "loop.steer_skip"
|
|
496
|
+
if (
|
|
497
|
+
message.role == "tool"
|
|
498
|
+
and "not executed" in message.content_text
|
|
499
|
+
and "too many consecutive tool errors" in message.content_text
|
|
500
|
+
):
|
|
501
|
+
return "loop.breaker_skip"
|
|
502
|
+
if (
|
|
503
|
+
message.role == "tool"
|
|
504
|
+
and "not executed" in message.content_text
|
|
505
|
+
and "earlier tool ended the turn" in message.content_text
|
|
506
|
+
):
|
|
507
|
+
return "loop.terminal_skip"
|
|
458
508
|
return None
|
|
459
509
|
|
|
460
510
|
|
|
@@ -465,6 +515,12 @@ async def _sync_history(
|
|
|
465
515
|
chat_id: str | None,
|
|
466
516
|
) -> str:
|
|
467
517
|
incoming = [_msg_from_json(item) for item in json.loads(history_json)]
|
|
518
|
+
if any(
|
|
519
|
+
message.role == "user"
|
|
520
|
+
and message.content_text.startswith("[system notice] The time budget")
|
|
521
|
+
for message in incoming
|
|
522
|
+
):
|
|
523
|
+
loop._wrap_up_active = True
|
|
468
524
|
current = manager.projection
|
|
469
525
|
if incoming[: len(current)] == current:
|
|
470
526
|
for message in incoming[len(current) :]:
|
|
@@ -512,6 +568,7 @@ async def run_native(
|
|
|
512
568
|
aio_loop = asyncio.get_running_loop()
|
|
513
569
|
out: asyncio.Queue[tuple[LoopEvent, threading.Event] | None] = asyncio.Queue()
|
|
514
570
|
loop._run_context = LoopContext(chat_id=chat_id)
|
|
571
|
+
loop._wrap_up_active = False
|
|
515
572
|
record_id = loop._record_id or chat_id
|
|
516
573
|
seed = list(messages)
|
|
517
574
|
first_seq = 0
|
|
@@ -540,6 +597,10 @@ async def run_native(
|
|
|
540
597
|
cancel_flag = threading.Event()
|
|
541
598
|
steer_q: queue.Queue[str] = queue.Queue()
|
|
542
599
|
active_tool_tasks: set[asyncio.Task[str]] = set()
|
|
600
|
+
active_stream_tasks: set[asyncio.Task[None]] = set()
|
|
601
|
+
stream_queues: dict[str, queue.Queue[str]] = {}
|
|
602
|
+
prefetched_stream_frames: dict[str, str] = {}
|
|
603
|
+
stream_counter = [0]
|
|
543
604
|
tool_cancel_reason = ["cancelled"]
|
|
544
605
|
run_started = time.monotonic()
|
|
545
606
|
surrogate_mode = [False]
|
|
@@ -562,6 +623,8 @@ async def run_native(
|
|
|
562
623
|
def cancel_now() -> None:
|
|
563
624
|
for task in tuple(active_tool_tasks):
|
|
564
625
|
task.cancel()
|
|
626
|
+
for task in tuple(active_stream_tasks):
|
|
627
|
+
task.cancel()
|
|
565
628
|
|
|
566
629
|
aio_loop.call_soon_threadsafe(cancel_now)
|
|
567
630
|
|
|
@@ -625,17 +688,78 @@ async def run_native(
|
|
|
625
688
|
consumed.wait()
|
|
626
689
|
|
|
627
690
|
def stream_llm(messages_json: str) -> str:
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
691
|
+
stream_counter[0] += 1
|
|
692
|
+
stream_id = f"stream-{stream_counter[0]}"
|
|
693
|
+
output: queue.Queue[str] = queue.Queue()
|
|
694
|
+
stream_queues[stream_id] = output
|
|
695
|
+
|
|
696
|
+
def start() -> None:
|
|
697
|
+
task = aio_loop.create_task(
|
|
698
|
+
_collect_stream(
|
|
699
|
+
loop,
|
|
700
|
+
messages_json,
|
|
701
|
+
tools,
|
|
702
|
+
cancel_flag,
|
|
703
|
+
run_started,
|
|
704
|
+
surrogate_mode,
|
|
705
|
+
output,
|
|
706
|
+
stream_id,
|
|
707
|
+
)
|
|
708
|
+
)
|
|
709
|
+
active_stream_tasks.add(task)
|
|
710
|
+
|
|
711
|
+
def done(completed: asyncio.Task[None]) -> None:
|
|
712
|
+
active_stream_tasks.discard(completed)
|
|
713
|
+
try:
|
|
714
|
+
completed.result()
|
|
715
|
+
except asyncio.CancelledError:
|
|
716
|
+
output.put(
|
|
717
|
+
json.dumps(
|
|
718
|
+
{
|
|
719
|
+
"kind": "error",
|
|
720
|
+
"error": "cancelled",
|
|
721
|
+
"error_kind": "transport",
|
|
722
|
+
}
|
|
723
|
+
)
|
|
724
|
+
)
|
|
725
|
+
except BaseException as exc: # noqa: BLE001
|
|
726
|
+
output.put(
|
|
727
|
+
json.dumps(
|
|
728
|
+
{
|
|
729
|
+
"kind": "error",
|
|
730
|
+
"error": str(exc),
|
|
731
|
+
"error_kind": "unknown",
|
|
732
|
+
}
|
|
733
|
+
)
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
task.add_done_callback(done)
|
|
737
|
+
|
|
738
|
+
aio_loop.call_soon_threadsafe(start)
|
|
739
|
+
first_frame = output.get()
|
|
740
|
+
first = json.loads(first_frame)
|
|
741
|
+
if first.get("kind") == "error":
|
|
742
|
+
stream_queues.pop(stream_id, None)
|
|
743
|
+
first["ok"] = False
|
|
744
|
+
return json.dumps(first)
|
|
745
|
+
prefetched_stream_frames[stream_id] = first_frame
|
|
746
|
+
return json.dumps({"ok": True, "stream_id": stream_id})
|
|
747
|
+
|
|
748
|
+
def next_stream_chunk(stream_id: str) -> str:
|
|
749
|
+
output = stream_queues.get(stream_id)
|
|
750
|
+
if output is None:
|
|
751
|
+
return json.dumps(
|
|
752
|
+
{
|
|
753
|
+
"kind": "error",
|
|
754
|
+
"error": f"unknown stream: {stream_id}",
|
|
755
|
+
"error_kind": "unknown",
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
frame = prefetched_stream_frames.pop(stream_id, None) or output.get()
|
|
759
|
+
kind = json.loads(frame).get("kind")
|
|
760
|
+
if kind in ("done", "error"):
|
|
761
|
+
stream_queues.pop(stream_id, None)
|
|
762
|
+
return frame
|
|
639
763
|
|
|
640
764
|
def exec_tool(call_json: str) -> str:
|
|
641
765
|
box: queue.Queue[str] = queue.Queue()
|
|
@@ -718,6 +842,9 @@ async def run_native(
|
|
|
718
842
|
ctx.accumulated_completion_tokens = int(
|
|
719
843
|
raw.get("accumulated_completion_tokens") or 0
|
|
720
844
|
)
|
|
845
|
+
ctx.reasoning_since_progress = int(
|
|
846
|
+
raw.get("reasoning_since_progress") or 0
|
|
847
|
+
)
|
|
721
848
|
return json.dumps({"ok": True})
|
|
722
849
|
|
|
723
850
|
def before_completion(draft_json: str) -> str:
|
|
@@ -776,6 +903,7 @@ async def run_native(
|
|
|
776
903
|
getattr(loop._provider, "model", "unknown"),
|
|
777
904
|
emit,
|
|
778
905
|
stream_llm,
|
|
906
|
+
next_stream_chunk,
|
|
779
907
|
exec_tool,
|
|
780
908
|
tool_concurrency_safe,
|
|
781
909
|
tool_dedup_exempt,
|
|
@@ -384,16 +384,16 @@ class OrchestrationExecutor:
|
|
|
384
384
|
return bool(inner_exempt and inner_exempt(call))
|
|
385
385
|
|
|
386
386
|
def concurrency_safe(self, call: ToolCall) -> bool:
|
|
387
|
-
#
|
|
388
|
-
#
|
|
389
|
-
#
|
|
387
|
+
# Spawn consumes the bounded parallel-capacity pool, so batching two
|
|
388
|
+
# spawns makes which call succeeds scheduler-dependent. Wait/list only
|
|
389
|
+
# observe pool state and may batch.
|
|
390
390
|
if call.name in {
|
|
391
|
-
self._config.spawn_tool,
|
|
392
391
|
self._config.wait_tool,
|
|
393
392
|
self._config.list_tool,
|
|
394
393
|
}:
|
|
395
394
|
return True
|
|
396
395
|
if call.name in {
|
|
396
|
+
self._config.spawn_tool,
|
|
397
397
|
self._config.send_tool,
|
|
398
398
|
self._config.close_tool,
|
|
399
399
|
self._config.interrupt_tool,
|
|
@@ -202,6 +202,9 @@ class AgentPool:
|
|
|
202
202
|
"childId": child_id,
|
|
203
203
|
"depth": self._depth + 1,
|
|
204
204
|
"task": task,
|
|
205
|
+
# Present only when the factory gave the child a durable
|
|
206
|
+
# record; hosts read that record to show the child's process.
|
|
207
|
+
**({"recordId": loop.record_id} if loop.record_id else {}),
|
|
205
208
|
**(event_extra or {}),
|
|
206
209
|
},
|
|
207
210
|
)
|