steerable-agent-runtime 0.6.34__tar.gz → 0.6.35__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.34 → steerable_agent_runtime-0.6.35}/PKG-INFO +1 -1
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/pyproject.toml +1 -1
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/__init__.py +4 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/ask_user.py +4 -2
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/pool.py +14 -1
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/subagent.py +133 -8
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_subagent.py +232 -16
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/README.md +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/setup.cfg +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/antihallucination.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/approval.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/branch.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/cache_control.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/calibration.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/compaction.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/config.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/default.harness.json +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/default.harness.yaml +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/delegation_gate.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/errors.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/handoff.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/harness.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/history.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/hooks.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/__init__.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/compat.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/errors.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/google_genai.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/openai_compat.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/openai_responses.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/parts.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/presets.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/loop.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/maintenance.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/mcp.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/model_info.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/native_bridge.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/orchestration.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/otel.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/plugins.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/pricing.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/pseudo.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/recording.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/reminders.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/replay.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/resume.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/retry.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/skills.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/spill.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/storage/__init__.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/todo.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/tokens.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/tool_search.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/tools.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/tracing.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/transport/__init__.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime/world_state.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_antihallucination.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_approval.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_approval_policy.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_ask_user.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_branch.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_cache_control.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_cache_instrumentation.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_calibration.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_compaction.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_compaction_policy.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_compaction_strategy.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_config.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_content_parts.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_delegation_gate.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_error_taxonomy.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_fragment_bounds.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_gateway_catalog.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_golden.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_handoff.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_harness.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_harness_spec.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_history.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_history_persistence.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_hooks.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_in_memory_storage.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_llm_gemini_wire.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_llm_responses_wire.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_llm_stream_e2e.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_llm_wire_helpers.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_long_session.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_loop.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_loop_cancellation.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_loop_replay.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_loop_sandbox_event.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_loop_terminal.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_maintenance.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_mcp.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_mcp_server.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_model_catalog.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_model_equivalence.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_model_info.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_model_resolve.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_native_default.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_observation_aging.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_orchestration.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_otel.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_parallel_tools.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_plugins.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_provider_compat.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_provider_presets.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_pseudo.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_pyo3_api_surface.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_recording.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_reminders.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_replay_crosslang.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_resume.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_retry_hooks.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_safety_gate.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_sandboxed.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_skills.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_soft_timeout.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_spill.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_sqlite_storage.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_steer.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_storage_contract.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_stream_strip.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_system_proxy.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_todo.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_todo_gate.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tokens.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tool_exposure.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tool_hygiene.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tool_router.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tool_schema.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_tool_timeout.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_trace_recorder.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_transport_jsonrpc.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_transport_sse.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_usage_attribution.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_world_state.py +0 -0
- {steerable_agent_runtime-0.6.34 → steerable_agent_runtime-0.6.35}/tests/test_write_lease.py +0 -0
|
@@ -250,9 +250,11 @@ from .spill import FilesystemSpillStore, InMemorySpillStore, SpillHooks, SpillSt
|
|
|
250
250
|
from .storage import StorageAdapter
|
|
251
251
|
from .subagent import (
|
|
252
252
|
FilteredToolsExecutor,
|
|
253
|
+
PARENT_INPUT_TOOL_NAME,
|
|
253
254
|
SubagentConfig,
|
|
254
255
|
SubagentExecutor,
|
|
255
256
|
SubagentRegistry,
|
|
257
|
+
parent_input_tool_descriptor,
|
|
256
258
|
subagent_tool_descriptor,
|
|
257
259
|
)
|
|
258
260
|
from .tokens import (
|
|
@@ -285,6 +287,7 @@ __all__ = [
|
|
|
285
287
|
"MODEL_INFOS",
|
|
286
288
|
"MODEL_PRICES",
|
|
287
289
|
"MODEL_TOKEN_FACTORS",
|
|
290
|
+
"PARENT_INPUT_TOOL_NAME",
|
|
288
291
|
"REASONING_EFFORT_ORDER",
|
|
289
292
|
"RECORD_FORMAT_VERSION",
|
|
290
293
|
"ReasoningEffortUnsupported",
|
|
@@ -465,6 +468,7 @@ __all__ = [
|
|
|
465
468
|
"load_transcript",
|
|
466
469
|
"make_ask_user_tool",
|
|
467
470
|
"make_todo_write_tool",
|
|
471
|
+
"parent_input_tool_descriptor",
|
|
468
472
|
"matches_conditions",
|
|
469
473
|
"mcp_invoker",
|
|
470
474
|
"merge_patch",
|
|
@@ -125,7 +125,9 @@ def _derive_header(text: str) -> str:
|
|
|
125
125
|
return truncated
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
def
|
|
128
|
+
def normalize_ask_user_questions(
|
|
129
|
+
questions: list[dict[str, Any]],
|
|
130
|
+
) -> list[dict[str, Any]]:
|
|
129
131
|
"""Map known alias keys onto the canonical payload fields, enforce the
|
|
130
132
|
Claude Code ``AskUserQuestion`` bounds (1-4 questions, 2-4 options per
|
|
131
133
|
select, ``header`` <=12 chars, ``multiSelect`` explicit), and require the
|
|
@@ -238,7 +240,7 @@ def make_ask_user_tool(handler: AskUserHandler) -> Callable[..., Awaitable[dict[
|
|
|
238
240
|
async def ask_user(
|
|
239
241
|
intro: str, questions: list[dict[str, Any]], outro: str | None = None
|
|
240
242
|
) -> dict[str, Any]:
|
|
241
|
-
normalized =
|
|
243
|
+
normalized = normalize_ask_user_questions(questions)
|
|
242
244
|
answers = await handler(intro, normalized)
|
|
243
245
|
return {
|
|
244
246
|
"intro": intro,
|
|
@@ -22,6 +22,9 @@ Design:
|
|
|
22
22
|
- **Children stay out of the parent record.** A child's transcript is its
|
|
23
23
|
own loop's; hosts that want child traces subscribe via ``event_sink``
|
|
24
24
|
(lifecycle events with lineage) or wrap children in their own hooks.
|
|
25
|
+
- **Session state is isolated.** A child runs with its durable record id as
|
|
26
|
+
``chat_id`` when available, otherwise a pool-local id, so session-scoped
|
|
27
|
+
tools such as ``todo_write`` cannot overwrite a sibling or parent list.
|
|
25
28
|
"""
|
|
26
29
|
|
|
27
30
|
from __future__ import annotations
|
|
@@ -30,6 +33,7 @@ import asyncio
|
|
|
30
33
|
import logging
|
|
31
34
|
from collections.abc import Callable
|
|
32
35
|
from dataclasses import dataclass
|
|
36
|
+
from itertools import count
|
|
33
37
|
from typing import Any
|
|
34
38
|
|
|
35
39
|
from .llm import LLMMessage
|
|
@@ -40,6 +44,7 @@ logger = logging.getLogger(__name__)
|
|
|
40
44
|
#: Grace between a cooperative close (``loop.cancel()``) and the hard-cancel
|
|
41
45
|
#: backstop for a child that fails to wind down.
|
|
42
46
|
_CLOSE_GRACE_S = 2.0
|
|
47
|
+
_POOL_CONTEXT_SEQUENCE = count(1)
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
class OrchestrationBudgetExceeded(Exception):
|
|
@@ -144,6 +149,7 @@ class AgentPool:
|
|
|
144
149
|
self._config = config
|
|
145
150
|
self._depth = depth
|
|
146
151
|
self._lineage = lineage
|
|
152
|
+
self._context_namespace = next(_POOL_CONTEXT_SEQUENCE)
|
|
147
153
|
self._loop_factory = loop_factory
|
|
148
154
|
self._event_sink = event_sink
|
|
149
155
|
self._children: dict[str, _ChildHandle] = {}
|
|
@@ -241,7 +247,14 @@ class AgentPool:
|
|
|
241
247
|
answer_parts: list[str] = []
|
|
242
248
|
status = "completed"
|
|
243
249
|
try:
|
|
244
|
-
async for event in handle.loop.run(
|
|
250
|
+
async for event in handle.loop.run(
|
|
251
|
+
seed,
|
|
252
|
+
tools=handle.schemas,
|
|
253
|
+
chat_id=(
|
|
254
|
+
handle.loop.record_id
|
|
255
|
+
or f"subagent:{self._context_namespace}:{handle.child_id}"
|
|
256
|
+
),
|
|
257
|
+
):
|
|
245
258
|
if event.kind == "content_delta":
|
|
246
259
|
answer_parts.append(str(event.data.get("delta") or ""))
|
|
247
260
|
elif event.kind == "completion":
|
|
@@ -27,6 +27,10 @@ Design:
|
|
|
27
27
|
``recordId``), which is how a host renders the delegation's process
|
|
28
28
|
afterwards. Without them the child runs storage-free, as before.
|
|
29
29
|
- The child's answer is its accumulated assistant text at completion.
|
|
30
|
+
- User interaction stays with the parent. Every child receives the
|
|
31
|
+
``request_parent_input`` coordination tool instead of ``ask_user``; a
|
|
32
|
+
request ends the child run and returns structured ``needs_input`` data so
|
|
33
|
+
the parent can ask and re-delegate with the answers.
|
|
30
34
|
- Opt-out: hosts advertise ``subagent_tool_descriptor`` in the tools list
|
|
31
35
|
and wrap their executor; the sidecar does both by default and
|
|
32
36
|
``params.subagent: false`` turns delegation off.
|
|
@@ -47,21 +51,54 @@ Design:
|
|
|
47
51
|
|
|
48
52
|
from __future__ import annotations
|
|
49
53
|
|
|
54
|
+
import json
|
|
50
55
|
from collections.abc import Callable
|
|
51
56
|
from dataclasses import dataclass
|
|
52
57
|
from typing import Any
|
|
53
58
|
|
|
54
59
|
from steerable_agent_protocol.generated import ToolCall, ToolResult
|
|
55
60
|
|
|
61
|
+
from .ask_user import (
|
|
62
|
+
ASK_USER_SCHEMA,
|
|
63
|
+
ASK_USER_TOOL_NAME,
|
|
64
|
+
normalize_ask_user_questions,
|
|
65
|
+
)
|
|
66
|
+
from .errors import ToolDispatchError
|
|
56
67
|
from .llm import LLMProvider
|
|
57
68
|
from .loop import CoreLoop, LoopConfig, LoopContext, LoopHooks, ToolExecutor
|
|
58
|
-
from .pool import
|
|
69
|
+
from .pool import (
|
|
70
|
+
AgentPool,
|
|
71
|
+
LoopFactory,
|
|
72
|
+
OrchestrationBudgetExceeded,
|
|
73
|
+
OrchestrationConfig,
|
|
74
|
+
)
|
|
59
75
|
|
|
60
76
|
#: ``profile`` label on lifecycle events for a delegation that named no
|
|
61
77
|
#: ``subagent_type`` — mirrors the tool schema's "default general-purpose
|
|
62
78
|
#: profile" wording.
|
|
63
79
|
DEFAULT_PROFILE_LABEL = "general-purpose"
|
|
64
80
|
|
|
81
|
+
#: Child-only coordination tool. A child reports missing user-owned input to
|
|
82
|
+
#: its parent instead of opening an interactive question card itself.
|
|
83
|
+
PARENT_INPUT_TOOL_NAME = "request_parent_input"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parent_input_tool_descriptor() -> dict[str, Any]:
|
|
87
|
+
"""OpenAI schema for a child to return structured questions to its parent."""
|
|
88
|
+
return {
|
|
89
|
+
"type": "function",
|
|
90
|
+
"function": {
|
|
91
|
+
"name": PARENT_INPUT_TOOL_NAME,
|
|
92
|
+
"description": (
|
|
93
|
+
"Report information that only the user can provide to the "
|
|
94
|
+
"parent agent. Do not call ask_user from a sub-agent. After "
|
|
95
|
+
"calling this tool, stop and let the parent ask the user, then "
|
|
96
|
+
"re-delegate a self-contained task with the answers."
|
|
97
|
+
),
|
|
98
|
+
"parameters": ASK_USER_SCHEMA,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
65
102
|
|
|
66
103
|
@dataclass(frozen=True, slots=True)
|
|
67
104
|
class SubagentConfig:
|
|
@@ -106,7 +143,9 @@ class SubagentConfig:
|
|
|
106
143
|
description: str = (
|
|
107
144
|
"Delegate a self-contained subtask to a sub-agent with its own "
|
|
108
145
|
"reasoning loop. Good for parallelizable or context-heavy subtasks; "
|
|
109
|
-
"the sub-agent returns only its final answer."
|
|
146
|
+
"the sub-agent returns only its final answer. If it returns status "
|
|
147
|
+
"needs_input, ask the user the returned questions and then delegate "
|
|
148
|
+
"again with the answers included in the task."
|
|
110
149
|
)
|
|
111
150
|
|
|
112
151
|
|
|
@@ -174,9 +213,7 @@ def subagent_tool_descriptor(
|
|
|
174
213
|
f"- {name}: {(registry.get(name) or config).description}"
|
|
175
214
|
for name in registry.names()
|
|
176
215
|
)
|
|
177
|
-
description =
|
|
178
|
-
f"{description}\n\nAvailable subagent_type profiles:\n{roster}"
|
|
179
|
-
)
|
|
216
|
+
description = f"{description}\n\nAvailable subagent_type profiles:\n{roster}"
|
|
180
217
|
return {
|
|
181
218
|
"type": "function",
|
|
182
219
|
"function": {
|
|
@@ -234,6 +271,72 @@ class FilteredToolsExecutor:
|
|
|
234
271
|
return bool(inner_safe and inner_safe(call))
|
|
235
272
|
|
|
236
273
|
|
|
274
|
+
class _ParentInputExecutor:
|
|
275
|
+
"""Keep user interaction at the parent while recording child questions."""
|
|
276
|
+
|
|
277
|
+
def __init__(
|
|
278
|
+
self,
|
|
279
|
+
inner: ToolExecutor,
|
|
280
|
+
on_request: Callable[[dict[str, Any]], None],
|
|
281
|
+
) -> None:
|
|
282
|
+
self._inner = inner
|
|
283
|
+
self._on_request = on_request
|
|
284
|
+
|
|
285
|
+
async def execute(self, call: ToolCall, ctx: LoopContext) -> ToolResult:
|
|
286
|
+
if call.name == ASK_USER_TOOL_NAME:
|
|
287
|
+
return ToolResult(
|
|
288
|
+
success=False,
|
|
289
|
+
error=(
|
|
290
|
+
"sub-agents cannot call ask_user directly; call "
|
|
291
|
+
f"{PARENT_INPUT_TOOL_NAME} so the parent agent can ask"
|
|
292
|
+
),
|
|
293
|
+
needsFollowup=True,
|
|
294
|
+
)
|
|
295
|
+
if call.name != PARENT_INPUT_TOOL_NAME:
|
|
296
|
+
return await self._inner.execute(call, ctx)
|
|
297
|
+
intro = str(call.arguments.get("intro") or "").strip()
|
|
298
|
+
if not intro:
|
|
299
|
+
return ToolResult(
|
|
300
|
+
success=False,
|
|
301
|
+
error=f"{PARENT_INPUT_TOOL_NAME}: intro must be non-empty",
|
|
302
|
+
needsFollowup=True,
|
|
303
|
+
)
|
|
304
|
+
try:
|
|
305
|
+
questions = normalize_ask_user_questions(call.arguments.get("questions"))
|
|
306
|
+
except ToolDispatchError as exc:
|
|
307
|
+
return ToolResult(
|
|
308
|
+
success=False,
|
|
309
|
+
error=str(exc),
|
|
310
|
+
needsFollowup=True,
|
|
311
|
+
)
|
|
312
|
+
payload = {
|
|
313
|
+
"status": "needs_input",
|
|
314
|
+
"intro": intro,
|
|
315
|
+
"questions": questions,
|
|
316
|
+
**(
|
|
317
|
+
{"outro": str(call.arguments["outro"])}
|
|
318
|
+
if call.arguments.get("outro") is not None
|
|
319
|
+
else {}
|
|
320
|
+
),
|
|
321
|
+
}
|
|
322
|
+
self._on_request(payload)
|
|
323
|
+
return ToolResult(
|
|
324
|
+
success=True,
|
|
325
|
+
terminal=True,
|
|
326
|
+
message=(
|
|
327
|
+
"The parent-input request was recorded. Stop this subtask "
|
|
328
|
+
"and return control to the parent agent."
|
|
329
|
+
),
|
|
330
|
+
data={**payload, "terminal_status": "completed"},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
def concurrency_safe(self, call: ToolCall) -> bool:
|
|
334
|
+
if call.name in {ASK_USER_TOOL_NAME, PARENT_INPUT_TOOL_NAME}:
|
|
335
|
+
return False
|
|
336
|
+
inner_safe = getattr(self._inner, "concurrency_safe", None)
|
|
337
|
+
return bool(inner_safe and inner_safe(call))
|
|
338
|
+
|
|
339
|
+
|
|
237
340
|
def _schema_name(schema: dict[str, Any]) -> str:
|
|
238
341
|
function = schema.get("function")
|
|
239
342
|
if isinstance(function, dict) and function.get("name"):
|
|
@@ -288,6 +391,7 @@ class SubagentExecutor:
|
|
|
288
391
|
self._parent_tools = list(tools or [])
|
|
289
392
|
self._history_store = history_store
|
|
290
393
|
self._record_id_prefix = record_id_prefix
|
|
394
|
+
self._parent_input_requests: dict[str, dict[str, Any]] = {}
|
|
291
395
|
self._pool = pool or AgentPool(
|
|
292
396
|
config=OrchestrationConfig(max_parallel=self._config.max_parallel),
|
|
293
397
|
depth=0,
|
|
@@ -353,6 +457,18 @@ class SubagentExecutor:
|
|
|
353
457
|
error=f"orchestration_budget_exceeded: {exc}",
|
|
354
458
|
needsFollowup=True,
|
|
355
459
|
)
|
|
460
|
+
parent_input = self._parent_input_requests.pop(outcome.child_id, None)
|
|
461
|
+
if parent_input is not None:
|
|
462
|
+
return ToolResult(
|
|
463
|
+
success=True,
|
|
464
|
+
message=json.dumps(parent_input, ensure_ascii=False),
|
|
465
|
+
data=parent_input,
|
|
466
|
+
needsFollowup=True,
|
|
467
|
+
nextAction=(
|
|
468
|
+
"Parent agent: call ask_user with these questions, then "
|
|
469
|
+
"delegate again with the answers in the task."
|
|
470
|
+
),
|
|
471
|
+
)
|
|
356
472
|
if outcome.status != "completed":
|
|
357
473
|
return ToolResult(
|
|
358
474
|
success=False,
|
|
@@ -377,14 +493,17 @@ class SubagentExecutor:
|
|
|
377
493
|
# execute() fails closed before the spawn when a
|
|
378
494
|
# model-bearing profile has no provider factory.
|
|
379
495
|
provider = self._provider_factory(config.model)
|
|
380
|
-
schemas: list[dict[str, Any]]
|
|
496
|
+
schemas: list[dict[str, Any]] = []
|
|
381
497
|
if config.allow_tools:
|
|
382
498
|
schemas = [
|
|
383
499
|
schema
|
|
384
500
|
for schema in self._parent_tools
|
|
385
501
|
if _schema_name(schema) != self._config.tool_name
|
|
502
|
+
and _schema_name(schema) != ASK_USER_TOOL_NAME
|
|
503
|
+
and _schema_name(schema) != PARENT_INPUT_TOOL_NAME
|
|
386
504
|
and (tool_filter is None or _schema_name(schema) in tool_filter)
|
|
387
|
-
]
|
|
505
|
+
]
|
|
506
|
+
schemas.append(parent_input_tool_descriptor())
|
|
388
507
|
# A child writes its own durable record when the host gave us a
|
|
389
508
|
# store and a prefix, so the delegation's process can be read
|
|
390
509
|
# back afterwards (`<parent record>:child:<lineage id>`). The
|
|
@@ -398,7 +517,12 @@ class SubagentExecutor:
|
|
|
398
517
|
return (
|
|
399
518
|
CoreLoop(
|
|
400
519
|
provider,
|
|
401
|
-
|
|
520
|
+
_ParentInputExecutor(
|
|
521
|
+
self._child_executor(config),
|
|
522
|
+
lambda payload: self._parent_input_requests.__setitem__(
|
|
523
|
+
child_id, payload
|
|
524
|
+
),
|
|
525
|
+
),
|
|
402
526
|
LoopConfig(
|
|
403
527
|
max_rounds=config.max_rounds,
|
|
404
528
|
max_tool_errors=config.max_tool_errors,
|
|
@@ -424,6 +548,7 @@ class SubagentExecutor:
|
|
|
424
548
|
hosts call this on stream teardown (a no-op twice, so a shared
|
|
425
549
|
pool may also be shut down via the orchestration executor)."""
|
|
426
550
|
await self._pool.shutdown()
|
|
551
|
+
self._parent_input_requests.clear()
|
|
427
552
|
|
|
428
553
|
def concurrency_safe(self, call: ToolCall) -> bool:
|
|
429
554
|
# Delegation spawns a full child loop — batched with siblings only
|
|
@@ -6,6 +6,7 @@ import asyncio
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
from steerable_agent_runtime import (
|
|
9
|
+
PARENT_INPUT_TOOL_NAME,
|
|
9
10
|
CoreLoop,
|
|
10
11
|
LoopConfig,
|
|
11
12
|
RouterToolExecutor,
|
|
@@ -13,15 +14,17 @@ from steerable_agent_runtime import (
|
|
|
13
14
|
SubagentExecutor,
|
|
14
15
|
SubagentRegistry,
|
|
15
16
|
ToolRouter,
|
|
17
|
+
parent_input_tool_descriptor,
|
|
16
18
|
subagent_tool_descriptor,
|
|
17
19
|
)
|
|
18
20
|
from steerable_agent_runtime.llm import LLMMessage, LLMStreamChunk
|
|
19
|
-
|
|
20
21
|
from test_loop import collect
|
|
21
22
|
from test_trace_recorder import make_provider, tc
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
async def _run_parent(
|
|
25
|
+
async def _run_parent(
|
|
26
|
+
script, router: ToolRouter, *, config: SubagentConfig | None = None
|
|
27
|
+
):
|
|
25
28
|
provider = make_provider(script)
|
|
26
29
|
executor = SubagentExecutor(RouterToolExecutor(router), provider, config)
|
|
27
30
|
loop = CoreLoop(provider, executor, LoopConfig())
|
|
@@ -80,6 +83,100 @@ async def test_child_writes_its_own_durable_record_and_publishes_the_id() -> Non
|
|
|
80
83
|
assert "chat-1" not in history.records
|
|
81
84
|
|
|
82
85
|
|
|
86
|
+
@pytest.mark.asyncio
|
|
87
|
+
async def test_children_use_their_record_ids_as_isolated_chat_contexts() -> None:
|
|
88
|
+
"""Session-scoped child tools such as todo_write must not share the empty
|
|
89
|
+
chat id or overwrite the parent's state."""
|
|
90
|
+
contexts: list[str] = []
|
|
91
|
+
router = ToolRouter()
|
|
92
|
+
|
|
93
|
+
async def capture_context(
|
|
94
|
+
label: str, context: dict | None = None
|
|
95
|
+
) -> dict[str, str]:
|
|
96
|
+
chat_id = str((context or {}).get("chat_id") or "")
|
|
97
|
+
contexts.append(chat_id)
|
|
98
|
+
return {"label": label, "chatId": chat_id}
|
|
99
|
+
|
|
100
|
+
router.register(capture_context)
|
|
101
|
+
provider = make_provider(
|
|
102
|
+
[
|
|
103
|
+
{"tool_calls": [tc("delegate_subagent", {"task": "first"})]},
|
|
104
|
+
{"tool_calls": [tc("capture_context", {"label": "first"})]},
|
|
105
|
+
{"content": "first done"},
|
|
106
|
+
{"tool_calls": [tc("delegate_subagent", {"task": "second"})]},
|
|
107
|
+
{"tool_calls": [tc("capture_context", {"label": "second"})]},
|
|
108
|
+
{"content": "second done"},
|
|
109
|
+
{"content": "parent done"},
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
history = _MemoryHistory()
|
|
113
|
+
capture_schema = {
|
|
114
|
+
"type": "function",
|
|
115
|
+
"function": {
|
|
116
|
+
"name": "capture_context",
|
|
117
|
+
"parameters": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"properties": {"label": {"type": "string"}},
|
|
120
|
+
"required": ["label"],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
executor = SubagentExecutor(
|
|
125
|
+
RouterToolExecutor(router),
|
|
126
|
+
provider,
|
|
127
|
+
tools=[capture_schema],
|
|
128
|
+
history_store=history,
|
|
129
|
+
record_id_prefix="chat-1",
|
|
130
|
+
)
|
|
131
|
+
loop = CoreLoop(provider, executor, LoopConfig())
|
|
132
|
+
[e async for e in loop.run([LLMMessage.text_of("user", "go")])]
|
|
133
|
+
await executor.shutdown()
|
|
134
|
+
|
|
135
|
+
assert contexts == ["chat-1:child:0.1", "chat-1:child:0.2"]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.mark.asyncio
|
|
139
|
+
async def test_children_without_records_use_distinct_pool_local_chat_contexts() -> None:
|
|
140
|
+
contexts: list[str] = []
|
|
141
|
+
router = ToolRouter()
|
|
142
|
+
|
|
143
|
+
async def capture_context(context: dict | None = None) -> str:
|
|
144
|
+
chat_id = str((context or {}).get("chat_id") or "")
|
|
145
|
+
contexts.append(chat_id)
|
|
146
|
+
return chat_id
|
|
147
|
+
|
|
148
|
+
router.register(capture_context)
|
|
149
|
+
capture_schema = {
|
|
150
|
+
"type": "function",
|
|
151
|
+
"function": {"name": "capture_context", "parameters": {"type": "object"}},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async def run_one() -> None:
|
|
155
|
+
provider = make_provider(
|
|
156
|
+
[
|
|
157
|
+
{"tool_calls": [tc("delegate_subagent", {"task": "capture"})]},
|
|
158
|
+
{"tool_calls": [tc("capture_context")]},
|
|
159
|
+
{"content": "child done"},
|
|
160
|
+
{"content": "parent done"},
|
|
161
|
+
]
|
|
162
|
+
)
|
|
163
|
+
executor = SubagentExecutor(
|
|
164
|
+
RouterToolExecutor(router),
|
|
165
|
+
provider,
|
|
166
|
+
tools=[capture_schema],
|
|
167
|
+
)
|
|
168
|
+
loop = CoreLoop(provider, executor, LoopConfig())
|
|
169
|
+
[e async for e in loop.run([LLMMessage.text_of("user", "go")])]
|
|
170
|
+
await executor.shutdown()
|
|
171
|
+
|
|
172
|
+
await run_one()
|
|
173
|
+
await run_one()
|
|
174
|
+
|
|
175
|
+
assert len(contexts) == 2
|
|
176
|
+
assert contexts[0] != contexts[1]
|
|
177
|
+
assert all(context.startswith("subagent:") for context in contexts)
|
|
178
|
+
|
|
179
|
+
|
|
83
180
|
@pytest.mark.asyncio
|
|
84
181
|
async def test_child_stays_storage_free_without_a_prefix() -> None:
|
|
85
182
|
provider = make_provider(
|
|
@@ -313,7 +410,9 @@ def test_descriptor_is_openai_tool_schema() -> None:
|
|
|
313
410
|
|
|
314
411
|
def test_descriptor_with_registry_advertises_subagent_type_enum() -> None:
|
|
315
412
|
registry = SubagentRegistry()
|
|
316
|
-
registry.register(
|
|
413
|
+
registry.register(
|
|
414
|
+
"researcher", SubagentConfig(tool_filter=frozenset({"read_file"}))
|
|
415
|
+
)
|
|
317
416
|
registry.register("writer", SubagentConfig())
|
|
318
417
|
d = subagent_tool_descriptor(registry=registry)
|
|
319
418
|
prop = d["function"]["parameters"]["properties"]["subagent_type"]
|
|
@@ -351,9 +450,7 @@ async def test_registered_profile_governs_the_child() -> None:
|
|
|
351
450
|
{"content": "parent done"},
|
|
352
451
|
]
|
|
353
452
|
)
|
|
354
|
-
executor = SubagentExecutor(
|
|
355
|
-
RouterToolExecutor(router), provider, registry=registry
|
|
356
|
-
)
|
|
453
|
+
executor = SubagentExecutor(RouterToolExecutor(router), provider, registry=registry)
|
|
357
454
|
loop = CoreLoop(provider, executor, LoopConfig())
|
|
358
455
|
events = [e async for e in loop.run([LLMMessage.text_of("user", "go")])]
|
|
359
456
|
|
|
@@ -720,9 +817,7 @@ async def test_attach_pool_shares_the_orchestration_pool() -> None:
|
|
|
720
817
|
]
|
|
721
818
|
)
|
|
722
819
|
subagent = SubagentExecutor(RouterToolExecutor(ToolRouter()), provider)
|
|
723
|
-
orchestration = OrchestrationExecutor(
|
|
724
|
-
subagent, provider, OrchestrationConfig()
|
|
725
|
-
)
|
|
820
|
+
orchestration = OrchestrationExecutor(subagent, provider, OrchestrationConfig())
|
|
726
821
|
subagent.attach_pool(orchestration.pool)
|
|
727
822
|
loop = CoreLoop(provider, orchestration, LoopConfig())
|
|
728
823
|
events = [
|
|
@@ -745,9 +840,8 @@ async def test_attach_pool_shares_the_orchestration_pool() -> None:
|
|
|
745
840
|
|
|
746
841
|
@pytest.mark.asyncio
|
|
747
842
|
async def test_child_advertises_the_delegated_tool_surface() -> None:
|
|
748
|
-
"""Children see the parent
|
|
749
|
-
|
|
750
|
-
never re-delegates (depth-1 by construction, advertised honestly)."""
|
|
843
|
+
"""Children see the filtered parent tools plus parent-input coordination,
|
|
844
|
+
but never delegation itself (depth-1 by construction)."""
|
|
751
845
|
seen_tools: list[list[str] | None] = []
|
|
752
846
|
script = iter(
|
|
753
847
|
[
|
|
@@ -765,9 +859,7 @@ async def test_child_advertises_the_delegated_tool_surface() -> None:
|
|
|
765
859
|
raise NotImplementedError
|
|
766
860
|
|
|
767
861
|
def stream(self, messages, *, tools=None, **kw):
|
|
768
|
-
seen_tools.append(
|
|
769
|
-
[t["function"]["name"] for t in tools] if tools else None
|
|
770
|
-
)
|
|
862
|
+
seen_tools.append([t["function"]["name"] for t in tools] if tools else None)
|
|
771
863
|
entry = next(script)
|
|
772
864
|
|
|
773
865
|
async def _gen():
|
|
@@ -800,7 +892,131 @@ async def test_child_advertises_the_delegated_tool_surface() -> None:
|
|
|
800
892
|
# Round 1 is the parent's request (full surface); round 2 is the
|
|
801
893
|
# child's — filtered to the delegated domain.
|
|
802
894
|
assert seen_tools[0] == ["read_file", "write_file", "delegate_subagent"]
|
|
803
|
-
assert seen_tools[1] == ["read_file"]
|
|
895
|
+
assert seen_tools[1] == ["read_file", PARENT_INPUT_TOOL_NAME]
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
@pytest.mark.asyncio
|
|
899
|
+
async def test_child_uses_parent_input_protocol_instead_of_ask_user() -> None:
|
|
900
|
+
"""A child never receives the interactive ``ask_user`` tool. It always
|
|
901
|
+
receives the child-only parent-input tool, even when the profile narrows
|
|
902
|
+
the delegated host-tool domain."""
|
|
903
|
+
seen_tools: list[list[str] | None] = []
|
|
904
|
+
script = iter(
|
|
905
|
+
[
|
|
906
|
+
{"tool_calls": [tc("delegate_subagent", {"task": "prepare"})]},
|
|
907
|
+
{"content": "child done"},
|
|
908
|
+
{"content": "parent done"},
|
|
909
|
+
]
|
|
910
|
+
)
|
|
911
|
+
|
|
912
|
+
class _CapturingProvider:
|
|
913
|
+
name = "fake"
|
|
914
|
+
model = "fake-model"
|
|
915
|
+
|
|
916
|
+
async def complete(self, messages, *, tools=None, **kw):
|
|
917
|
+
raise NotImplementedError
|
|
918
|
+
|
|
919
|
+
def stream(self, messages, *, tools=None, **kw):
|
|
920
|
+
seen_tools.append([t["function"]["name"] for t in tools] if tools else None)
|
|
921
|
+
entry = next(script)
|
|
922
|
+
|
|
923
|
+
async def _gen():
|
|
924
|
+
if entry.get("content"):
|
|
925
|
+
yield LLMStreamChunk(content_delta=entry["content"])
|
|
926
|
+
for call in entry.get("tool_calls", []):
|
|
927
|
+
yield LLMStreamChunk(tool_call_delta=call)
|
|
928
|
+
yield LLMStreamChunk(
|
|
929
|
+
finish_reason="tool_calls" if entry.get("tool_calls") else "stop"
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
return _gen()
|
|
933
|
+
|
|
934
|
+
parent_tools = [
|
|
935
|
+
{"type": "function", "function": {"name": "read_file", "parameters": {}}},
|
|
936
|
+
{"type": "function", "function": {"name": "ask_user", "parameters": {}}},
|
|
937
|
+
subagent_tool_descriptor(),
|
|
938
|
+
]
|
|
939
|
+
provider = _CapturingProvider()
|
|
940
|
+
executor = SubagentExecutor(
|
|
941
|
+
RouterToolExecutor(ToolRouter()),
|
|
942
|
+
provider,
|
|
943
|
+
SubagentConfig(allow_tools=False),
|
|
944
|
+
tools=parent_tools,
|
|
945
|
+
)
|
|
946
|
+
loop = CoreLoop(provider, executor, LoopConfig())
|
|
947
|
+
[e async for e in loop.run([LLMMessage.text_of("user", "go")], tools=parent_tools)]
|
|
948
|
+
await executor.shutdown()
|
|
949
|
+
|
|
950
|
+
assert seen_tools[0] == ["read_file", "ask_user", "delegate_subagent"]
|
|
951
|
+
assert seen_tools[1] == [PARENT_INPUT_TOOL_NAME]
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
@pytest.mark.asyncio
|
|
955
|
+
async def test_parent_input_request_becomes_structured_delegation_result() -> None:
|
|
956
|
+
direct_ask_calls = 0
|
|
957
|
+
|
|
958
|
+
async def ask_user(**kwargs) -> dict:
|
|
959
|
+
nonlocal direct_ask_calls
|
|
960
|
+
direct_ask_calls += 1
|
|
961
|
+
return kwargs
|
|
962
|
+
|
|
963
|
+
router = ToolRouter()
|
|
964
|
+
router.register(ask_user)
|
|
965
|
+
questions = [
|
|
966
|
+
{
|
|
967
|
+
"id": "template",
|
|
968
|
+
"text": "选择哪种模板?",
|
|
969
|
+
"header": "模板",
|
|
970
|
+
"type": "select",
|
|
971
|
+
"options": ["标准", "精简"],
|
|
972
|
+
"multiSelect": False,
|
|
973
|
+
}
|
|
974
|
+
]
|
|
975
|
+
provider = make_provider(
|
|
976
|
+
[
|
|
977
|
+
{"tool_calls": [tc("delegate_subagent", {"task": "prepare"})]},
|
|
978
|
+
{
|
|
979
|
+
"tool_calls": [
|
|
980
|
+
tc(
|
|
981
|
+
"ask_user",
|
|
982
|
+
{"intro": "wrong path", "questions": questions},
|
|
983
|
+
)
|
|
984
|
+
]
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
"tool_calls": [
|
|
988
|
+
tc(
|
|
989
|
+
PARENT_INPUT_TOOL_NAME,
|
|
990
|
+
{"intro": "missing questions"},
|
|
991
|
+
)
|
|
992
|
+
]
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
"tool_calls": [
|
|
996
|
+
tc(
|
|
997
|
+
PARENT_INPUT_TOOL_NAME,
|
|
998
|
+
{"intro": "需要补充信息", "questions": questions},
|
|
999
|
+
)
|
|
1000
|
+
]
|
|
1001
|
+
},
|
|
1002
|
+
{"content": "parent will ask"},
|
|
1003
|
+
]
|
|
1004
|
+
)
|
|
1005
|
+
executor = SubagentExecutor(
|
|
1006
|
+
RouterToolExecutor(router),
|
|
1007
|
+
provider,
|
|
1008
|
+
tools=[parent_input_tool_descriptor()],
|
|
1009
|
+
)
|
|
1010
|
+
loop = CoreLoop(provider, executor, LoopConfig())
|
|
1011
|
+
events = [e async for e in loop.run([LLMMessage.text_of("user", "go")])]
|
|
1012
|
+
await executor.shutdown()
|
|
1013
|
+
|
|
1014
|
+
result = _tool_results(events)[0]
|
|
1015
|
+
assert result["success"] is True
|
|
1016
|
+
assert '"status": "needs_input"' in result["resultPreview"]
|
|
1017
|
+
assert "选择哪种模板?" in result["resultPreview"]
|
|
1018
|
+
assert provider._idx == 5
|
|
1019
|
+
assert direct_ask_calls == 0
|
|
804
1020
|
|
|
805
1021
|
|
|
806
1022
|
@pytest.mark.asyncio
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|