aip-agents-binary 0.0.0__py3-none-any.whl
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.
- aip_agents/__init__.py +66 -0
- aip_agents/a2a/__init__.py +19 -0
- aip_agents/a2a/server/__init__.py +10 -0
- aip_agents/a2a/server/base_executor.py +1125 -0
- aip_agents/a2a/server/google_adk_executor.py +198 -0
- aip_agents/a2a/server/langflow_executor.py +180 -0
- aip_agents/a2a/server/langgraph_executor.py +270 -0
- aip_agents/a2a/types.py +232 -0
- aip_agents/a2ui/__init__.py +26 -0
- aip_agents/a2ui/catalog_provider.py +109 -0
- aip_agents/a2ui/examples/0.8/booking_form.json +1 -0
- aip_agents/a2ui/examples/0.8/confirmation.json +1 -0
- aip_agents/a2ui/examples/0.8/single_column_list.json +1 -0
- aip_agents/a2ui/examples/0.9/booking_form.json +1 -0
- aip_agents/a2ui/examples/0.9/confirmation.json +1 -0
- aip_agents/a2ui/examples/0.9/single_column_list.json +1 -0
- aip_agents/a2ui/middleware.py +754 -0
- aip_agents/a2ui/models.py +153 -0
- aip_agents/a2ui/public_url.py +138 -0
- aip_agents/a2ui/stream_filter.py +67 -0
- aip_agents/agent/__init__.py +72 -0
- aip_agents/agent/base_agent.py +1437 -0
- aip_agents/agent/base_langgraph_agent.py +4048 -0
- aip_agents/agent/google_adk_agent.py +932 -0
- aip_agents/agent/google_adk_constants.py +11 -0
- aip_agents/agent/graph_runtime.py +185 -0
- aip_agents/agent/hitl/__init__.py +24 -0
- aip_agents/agent/hitl/config.py +28 -0
- aip_agents/agent/hitl/langgraph_hitl_mixin.py +515 -0
- aip_agents/agent/hitl/manager.py +532 -0
- aip_agents/agent/hitl/models.py +18 -0
- aip_agents/agent/hitl/prompt/__init__.py +9 -0
- aip_agents/agent/hitl/prompt/base.py +42 -0
- aip_agents/agent/hitl/prompt/deferred.py +73 -0
- aip_agents/agent/hitl/registry.py +149 -0
- aip_agents/agent/interface.py +138 -0
- aip_agents/agent/interfaces.py +65 -0
- aip_agents/agent/langflow_agent.py +464 -0
- aip_agents/agent/langgraph_react_agent.py +3741 -0
- aip_agents/agent/model_invocation_adapter.py +346 -0
- aip_agents/agent/system_instruction_context.py +34 -0
- aip_agents/agent/tool_execution_engine.py +587 -0
- aip_agents/audio_interface/__init__.py +30 -0
- aip_agents/audio_interface/audio_agent_adapter.py +254 -0
- aip_agents/audio_interface/config.py +146 -0
- aip_agents/audio_interface/errors.py +15 -0
- aip_agents/audio_interface/livekit_audio_session.py +504 -0
- aip_agents/audio_interface/livekit_realtime_audio_session.py +142 -0
- aip_agents/audio_interface/session_factory.py +40 -0
- aip_agents/clients/__init__.py +10 -0
- aip_agents/clients/langflow/__init__.py +10 -0
- aip_agents/clients/langflow/client.py +477 -0
- aip_agents/clients/langflow/types.py +18 -0
- aip_agents/common/__init__.py +5 -0
- aip_agents/common/callable_introspection.py +17 -0
- aip_agents/constants.py +23 -0
- aip_agents/context/__init__.py +5 -0
- aip_agents/context/budget.py +237 -0
- aip_agents/context/compaction.py +339 -0
- aip_agents/context/metadata.py +169 -0
- aip_agents/context/offload.py +449 -0
- aip_agents/context/preparation.py +196 -0
- aip_agents/context/prompt.py +61 -0
- aip_agents/context/runtime.py +149 -0
- aip_agents/context/session_recall.py +69 -0
- aip_agents/context/state.py +48 -0
- aip_agents/context/utils.py +44 -0
- aip_agents/credentials/manager.py +132 -0
- aip_agents/examples/__init__.py +5 -0
- aip_agents/examples/audio_livekit_session.py +116 -0
- aip_agents/examples/compare_streaming_client.py +783 -0
- aip_agents/examples/compare_streaming_server.py +141 -0
- aip_agents/examples/gl_connectors_smoke.py +148 -0
- aip_agents/examples/hello_world_a2a_google_adk_client.py +49 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_agent.py +48 -0
- aip_agents/examples/hello_world_a2a_google_adk_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_google_adk_server.py +78 -0
- aip_agents/examples/hello_world_a2a_langchain_client.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_agent.py +39 -0
- aip_agents/examples/hello_world_a2a_langchain_client_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_a2a_langchain_client_streaming.py +41 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_client_streaming.py +60 -0
- aip_agents/examples/hello_world_a2a_langchain_reference_server.py +104 -0
- aip_agents/examples/hello_world_a2a_langchain_server.py +78 -0
- aip_agents/examples/hello_world_a2a_langchain_server_lm_invoker.py +77 -0
- aip_agents/examples/hello_world_a2a_langflow_client.py +83 -0
- aip_agents/examples/hello_world_a2a_langflow_server.py +80 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client.py +73 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_client_streaming.py +76 -0
- aip_agents/examples/hello_world_a2a_langgraph_artifact_server.py +91 -0
- aip_agents/examples/hello_world_a2a_langgraph_client.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent.py +54 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_agent_lm_invoker.py +32 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming.py +50 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_lm_invoker.py +44 -0
- aip_agents/examples/hello_world_a2a_langgraph_client_streaming_tool_streaming.py +92 -0
- aip_agents/examples/hello_world_a2a_langgraph_server.py +83 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_lm_invoker.py +78 -0
- aip_agents/examples/hello_world_a2a_langgraph_server_tool_streaming.py +131 -0
- aip_agents/examples/hello_world_a2a_mcp_langgraph.py +196 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_client.py +244 -0
- aip_agents/examples/hello_world_a2a_three_level_agent_hierarchy_server.py +250 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_client.py +57 -0
- aip_agents/examples/hello_world_a2a_with_metadata_langchain_server_lm_invoker.py +79 -0
- aip_agents/examples/hello_world_filesystem.py +162 -0
- aip_agents/examples/hello_world_google_adk.py +41 -0
- aip_agents/examples/hello_world_google_adk_mcp_http.py +34 -0
- aip_agents/examples/hello_world_google_adk_mcp_http_stream.py +40 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_sse_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio.py +44 -0
- aip_agents/examples/hello_world_google_adk_mcp_stdio_stream.py +48 -0
- aip_agents/examples/hello_world_google_adk_stream.py +44 -0
- aip_agents/examples/hello_world_langchain.py +28 -0
- aip_agents/examples/hello_world_langchain_lm_invoker.py +15 -0
- aip_agents/examples/hello_world_langchain_mcp_http.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_http_interactive.py +130 -0
- aip_agents/examples/hello_world_langchain_mcp_http_stream.py +42 -0
- aip_agents/examples/hello_world_langchain_mcp_multi_server.py +155 -0
- aip_agents/examples/hello_world_langchain_mcp_sse.py +34 -0
- aip_agents/examples/hello_world_langchain_mcp_sse_stream.py +40 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio.py +30 -0
- aip_agents/examples/hello_world_langchain_mcp_stdio_stream.py +41 -0
- aip_agents/examples/hello_world_langchain_stream.py +36 -0
- aip_agents/examples/hello_world_langchain_stream_lm_invoker.py +39 -0
- aip_agents/examples/hello_world_langflow_agent.py +163 -0
- aip_agents/examples/hello_world_langgraph.py +39 -0
- aip_agents/examples/hello_world_langgraph_gl_connector_twitter.py +44 -0
- aip_agents/examples/hello_world_langgraph_mcp_http.py +31 -0
- aip_agents/examples/hello_world_langgraph_mcp_http_stream.py +34 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_sse_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio.py +35 -0
- aip_agents/examples/hello_world_langgraph_mcp_stdio_stream.py +50 -0
- aip_agents/examples/hello_world_langgraph_skills.py +62 -0
- aip_agents/examples/hello_world_langgraph_stream.py +43 -0
- aip_agents/examples/hello_world_langgraph_stream_lm_invoker.py +37 -0
- aip_agents/examples/hello_world_model_switch_cli.py +210 -0
- aip_agents/examples/hello_world_multi_agent_adk.py +75 -0
- aip_agents/examples/hello_world_multi_agent_langchain.py +54 -0
- aip_agents/examples/hello_world_multi_agent_langgraph.py +66 -0
- aip_agents/examples/hello_world_multi_agent_langgraph_lm_invoker.py +69 -0
- aip_agents/examples/hello_world_multi_agent_tool_output_client.py +57 -0
- aip_agents/examples/hello_world_multi_agent_tool_output_server.py +131 -0
- aip_agents/examples/hello_world_pii_logger.py +21 -0
- aip_agents/examples/hello_world_ptc.py +49 -0
- aip_agents/examples/hello_world_ptc_custom_tools.py +83 -0
- aip_agents/examples/hello_world_sentry.py +133 -0
- aip_agents/examples/hello_world_step_limits.py +273 -0
- aip_agents/examples/hello_world_stock_a2a_server.py +100 -0
- aip_agents/examples/hello_world_tool_output_client.py +55 -0
- aip_agents/examples/hello_world_tool_output_server.py +113 -0
- aip_agents/examples/hitl_demo.py +724 -0
- aip_agents/examples/mcp_configs/configs.py +63 -0
- aip_agents/examples/mcp_servers/common.py +76 -0
- aip_agents/examples/mcp_servers/mcp_name.py +29 -0
- aip_agents/examples/mcp_servers/mcp_server_http.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_sse.py +19 -0
- aip_agents/examples/mcp_servers/mcp_server_stdio.py +19 -0
- aip_agents/examples/mcp_servers/mcp_time.py +10 -0
- aip_agents/examples/pii_demo_langgraph_client.py +69 -0
- aip_agents/examples/pii_demo_langgraph_server.py +125 -0
- aip_agents/examples/pii_demo_multi_agent_client.py +80 -0
- aip_agents/examples/pii_demo_multi_agent_server.py +246 -0
- aip_agents/examples/todolist_planning_a2a_langchain_client.py +70 -0
- aip_agents/examples/todolist_planning_a2a_langgraph_server.py +87 -0
- aip_agents/examples/tools/__init__.py +27 -0
- aip_agents/examples/tools/adk_arithmetic_tools.py +36 -0
- aip_agents/examples/tools/adk_weather_tool.py +60 -0
- aip_agents/examples/tools/data_generator_tool.py +103 -0
- aip_agents/examples/tools/data_visualization_tool.py +312 -0
- aip_agents/examples/tools/image_artifact_tool.py +136 -0
- aip_agents/examples/tools/langchain_arithmetic_tools.py +26 -0
- aip_agents/examples/tools/langchain_currency_exchange_tool.py +88 -0
- aip_agents/examples/tools/langchain_graph_artifact_tool.py +172 -0
- aip_agents/examples/tools/langchain_weather_tool.py +48 -0
- aip_agents/examples/tools/langgraph_streaming_tool.py +130 -0
- aip_agents/examples/tools/mock_retrieval_tool.py +56 -0
- aip_agents/examples/tools/multiply_tool.py +43 -0
- aip_agents/examples/tools/pii_demo_tools.py +189 -0
- aip_agents/examples/tools/random_chart_tool.py +142 -0
- aip_agents/examples/tools/serper_tool.py +202 -0
- aip_agents/examples/tools/stock_tools.py +82 -0
- aip_agents/examples/tools/table_generator_tool.py +167 -0
- aip_agents/examples/tools/time_tool.py +82 -0
- aip_agents/examples/tools/weather_forecast_tool.py +38 -0
- aip_agents/guardrails/__init__.py +83 -0
- aip_agents/guardrails/engines/__init__.py +69 -0
- aip_agents/guardrails/engines/base.py +90 -0
- aip_agents/guardrails/engines/nemo.py +101 -0
- aip_agents/guardrails/engines/phrase_matcher.py +113 -0
- aip_agents/guardrails/exceptions.py +39 -0
- aip_agents/guardrails/manager.py +163 -0
- aip_agents/guardrails/middleware.py +199 -0
- aip_agents/guardrails/schemas.py +63 -0
- aip_agents/guardrails/utils.py +45 -0
- aip_agents/integration/__init__.py +43 -0
- aip_agents/integration/agent.py +58 -0
- aip_agents/integration/guardrails.py +52 -0
- aip_agents/integration/hitl.py +71 -0
- aip_agents/integration/ptc.py +95 -0
- aip_agents/integration/skills.py +37 -0
- aip_agents/integration/storage.py +81 -0
- aip_agents/integration/version.py +103 -0
- aip_agents/mcp/__init__.py +1 -0
- aip_agents/mcp/client/__init__.py +60 -0
- aip_agents/mcp/client/base_mcp_client.py +377 -0
- aip_agents/mcp/client/connection_manager.py +231 -0
- aip_agents/mcp/client/errors.py +119 -0
- aip_agents/mcp/client/google_adk/__init__.py +45 -0
- aip_agents/mcp/client/google_adk/client.py +383 -0
- aip_agents/mcp/client/langchain/__init__.py +11 -0
- aip_agents/mcp/client/langchain/client.py +265 -0
- aip_agents/mcp/client/persistent_session.py +704 -0
- aip_agents/mcp/client/session_pool.py +358 -0
- aip_agents/mcp/client/transports.py +288 -0
- aip_agents/mcp/utils/__init__.py +7 -0
- aip_agents/mcp/utils/config_validator.py +139 -0
- aip_agents/memory/__init__.py +14 -0
- aip_agents/memory/adapters/__init__.py +10 -0
- aip_agents/memory/adapters/base_adapter.py +815 -0
- aip_agents/memory/adapters/mem0.py +286 -0
- aip_agents/memory/base.py +84 -0
- aip_agents/memory/constants.py +49 -0
- aip_agents/memory/factory.py +86 -0
- aip_agents/memory/guidance.py +20 -0
- aip_agents/memory/simple_memory.py +47 -0
- aip_agents/middleware/__init__.py +65 -0
- aip_agents/middleware/backends/__init__.py +72 -0
- aip_agents/middleware/backends/in_memory.py +578 -0
- aip_agents/middleware/backends/local_disk.py +1203 -0
- aip_agents/middleware/backends/protocol.py +353 -0
- aip_agents/middleware/backends/sandbox.py +1431 -0
- aip_agents/middleware/backends/sandbox_files.py +113 -0
- aip_agents/middleware/backends/utils.py +293 -0
- aip_agents/middleware/base.py +239 -0
- aip_agents/middleware/filesystem.py +690 -0
- aip_agents/middleware/manager.py +291 -0
- aip_agents/middleware/memory.py +532 -0
- aip_agents/middleware/pii_middleware.py +192 -0
- aip_agents/middleware/schema.py +19 -0
- aip_agents/middleware/skills.py +872 -0
- aip_agents/middleware/task_delegation.py +1656 -0
- aip_agents/middleware/todolist.py +276 -0
- aip_agents/middleware/tools/__init__.py +23 -0
- aip_agents/middleware/tools/edit_file.py +70 -0
- aip_agents/middleware/tools/execute.py +428 -0
- aip_agents/middleware/tools/grep_file.py +79 -0
- aip_agents/middleware/tools/ls.py +71 -0
- aip_agents/middleware/tools/read_file.py +239 -0
- aip_agents/middleware/tools/skill_load.py +193 -0
- aip_agents/middleware/tools/skill_resource.py +338 -0
- aip_agents/middleware/tools/write_file.py +114 -0
- aip_agents/middleware/vision.py +322 -0
- aip_agents/ptc/__init__.py +87 -0
- aip_agents/ptc/custom_tools.py +511 -0
- aip_agents/ptc/custom_tools_payload.py +499 -0
- aip_agents/ptc/custom_tools_templates/__init__.py +1 -0
- aip_agents/ptc/custom_tools_templates/custom_build_function.py.template +24 -0
- aip_agents/ptc/custom_tools_templates/custom_init.py.template +26 -0
- aip_agents/ptc/custom_tools_templates/custom_invoke.py.template +149 -0
- aip_agents/ptc/custom_tools_templates/custom_registry.py.template +112 -0
- aip_agents/ptc/custom_tools_templates/custom_sources_init.py.template +7 -0
- aip_agents/ptc/custom_tools_templates/custom_wrapper.py.template +87 -0
- aip_agents/ptc/doc_gen.py +122 -0
- aip_agents/ptc/exceptions.py +57 -0
- aip_agents/ptc/executor.py +627 -0
- aip_agents/ptc/mcp/__init__.py +45 -0
- aip_agents/ptc/mcp/sandbox_bridge.py +679 -0
- aip_agents/ptc/mcp/templates/__init__.py +1 -0
- aip_agents/ptc/mcp/templates/mcp_client.py.template +240 -0
- aip_agents/ptc/naming.py +430 -0
- aip_agents/ptc/payload.py +26 -0
- aip_agents/ptc/prompt_builder.py +674 -0
- aip_agents/ptc/ptc_helper.py +16 -0
- aip_agents/ptc/sandbox_bridge.py +297 -0
- aip_agents/ptc/template_utils.py +33 -0
- aip_agents/ptc/templates/__init__.py +1 -0
- aip_agents/ptc/templates/ptc_helper.py.template +134 -0
- aip_agents/ptc/tool_def_helpers.py +101 -0
- aip_agents/ptc/tool_enrichment.py +163 -0
- aip_agents/resilience/__init__.py +10 -0
- aip_agents/resilience/base.py +46 -0
- aip_agents/resilience/tool.py +1661 -0
- aip_agents/sandbox/__init__.py +62 -0
- aip_agents/sandbox/defaults.py +220 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1-datasaur.Dockerfile +120 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-1.Dockerfile +100 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2-datasaur.Dockerfile +143 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-2.Dockerfile +119 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3-datasaur.Dockerfile +173 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-3.Dockerfile +130 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.Dockerfile +164 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.patch_js_context.py +41 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2-4-runner-managed.requirements.txt +29 -0
- aip_agents/sandbox/dockerfiles/aip-agents-sandbox-v2.Dockerfile +87 -0
- aip_agents/sandbox/e2b_runtime.py +27 -0
- aip_agents/sandbox/image_registry.py +98 -0
- aip_agents/sandbox/providers/__init__.py +76 -0
- aip_agents/sandbox/providers/base.py +392 -0
- aip_agents/sandbox/providers/bedrock.py +28 -0
- aip_agents/sandbox/providers/e2b.py +230 -0
- aip_agents/sandbox/providers/opensandbox.py +163 -0
- aip_agents/sandbox/sandbox_runtime.py +967 -0
- aip_agents/sandbox/template_builder.py +240 -0
- aip_agents/sandbox/types.py +141 -0
- aip_agents/sandbox/validation.py +50 -0
- aip_agents/schema/__init__.py +106 -0
- aip_agents/schema/a2a.py +58 -0
- aip_agents/schema/agent.py +117 -0
- aip_agents/schema/artifacts.py +75 -0
- aip_agents/schema/attachments.py +425 -0
- aip_agents/schema/hitl.py +157 -0
- aip_agents/schema/langgraph.py +47 -0
- aip_agents/schema/model_id.py +103 -0
- aip_agents/schema/model_usage.py +50 -0
- aip_agents/schema/runtime_metadata.py +78 -0
- aip_agents/schema/step_limit.py +108 -0
- aip_agents/schema/storage.py +40 -0
- aip_agents/schema/tool_payloads.py +68 -0
- aip_agents/sentry/__init__.py +11 -0
- aip_agents/sentry/sentry.py +267 -0
- aip_agents/skills/__init__.py +30 -0
- aip_agents/skills/errors.py +110 -0
- aip_agents/skills/installer.py +479 -0
- aip_agents/skills/models.py +769 -0
- aip_agents/skills/stager.py +224 -0
- aip_agents/skills/validation.py +53 -0
- aip_agents/storage/__init__.py +41 -0
- aip_agents/storage/base.py +85 -0
- aip_agents/storage/clients/__init__.py +12 -0
- aip_agents/storage/clients/minio_client.py +318 -0
- aip_agents/storage/config.py +62 -0
- aip_agents/storage/providers/__init__.py +15 -0
- aip_agents/storage/providers/base.py +106 -0
- aip_agents/storage/providers/memory.py +114 -0
- aip_agents/storage/providers/object_storage.py +214 -0
- aip_agents/tools/__init__.py +65 -0
- aip_agents/tools/agent_schedule_tool.py +392 -0
- aip_agents/tools/browser_use/__init__.py +83 -0
- aip_agents/tools/browser_use/action_parser.py +104 -0
- aip_agents/tools/browser_use/browser_use_tool.py +1118 -0
- aip_agents/tools/browser_use/llm_config.py +120 -0
- aip_agents/tools/browser_use/minio_storage.py +203 -0
- aip_agents/tools/browser_use/schemas.py +121 -0
- aip_agents/tools/browser_use/session.py +13 -0
- aip_agents/tools/browser_use/session_errors.py +132 -0
- aip_agents/tools/browser_use/steel_session_recording.py +321 -0
- aip_agents/tools/browser_use/streaming.py +263 -0
- aip_agents/tools/browser_use/structured_data_parser.py +257 -0
- aip_agents/tools/browser_use/structured_data_recovery.py +204 -0
- aip_agents/tools/browser_use/types.py +37 -0
- aip_agents/tools/code_sandbox/__init__.py +48 -0
- aip_agents/tools/code_sandbox/code_sandbox_tool.py +350 -0
- aip_agents/tools/code_sandbox/constant.py +13 -0
- aip_agents/tools/code_sandbox/e2b_cloud_sandbox_extended.py +346 -0
- aip_agents/tools/code_sandbox/e2b_sandbox_tool.py +65 -0
- aip_agents/tools/computer_use/__init__.py +75 -0
- aip_agents/tools/computer_use/computer_use_tool.py +910 -0
- aip_agents/tools/computer_use/schemas.py +78 -0
- aip_agents/tools/computer_use/streaming.py +198 -0
- aip_agents/tools/computer_use/types.py +32 -0
- aip_agents/tools/constants.py +177 -0
- aip_agents/tools/date_range_tool.py +554 -0
- aip_agents/tools/document_loader/__init__.py +44 -0
- aip_agents/tools/document_loader/base_reader.py +302 -0
- aip_agents/tools/document_loader/docx_reader_tool.py +68 -0
- aip_agents/tools/document_loader/excel_reader_tool.py +171 -0
- aip_agents/tools/document_loader/pdf_reader_tool.py +79 -0
- aip_agents/tools/document_loader/pdf_splitter.py +169 -0
- aip_agents/tools/execute_ptc_code.py +582 -0
- aip_agents/tools/gl_connector/__init__.py +5 -0
- aip_agents/tools/gl_connector/tool.py +518 -0
- aip_agents/tools/gl_connector_tools.py +170 -0
- aip_agents/tools/memory_search/__init__.py +29 -0
- aip_agents/tools/memory_search/base.py +213 -0
- aip_agents/tools/memory_search/mem0.py +424 -0
- aip_agents/tools/memory_search/schema.py +81 -0
- aip_agents/tools/memory_search_tool.py +34 -0
- aip_agents/tools/streaming_common.py +577 -0
- aip_agents/tools/time_tool.py +117 -0
- aip_agents/tools/tool_config_injector.py +300 -0
- aip_agents/tools/web_search/__init__.py +15 -0
- aip_agents/tools/web_search/serper_tool.py +187 -0
- aip_agents/types/__init__.py +73 -0
- aip_agents/types/a2a_events.py +13 -0
- aip_agents/utils/__init__.py +91 -0
- aip_agents/utils/a2a_connector.py +1758 -0
- aip_agents/utils/artifact_helpers.py +542 -0
- aip_agents/utils/artifacts/__init__.py +20 -0
- aip_agents/utils/artifacts/tracker.py +367 -0
- aip_agents/utils/attachment_helpers.py +178 -0
- aip_agents/utils/constants.py +22 -0
- aip_agents/utils/datetime/__init__.py +34 -0
- aip_agents/utils/datetime/normalization.py +231 -0
- aip_agents/utils/datetime/timezone.py +206 -0
- aip_agents/utils/env_loader.py +27 -0
- aip_agents/utils/event_handler_registry.py +58 -0
- aip_agents/utils/file_prompt_utils.py +276 -0
- aip_agents/utils/file_watchers/__init__.py +33 -0
- aip_agents/utils/file_watchers/base.py +46 -0
- aip_agents/utils/file_watchers/e2b.py +128 -0
- aip_agents/utils/file_watchers/factory.py +39 -0
- aip_agents/utils/file_watchers/ls_diff.py +88 -0
- aip_agents/utils/file_watchers/sandbox.py +66 -0
- aip_agents/utils/file_watchers/watchdog.py +97 -0
- aip_agents/utils/final_response_builder.py +211 -0
- aip_agents/utils/formatter_llm_client.py +258 -0
- aip_agents/utils/langgraph/__init__.py +19 -0
- aip_agents/utils/langgraph/converter.py +399 -0
- aip_agents/utils/langgraph/delegation_context.py +22 -0
- aip_agents/utils/langgraph/tool_managers/__init__.py +15 -0
- aip_agents/utils/langgraph/tool_managers/a2a_tool_manager.py +99 -0
- aip_agents/utils/langgraph/tool_managers/base_tool_manager.py +66 -0
- aip_agents/utils/langgraph/tool_managers/delegation_tool_manager.py +1329 -0
- aip_agents/utils/langgraph/tool_output_management.py +1047 -0
- aip_agents/utils/logger.py +195 -0
- aip_agents/utils/metadata/__init__.py +27 -0
- aip_agents/utils/metadata/activity_metadata_helper.py +407 -0
- aip_agents/utils/metadata/activity_narrative/__init__.py +35 -0
- aip_agents/utils/metadata/activity_narrative/builder.py +822 -0
- aip_agents/utils/metadata/activity_narrative/constants.py +51 -0
- aip_agents/utils/metadata/activity_narrative/context.py +49 -0
- aip_agents/utils/metadata/activity_narrative/formatters.py +230 -0
- aip_agents/utils/metadata/activity_narrative/utils.py +35 -0
- aip_agents/utils/metadata/schemas/__init__.py +16 -0
- aip_agents/utils/metadata/schemas/activity_schema.py +29 -0
- aip_agents/utils/metadata/schemas/thinking_schema.py +31 -0
- aip_agents/utils/metadata/thinking_metadata_helper.py +38 -0
- aip_agents/utils/metadata_helper.py +363 -0
- aip_agents/utils/name_preprocessor/__init__.py +17 -0
- aip_agents/utils/name_preprocessor/base_name_preprocessor.py +73 -0
- aip_agents/utils/name_preprocessor/google_name_preprocessor.py +100 -0
- aip_agents/utils/name_preprocessor/name_preprocessor.py +87 -0
- aip_agents/utils/name_preprocessor/openai_name_preprocessor.py +48 -0
- aip_agents/utils/pii/__init__.py +29 -0
- aip_agents/utils/pii/pii_handler.py +555 -0
- aip_agents/utils/pii/pii_helper.py +225 -0
- aip_agents/utils/pii/uuid_deanonymizer_mapping.py +195 -0
- aip_agents/utils/reference_helper.py +273 -0
- aip_agents/utils/sse_chunk_transformer.py +836 -0
- aip_agents/utils/step_limit_manager.py +265 -0
- aip_agents/utils/token_usage_helper.py +531 -0
- aip_agents_binary-0.0.0.dist-info/METADATA +825 -0
- aip_agents_binary-0.0.0.dist-info/RECORD +447 -0
- aip_agents_binary-0.0.0.dist-info/WHEEL +5 -0
- aip_agents_binary-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1656 @@
|
|
|
1
|
+
"""Task delegation middleware for dynamic worker spawning.
|
|
2
|
+
|
|
3
|
+
This module provides a ``task`` tool that allows a parent agent to delegate
|
|
4
|
+
one bounded sub-task to a worker agent at runtime, while enforcing delegation
|
|
5
|
+
depth, concurrency, and tool-scope constraints.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import threading
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from collections.abc import AsyncIterator
|
|
18
|
+
from contextvars import ContextVar
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any, cast
|
|
21
|
+
|
|
22
|
+
from langchain_core.runnables import RunnableConfig
|
|
23
|
+
from langchain_core.tools import ArgsSchema, BaseTool
|
|
24
|
+
from langgraph.config import get_stream_writer
|
|
25
|
+
from langgraph.types import Command, StreamWriter
|
|
26
|
+
from pydantic import BaseModel, Field, SkipValidation
|
|
27
|
+
|
|
28
|
+
from aip_agents.middleware.base import AgentMiddleware, ModelRequest
|
|
29
|
+
from aip_agents.middleware.schema import MiddlewareToolConfig
|
|
30
|
+
from aip_agents.schema.step_limit import StepLimitConfig
|
|
31
|
+
from aip_agents.utils.langgraph.tool_managers.delegation_tool_manager import (
|
|
32
|
+
DELEGATED_AGENT_TIMEOUT_CONFIG_ERROR_KEY,
|
|
33
|
+
INVALID_DELEGATED_AGENT_TIMEOUT_MESSAGE,
|
|
34
|
+
)
|
|
35
|
+
from aip_agents.utils.logger import get_logger
|
|
36
|
+
from aip_agents.utils.metadata_helper import MetadataFieldKeys, get_next_step_number
|
|
37
|
+
from aip_agents.utils.pii.pii_helper import extract_pii_mapping_from_agent_response
|
|
38
|
+
from aip_agents.utils.step_limit_manager import (
|
|
39
|
+
_DELEGATION_CHAIN_CVAR,
|
|
40
|
+
_DELEGATION_DEPTH_CVAR,
|
|
41
|
+
_REMAINING_STEP_BUDGET_CVAR,
|
|
42
|
+
_STEP_LIMIT_CONFIG_CVAR,
|
|
43
|
+
StepLimitManager,
|
|
44
|
+
)
|
|
45
|
+
from aip_agents.utils.token_usage_helper import (
|
|
46
|
+
MODEL_USAGE_DELTA_KEY,
|
|
47
|
+
MODEL_USAGE_KEY,
|
|
48
|
+
STEP_USAGE_KEY,
|
|
49
|
+
TOTAL_USAGE_KEY,
|
|
50
|
+
USAGE_METADATA_KEY,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
logger = get_logger(__name__)
|
|
54
|
+
|
|
55
|
+
TASK_DELEGATION_SYSTEM_PROMPT = (
|
|
56
|
+
"You are a Coordinator using the `task` tool for bounded delegation.\n"
|
|
57
|
+
"Default strategy: decompose multi-part or research-heavy requests into delegated tracks, then synthesize worker outputs.\n"
|
|
58
|
+
"When a request has independent sections or subtasks, delegate each section with `task(...)` before final synthesis.\n"
|
|
59
|
+
"Keep parent execution focused on orchestration, validation, and final artifact assembly.\n"
|
|
60
|
+
"After delegating a track, do not repeat materially equivalent tool workflows in the parent; issue a follow-up task instead.\n"
|
|
61
|
+
"Write task calls with explicit objective, scope, constraints, and output format."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
WORKER_DELEGATION_NOT_ALLOWED = "worker_delegation_not_allowed"
|
|
65
|
+
WORKER_CONCURRENCY_LIMIT_REACHED = "worker concurrency limit reached"
|
|
66
|
+
UNKNOWN_OR_DISALLOWED_TOOLS = "unknown_or_disallowed_tools"
|
|
67
|
+
INSUFFICIENT_STEP_BUDGET = "insufficient_step_budget"
|
|
68
|
+
DEFAULT_MAX_CONCURRENT_WORKERS = 5
|
|
69
|
+
DEFAULT_MAX_PENDING_WORKERS = 20
|
|
70
|
+
MAX_ERROR_MESSAGE_LENGTH = 400
|
|
71
|
+
ALLOWED_TOOL_NAME_PREFIXES = ("functions.", "function.", "tools.")
|
|
72
|
+
EVENT_TYPE_TOOL_CALL = "tool_call"
|
|
73
|
+
EVENT_TYPE_TOOL_RESULT = "tool_result"
|
|
74
|
+
EVENT_TYPE_FINAL_RESPONSE = "final_response"
|
|
75
|
+
EVENT_TYPE_CONTENT_CHUNK = "content_chunk"
|
|
76
|
+
EVENT_TYPE_STATUS_UPDATE = "status_update"
|
|
77
|
+
SUB_AGENT_EVENT_METADATA_KEY = "is_sub_agent_event"
|
|
78
|
+
DELEGATION_METADATA_KEY = "delegation_worker_timeout"
|
|
79
|
+
DELEGATION_FAILED_STATUS = "failed"
|
|
80
|
+
DELEGATION_TOOL_TIMEOUT_REASON = "tool_timeout"
|
|
81
|
+
SUB_AGENT_TERMINAL_EVENT_REMAP = {
|
|
82
|
+
EVENT_TYPE_FINAL_RESPONSE: EVENT_TYPE_CONTENT_CHUNK,
|
|
83
|
+
"error": EVENT_TYPE_STATUS_UPDATE,
|
|
84
|
+
"step_limit_exceeded": EVENT_TYPE_STATUS_UPDATE,
|
|
85
|
+
}
|
|
86
|
+
METADATA_INTERNAL_PREFIXES = ("__", "langgraph_", "langchain_")
|
|
87
|
+
METADATA_INTERNAL_KEYS = {"step_id", "previous_step_ids", "agent_name"}
|
|
88
|
+
_TASK_SUB_START_STEP_CVAR: ContextVar[dict[str, str] | None] = ContextVar("_task_sub_start_step_cvar", default=None)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TaskDelegationConfig(BaseModel):
|
|
92
|
+
"""Configuration for dynamic task delegation middleware."""
|
|
93
|
+
|
|
94
|
+
max_concurrent_workers: int = DEFAULT_MAX_CONCURRENT_WORKERS
|
|
95
|
+
max_pending_workers: int = DEFAULT_MAX_PENDING_WORKERS
|
|
96
|
+
timeout_seconds: float | None = None
|
|
97
|
+
|
|
98
|
+
def create_middleware(self, parent_agent: Any) -> TaskDelegationMiddleware:
|
|
99
|
+
"""Create task delegation middleware for one parent agent.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
TaskDelegationMiddleware: Configured middleware instance.
|
|
103
|
+
"""
|
|
104
|
+
return TaskDelegationMiddleware(
|
|
105
|
+
parent_agent=parent_agent,
|
|
106
|
+
max_concurrent_workers=self.max_concurrent_workers,
|
|
107
|
+
max_pending_workers=self.max_pending_workers,
|
|
108
|
+
worker_timeout_seconds=self.timeout_seconds,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TaskToolSchema(BaseModel):
|
|
113
|
+
"""Schema for task tool inputs.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
query (str): Delegated task mission text.
|
|
117
|
+
prompt (str | None): Optional additional instruction context.
|
|
118
|
+
allowed_tools (list[str] | None): Optional narrowing-only allowlist.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
query: str = Field(..., description="Delegated task mission text")
|
|
122
|
+
prompt: str | None = Field(default=None, description="Optional extra instruction context")
|
|
123
|
+
allowed_tools: list[str] | None = Field(default=None, description="Optional narrowing-only tool allowlist")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TaskResult(BaseModel):
|
|
127
|
+
"""Deterministic task execution result.
|
|
128
|
+
|
|
129
|
+
Attributes:
|
|
130
|
+
success (bool): True when delegated execution completed successfully.
|
|
131
|
+
result (str): Worker output text, or empty string on failure.
|
|
132
|
+
error (str | None): Failure reason when ``success`` is False.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
success: bool
|
|
136
|
+
result: str
|
|
137
|
+
error: str | None = None
|
|
138
|
+
worker_response: dict[str, Any] | None = Field(default=None, exclude=True, repr=False)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class WorkerExecutionPlan:
|
|
143
|
+
"""Precomputed worker execution inputs used by ``run_task``.
|
|
144
|
+
|
|
145
|
+
Attributes:
|
|
146
|
+
worker_tools (list[BaseTool]): Effective toolset for worker execution.
|
|
147
|
+
filesystem_backend (Any | None): Shared filesystem backend when enabled.
|
|
148
|
+
use_shared_filesystem_middleware (bool): Whether worker should reuse parent
|
|
149
|
+
filesystem middleware backend.
|
|
150
|
+
child_budget (int): Derived worker step budget.
|
|
151
|
+
worker_instruction (str): Final worker system prompt.
|
|
152
|
+
worker_kwargs (dict[str, Any]): Child runnable kwargs.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
worker_tools: list[BaseTool]
|
|
156
|
+
filesystem_backend: Any | None
|
|
157
|
+
use_shared_filesystem_middleware: bool
|
|
158
|
+
child_budget: int
|
|
159
|
+
worker_instruction: str
|
|
160
|
+
worker_kwargs: dict[str, Any]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TaskDelegationTool(BaseTool):
|
|
164
|
+
"""LangChain-compatible ``task`` tool wrapper.
|
|
165
|
+
|
|
166
|
+
Attributes:
|
|
167
|
+
name (str): Tool name exposed to the model.
|
|
168
|
+
description (str): Tool behavior summary and argument contract.
|
|
169
|
+
args_schema (ArgsSchema): Input schema used for argument validation.
|
|
170
|
+
middleware (TaskDelegationMiddleware): Owning middleware instance.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
name: str = "task"
|
|
174
|
+
description: str = (
|
|
175
|
+
"Spawn an ephemeral worker for one delegated task. "
|
|
176
|
+
"Arguments: query (required), prompt (optional), allowed_tools (optional narrowing-only)."
|
|
177
|
+
)
|
|
178
|
+
args_schema: ArgsSchema = TaskToolSchema
|
|
179
|
+
tool_config_schema: type[BaseModel] = MiddlewareToolConfig
|
|
180
|
+
middleware: SkipValidation[TaskDelegationMiddleware] = Field(exclude=True)
|
|
181
|
+
metadata: dict[str, Any] | None = Field(default=None)
|
|
182
|
+
|
|
183
|
+
def _run(
|
|
184
|
+
self,
|
|
185
|
+
query: str,
|
|
186
|
+
prompt: str | None = None,
|
|
187
|
+
allowed_tools: list[str] | None = None,
|
|
188
|
+
config: RunnableConfig | None = None,
|
|
189
|
+
) -> str | dict[str, Any] | Command:
|
|
190
|
+
"""Execute delegated task synchronously.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
query (str): Delegated task mission text.
|
|
194
|
+
prompt (str | None, optional): Additional worker instructions.
|
|
195
|
+
Defaults to None.
|
|
196
|
+
allowed_tools (list[str] | None, optional): Narrowing-only allowlist
|
|
197
|
+
for worker tools. Defaults to None.
|
|
198
|
+
config (RunnableConfig | None, optional): Runnable execution config.
|
|
199
|
+
Defaults to None.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
str | dict[str, Any] | Command: Delegation-style tool output.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
asyncio.get_running_loop()
|
|
206
|
+
except RuntimeError:
|
|
207
|
+
return asyncio.run(self._arun(query=query, prompt=prompt, allowed_tools=allowed_tools, config=config))
|
|
208
|
+
|
|
209
|
+
return self._run_arun_in_thread(
|
|
210
|
+
query=query,
|
|
211
|
+
prompt=prompt,
|
|
212
|
+
allowed_tools=allowed_tools,
|
|
213
|
+
config=config,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _run_arun_in_thread(
|
|
217
|
+
self,
|
|
218
|
+
*,
|
|
219
|
+
query: str,
|
|
220
|
+
prompt: str | None,
|
|
221
|
+
allowed_tools: list[str] | None,
|
|
222
|
+
config: RunnableConfig | None,
|
|
223
|
+
) -> str | dict[str, Any] | Command:
|
|
224
|
+
"""Execute async task delegation from sync context with active event loop.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
query (str): Delegated task mission text.
|
|
228
|
+
prompt (str | None): Optional additional worker instructions.
|
|
229
|
+
allowed_tools (list[str] | None): Optional narrowing-only allowlist.
|
|
230
|
+
config (RunnableConfig | None): Runnable execution config.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
str | dict[str, Any] | Command: Delegation-style tool output.
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
RuntimeError: If thread execution returns no result.
|
|
237
|
+
Exception: Re-raises exceptions captured from async execution.
|
|
238
|
+
"""
|
|
239
|
+
result_holder: list[str | dict[str, Any] | Command] = []
|
|
240
|
+
error_holder: list[Exception] = []
|
|
241
|
+
|
|
242
|
+
def _runner() -> None:
|
|
243
|
+
try:
|
|
244
|
+
result_holder.append(
|
|
245
|
+
asyncio.run(self._arun(query=query, prompt=prompt, allowed_tools=allowed_tools, config=config))
|
|
246
|
+
)
|
|
247
|
+
except Exception as exc:
|
|
248
|
+
error_holder.append(exc)
|
|
249
|
+
|
|
250
|
+
thread = threading.Thread(target=_runner, daemon=True)
|
|
251
|
+
thread.start()
|
|
252
|
+
thread.join()
|
|
253
|
+
|
|
254
|
+
if error_holder:
|
|
255
|
+
raise error_holder[0]
|
|
256
|
+
if not result_holder:
|
|
257
|
+
raise RuntimeError("TaskDelegationTool._run produced no result")
|
|
258
|
+
|
|
259
|
+
return result_holder[0]
|
|
260
|
+
|
|
261
|
+
async def _arun(
|
|
262
|
+
self,
|
|
263
|
+
query: str,
|
|
264
|
+
prompt: str | None = None,
|
|
265
|
+
allowed_tools: list[str] | None = None,
|
|
266
|
+
config: RunnableConfig | None = None,
|
|
267
|
+
) -> str | dict[str, Any] | Command:
|
|
268
|
+
"""Execute delegated task asynchronously.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
query (str): Delegated task mission text.
|
|
272
|
+
prompt (str | None, optional): Additional worker instructions.
|
|
273
|
+
Defaults to None.
|
|
274
|
+
allowed_tools (list[str] | None, optional): Narrowing-only allowlist
|
|
275
|
+
for worker tools. Defaults to None.
|
|
276
|
+
config (RunnableConfig | None, optional): Runnable execution config.
|
|
277
|
+
Defaults to None.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
str | dict[str, Any] | Command: Delegation-style tool output.
|
|
281
|
+
"""
|
|
282
|
+
result = await self.middleware.run_task(
|
|
283
|
+
query=query,
|
|
284
|
+
prompt=prompt,
|
|
285
|
+
allowed_tools=allowed_tools,
|
|
286
|
+
config=config,
|
|
287
|
+
stream_writer=self._resolve_stream_writer(),
|
|
288
|
+
)
|
|
289
|
+
if not result.success:
|
|
290
|
+
return result.error or "task delegation failed"
|
|
291
|
+
|
|
292
|
+
if not isinstance(result.worker_response, dict):
|
|
293
|
+
return result.result
|
|
294
|
+
|
|
295
|
+
return self._format_task_response_with_extras(result.result, result.worker_response)
|
|
296
|
+
|
|
297
|
+
def _format_task_response_with_extras(
|
|
298
|
+
self,
|
|
299
|
+
text_response: str,
|
|
300
|
+
worker_response: dict[str, Any],
|
|
301
|
+
) -> str | Command:
|
|
302
|
+
"""Format worker response into delegation-compatible tool output.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
text_response (str): Worker text output.
|
|
306
|
+
worker_response (dict[str, Any]): Worker response payload.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
str | Command: Plain text when no extras are present; otherwise
|
|
310
|
+
``Command(update=...)`` with metadata and artifacts.
|
|
311
|
+
"""
|
|
312
|
+
full_state = worker_response.get("full_final_state")
|
|
313
|
+
if not isinstance(full_state, dict):
|
|
314
|
+
return text_response
|
|
315
|
+
|
|
316
|
+
metadata = full_state.get("metadata")
|
|
317
|
+
metadata_update = self.middleware._filter_worker_metadata(metadata) if isinstance(metadata, dict) else {}
|
|
318
|
+
|
|
319
|
+
total_usage = full_state.get("total_usage")
|
|
320
|
+
token_usage = total_usage if isinstance(total_usage, dict) and total_usage else None
|
|
321
|
+
|
|
322
|
+
references = full_state.get("references")
|
|
323
|
+
references_list = references if isinstance(references, list) else []
|
|
324
|
+
|
|
325
|
+
artifacts = full_state.get("artifacts")
|
|
326
|
+
artifacts_list = artifacts if isinstance(artifacts, list) else []
|
|
327
|
+
|
|
328
|
+
pii_mapping = extract_pii_mapping_from_agent_response(worker_response)
|
|
329
|
+
|
|
330
|
+
has_extras = any((metadata_update, token_usage, references_list, artifacts_list, pii_mapping))
|
|
331
|
+
if not has_extras:
|
|
332
|
+
return text_response
|
|
333
|
+
|
|
334
|
+
update_dict: dict[str, Any] = {"result": text_response}
|
|
335
|
+
if metadata_update:
|
|
336
|
+
update_dict["metadata"] = metadata_update
|
|
337
|
+
if token_usage:
|
|
338
|
+
update_dict[USAGE_METADATA_KEY] = token_usage
|
|
339
|
+
if references_list:
|
|
340
|
+
update_dict[MetadataFieldKeys.REFERENCES] = references_list
|
|
341
|
+
if artifacts_list:
|
|
342
|
+
update_dict["artifacts"] = artifacts_list
|
|
343
|
+
if pii_mapping:
|
|
344
|
+
update_dict[MetadataFieldKeys.PII_MAPPING] = pii_mapping
|
|
345
|
+
|
|
346
|
+
return Command(update=update_dict)
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _resolve_stream_writer() -> StreamWriter:
|
|
350
|
+
"""Resolve current LangGraph stream writer with no-op fallback.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
StreamWriter: Active stream writer or fallback no-op callable.
|
|
354
|
+
"""
|
|
355
|
+
try:
|
|
356
|
+
return cast(StreamWriter, get_stream_writer())
|
|
357
|
+
except Exception:
|
|
358
|
+
logger.debug("task_delegation_stream_writer_unavailable", exc_info=True)
|
|
359
|
+
|
|
360
|
+
def _noop_writer(_: Any) -> None:
|
|
361
|
+
"""Fallback no-op stream writer used outside streaming context."""
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
return _noop_writer
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class TaskDelegationMiddleware(AgentMiddleware):
|
|
368
|
+
"""Middleware that injects runtime task delegation support.
|
|
369
|
+
|
|
370
|
+
The middleware adds a ``task`` tool and enforces bounded worker delegation.
|
|
371
|
+
It prevents nested worker delegation, applies queue/concurrency limits, and
|
|
372
|
+
narrows worker tools when an allowlist is provided.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
def __init__(
|
|
376
|
+
self,
|
|
377
|
+
parent_agent: Any,
|
|
378
|
+
*,
|
|
379
|
+
max_concurrent_workers: int = DEFAULT_MAX_CONCURRENT_WORKERS,
|
|
380
|
+
max_pending_workers: int = DEFAULT_MAX_PENDING_WORKERS,
|
|
381
|
+
worker_timeout_seconds: float | None = None,
|
|
382
|
+
) -> None:
|
|
383
|
+
"""Initialize task delegation middleware.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
parent_agent (Any): Parent agent instance that owns this middleware.
|
|
387
|
+
max_concurrent_workers (int, optional): Maximum concurrently running
|
|
388
|
+
workers. Defaults to 5.
|
|
389
|
+
max_pending_workers (int, optional): Maximum pending worker requests
|
|
390
|
+
allowed in addition to active workers. Defaults to 20.
|
|
391
|
+
worker_timeout_seconds (float | None, optional): Parent-owned timeout
|
|
392
|
+
budget for each dynamic worker run. Defaults to None.
|
|
393
|
+
|
|
394
|
+
Raises:
|
|
395
|
+
ValueError: If concurrency or pending limits are invalid.
|
|
396
|
+
"""
|
|
397
|
+
if max_concurrent_workers < 1:
|
|
398
|
+
raise ValueError("max_concurrent_workers must be >= 1")
|
|
399
|
+
if max_pending_workers < 0:
|
|
400
|
+
raise ValueError("max_pending_workers must be >= 0")
|
|
401
|
+
|
|
402
|
+
self.parent_agent = parent_agent
|
|
403
|
+
self.max_concurrent_workers = max_concurrent_workers
|
|
404
|
+
self.max_pending_workers = max_pending_workers
|
|
405
|
+
self.worker_timeout_seconds = worker_timeout_seconds
|
|
406
|
+
self.system_prompt_additions = TASK_DELEGATION_SYSTEM_PROMPT
|
|
407
|
+
|
|
408
|
+
self._semaphore = asyncio.Semaphore(max_concurrent_workers)
|
|
409
|
+
self._pending_lock = asyncio.Lock()
|
|
410
|
+
self._pending_workers = 0
|
|
411
|
+
self._active_workers = 0
|
|
412
|
+
|
|
413
|
+
self.tools: list[BaseTool] = [TaskDelegationTool(middleware=self, metadata=self._build_task_tool_metadata())]
|
|
414
|
+
|
|
415
|
+
def _build_task_tool_metadata(self) -> dict[str, Any]:
|
|
416
|
+
"""Build delegation-style timeout metadata for the dynamic task tool.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
dict[str, Any]: Tool metadata consumed by delegation timeout policy.
|
|
420
|
+
"""
|
|
421
|
+
metadata: dict[str, Any] = {
|
|
422
|
+
"is_delegation_tool": True,
|
|
423
|
+
"delegated_agent_name": f"{self.parent_agent.name}_task_worker",
|
|
424
|
+
"tool_type": "delegation",
|
|
425
|
+
"delegation_manager": self.__class__.__name__,
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if self.worker_timeout_seconds is None:
|
|
429
|
+
return metadata
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
timeout_seconds = float(self.worker_timeout_seconds)
|
|
433
|
+
except (TypeError, ValueError):
|
|
434
|
+
metadata[DELEGATED_AGENT_TIMEOUT_CONFIG_ERROR_KEY] = INVALID_DELEGATED_AGENT_TIMEOUT_MESSAGE
|
|
435
|
+
return metadata
|
|
436
|
+
|
|
437
|
+
if timeout_seconds <= 0:
|
|
438
|
+
metadata[DELEGATED_AGENT_TIMEOUT_CONFIG_ERROR_KEY] = INVALID_DELEGATED_AGENT_TIMEOUT_MESSAGE
|
|
439
|
+
return metadata
|
|
440
|
+
|
|
441
|
+
metadata["delegated_agent_timeout_seconds"] = timeout_seconds
|
|
442
|
+
return metadata
|
|
443
|
+
|
|
444
|
+
def before_model(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
445
|
+
"""No-op hook before model invocation.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
state (dict[str, Any]): Current graph state.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
dict[str, Any]: Empty update.
|
|
452
|
+
"""
|
|
453
|
+
return {}
|
|
454
|
+
|
|
455
|
+
def modify_model_request(self, request: ModelRequest, state: dict[str, Any]) -> ModelRequest:
|
|
456
|
+
"""Pass through model request unchanged.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
request (ModelRequest): Outgoing model request payload.
|
|
460
|
+
state (dict[str, Any]): Current graph state.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
ModelRequest: Unmodified request.
|
|
464
|
+
"""
|
|
465
|
+
return request
|
|
466
|
+
|
|
467
|
+
def after_model(self, state: dict[str, Any]) -> dict[str, Any]:
|
|
468
|
+
"""No-op hook after model invocation.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
state (dict[str, Any]): Current graph state.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
dict[str, Any]: Empty update.
|
|
475
|
+
"""
|
|
476
|
+
return {}
|
|
477
|
+
|
|
478
|
+
async def run_task(
|
|
479
|
+
self,
|
|
480
|
+
*,
|
|
481
|
+
query: str,
|
|
482
|
+
prompt: str | None,
|
|
483
|
+
allowed_tools: list[str] | None,
|
|
484
|
+
config: RunnableConfig | None,
|
|
485
|
+
stream_writer: StreamWriter | None = None,
|
|
486
|
+
) -> TaskResult:
|
|
487
|
+
"""Run one delegated worker task with safety constraints.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
query (str): Delegated task mission text.
|
|
491
|
+
prompt (str | None): Optional additional worker instructions.
|
|
492
|
+
allowed_tools (list[str] | None): Optional narrowing-only allowlist.
|
|
493
|
+
config (RunnableConfig | None): Runnable execution config.
|
|
494
|
+
stream_writer (StreamWriter | None, optional): Parent stream writer
|
|
495
|
+
receiving forwarded sub-agent events. Defaults to None.
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
TaskResult: Execution result with success flag and error context.
|
|
499
|
+
|
|
500
|
+
Raises:
|
|
501
|
+
None: All internal failures are converted into ``TaskResult`` with
|
|
502
|
+
``success=False``.
|
|
503
|
+
"""
|
|
504
|
+
start_time = time.perf_counter()
|
|
505
|
+
task_id = uuid.uuid4().hex[:12]
|
|
506
|
+
queue_reserved = False
|
|
507
|
+
slot_acquired = False
|
|
508
|
+
sub_start_token = _TASK_SUB_START_STEP_CVAR.set({})
|
|
509
|
+
|
|
510
|
+
try:
|
|
511
|
+
preflight_failure = await self._validate_task_entry(task_id, start_time)
|
|
512
|
+
if preflight_failure is not None:
|
|
513
|
+
return preflight_failure
|
|
514
|
+
queue_reserved = True
|
|
515
|
+
|
|
516
|
+
concurrency_slot = await self._acquire_worker_slot()
|
|
517
|
+
slot_acquired = True
|
|
518
|
+
|
|
519
|
+
execution_plan_or_failure = self._build_worker_execution_plan(
|
|
520
|
+
task_id=task_id,
|
|
521
|
+
prompt=prompt,
|
|
522
|
+
allowed_tools=allowed_tools,
|
|
523
|
+
config=config,
|
|
524
|
+
start_time=start_time,
|
|
525
|
+
)
|
|
526
|
+
if isinstance(execution_plan_or_failure, TaskResult):
|
|
527
|
+
return execution_plan_or_failure
|
|
528
|
+
|
|
529
|
+
execution_plan = execution_plan_or_failure
|
|
530
|
+
self._log_worker_spawn(task_id, query, execution_plan, concurrency_slot)
|
|
531
|
+
return await self._execute_worker_task(
|
|
532
|
+
task_id=task_id,
|
|
533
|
+
query=query,
|
|
534
|
+
execution_plan=execution_plan,
|
|
535
|
+
stream_writer=stream_writer,
|
|
536
|
+
start_time=start_time,
|
|
537
|
+
)
|
|
538
|
+
finally:
|
|
539
|
+
if slot_acquired:
|
|
540
|
+
await self._release_worker_slot()
|
|
541
|
+
elif queue_reserved:
|
|
542
|
+
await self._release_pending_slot()
|
|
543
|
+
_TASK_SUB_START_STEP_CVAR.reset(sub_start_token)
|
|
544
|
+
|
|
545
|
+
async def _validate_task_entry(self, task_id: str, start_time: float) -> TaskResult | None:
|
|
546
|
+
"""Validate delegation entry constraints before worker acquisition.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
task_id (str): Delegation task identifier.
|
|
550
|
+
start_time (float): Perf counter timestamp at task start.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
TaskResult | None: Failure result when entry checks fail; otherwise
|
|
554
|
+
``None``.
|
|
555
|
+
"""
|
|
556
|
+
current_depth = _DELEGATION_DEPTH_CVAR.get() or 0
|
|
557
|
+
if current_depth > 0:
|
|
558
|
+
return self._failure(task_id, WORKER_DELEGATION_NOT_ALLOWED, start_time)
|
|
559
|
+
|
|
560
|
+
if not await self._reserve_queue_slot():
|
|
561
|
+
return self._failure(task_id, WORKER_CONCURRENCY_LIMIT_REACHED, start_time)
|
|
562
|
+
|
|
563
|
+
return None
|
|
564
|
+
|
|
565
|
+
async def _acquire_worker_slot(self) -> int:
|
|
566
|
+
"""Acquire semaphore and convert one pending slot into active execution.
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
int: Active worker count after acquisition.
|
|
570
|
+
"""
|
|
571
|
+
semaphore_acquired = False
|
|
572
|
+
try:
|
|
573
|
+
await self._semaphore.acquire()
|
|
574
|
+
semaphore_acquired = True
|
|
575
|
+
async with self._pending_lock:
|
|
576
|
+
self._pending_workers = max(0, self._pending_workers - 1)
|
|
577
|
+
self._active_workers += 1
|
|
578
|
+
return self._active_workers
|
|
579
|
+
except asyncio.CancelledError:
|
|
580
|
+
if semaphore_acquired:
|
|
581
|
+
self._semaphore.release()
|
|
582
|
+
raise
|
|
583
|
+
|
|
584
|
+
async def _release_pending_slot(self) -> None:
|
|
585
|
+
"""Release one reserved pending slot when worker acquisition is aborted."""
|
|
586
|
+
async with self._pending_lock:
|
|
587
|
+
self._pending_workers = max(0, self._pending_workers - 1)
|
|
588
|
+
|
|
589
|
+
async def _release_worker_slot(self) -> None:
|
|
590
|
+
"""Release one active worker slot and return semaphore permit."""
|
|
591
|
+
async with self._pending_lock:
|
|
592
|
+
self._active_workers = max(0, self._active_workers - 1)
|
|
593
|
+
self._semaphore.release()
|
|
594
|
+
|
|
595
|
+
def _build_worker_execution_plan(
|
|
596
|
+
self,
|
|
597
|
+
*,
|
|
598
|
+
task_id: str,
|
|
599
|
+
prompt: str | None,
|
|
600
|
+
allowed_tools: list[str] | None,
|
|
601
|
+
config: RunnableConfig | None,
|
|
602
|
+
start_time: float,
|
|
603
|
+
) -> WorkerExecutionPlan | TaskResult:
|
|
604
|
+
"""Build deterministic worker execution inputs.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
task_id (str): Delegation task identifier.
|
|
608
|
+
prompt (str | None): Optional additional worker instructions.
|
|
609
|
+
allowed_tools (list[str] | None): Optional narrowing-only allowlist.
|
|
610
|
+
config (RunnableConfig | None): Parent runnable config.
|
|
611
|
+
start_time (float): Perf counter timestamp at task start.
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
WorkerExecutionPlan | TaskResult: Worker plan on success, otherwise
|
|
615
|
+
a failure ``TaskResult``.
|
|
616
|
+
"""
|
|
617
|
+
worker_tools, tools_error = self._resolve_worker_tools(allowed_tools)
|
|
618
|
+
if tools_error is not None:
|
|
619
|
+
return self._failure(task_id, tools_error, start_time)
|
|
620
|
+
|
|
621
|
+
filesystem_backend, filesystem_tool_names = self._get_parent_filesystem_context()
|
|
622
|
+
use_shared_filesystem_middleware = filesystem_backend is not None and allowed_tools is None
|
|
623
|
+
if use_shared_filesystem_middleware and filesystem_tool_names:
|
|
624
|
+
worker_tools = self._exclude_parent_filesystem_tools(worker_tools, filesystem_tool_names)
|
|
625
|
+
|
|
626
|
+
child_budget = self._derive_child_budget()
|
|
627
|
+
if child_budget <= 0:
|
|
628
|
+
return self._failure(task_id, INSUFFICIENT_STEP_BUDGET, start_time)
|
|
629
|
+
|
|
630
|
+
return WorkerExecutionPlan(
|
|
631
|
+
worker_tools=worker_tools,
|
|
632
|
+
filesystem_backend=filesystem_backend,
|
|
633
|
+
use_shared_filesystem_middleware=use_shared_filesystem_middleware,
|
|
634
|
+
child_budget=child_budget,
|
|
635
|
+
worker_instruction=self._build_worker_instruction(prompt),
|
|
636
|
+
worker_kwargs=self._build_child_run_kwargs(config),
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
@staticmethod
|
|
640
|
+
def _exclude_parent_filesystem_tools(
|
|
641
|
+
worker_tools: list[BaseTool], filesystem_tool_names: set[str]
|
|
642
|
+
) -> list[BaseTool]:
|
|
643
|
+
"""Remove filesystem tools already provided by shared middleware.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
worker_tools (list[BaseTool]): Candidate worker tools.
|
|
647
|
+
filesystem_tool_names (set[str]): Parent filesystem tool names.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
list[BaseTool]: Filtered worker toolset.
|
|
651
|
+
"""
|
|
652
|
+
return [
|
|
653
|
+
tool
|
|
654
|
+
for tool in worker_tools
|
|
655
|
+
if isinstance(getattr(tool, "name", None), str) and getattr(tool, "name") not in filesystem_tool_names
|
|
656
|
+
]
|
|
657
|
+
|
|
658
|
+
def _log_worker_spawn(
|
|
659
|
+
self,
|
|
660
|
+
task_id: str,
|
|
661
|
+
query: str,
|
|
662
|
+
execution_plan: WorkerExecutionPlan,
|
|
663
|
+
concurrency_slot: int,
|
|
664
|
+
) -> None:
|
|
665
|
+
"""Emit deterministic worker spawn telemetry.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
task_id (str): Delegation task identifier.
|
|
669
|
+
query (str): Delegated task mission text.
|
|
670
|
+
execution_plan (WorkerExecutionPlan): Planned worker execution inputs.
|
|
671
|
+
concurrency_slot (int): Active worker slot number.
|
|
672
|
+
"""
|
|
673
|
+
logger.info(
|
|
674
|
+
"task_delegation_spawn task_id=%s parent_thread_id=%s tools_count=%s query_length=%s concurrency_slot=%s",
|
|
675
|
+
task_id,
|
|
676
|
+
execution_plan.worker_kwargs.get("configurable", {}).get(self.parent_agent.thread_id_key),
|
|
677
|
+
len(execution_plan.worker_tools),
|
|
678
|
+
len(query),
|
|
679
|
+
concurrency_slot,
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
async def _execute_worker_task(
|
|
683
|
+
self,
|
|
684
|
+
*,
|
|
685
|
+
task_id: str,
|
|
686
|
+
query: str,
|
|
687
|
+
execution_plan: WorkerExecutionPlan,
|
|
688
|
+
stream_writer: StreamWriter | None,
|
|
689
|
+
start_time: float,
|
|
690
|
+
) -> TaskResult:
|
|
691
|
+
"""Execute worker agent using a precomputed execution plan.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
task_id (str): Delegation task identifier.
|
|
695
|
+
query (str): Delegated task mission text.
|
|
696
|
+
execution_plan (WorkerExecutionPlan): Worker execution inputs.
|
|
697
|
+
stream_writer (StreamWriter | None): Optional parent stream writer.
|
|
698
|
+
start_time (float): Perf counter timestamp at task start.
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
TaskResult: Success or failure result.
|
|
702
|
+
"""
|
|
703
|
+
try:
|
|
704
|
+
from aip_agents.agent.langgraph_react_agent import LangGraphReactAgent
|
|
705
|
+
|
|
706
|
+
worker_model = self.parent_agent.model
|
|
707
|
+
if worker_model is None:
|
|
708
|
+
worker_model = self.parent_agent.lm_invoker
|
|
709
|
+
|
|
710
|
+
worker = LangGraphReactAgent(
|
|
711
|
+
name=f"{self.parent_agent.name}_worker_{task_id}",
|
|
712
|
+
instruction=execution_plan.worker_instruction,
|
|
713
|
+
model=worker_model,
|
|
714
|
+
tools=execution_plan.worker_tools,
|
|
715
|
+
filesystem=(
|
|
716
|
+
execution_plan.filesystem_backend if execution_plan.use_shared_filesystem_middleware else None
|
|
717
|
+
),
|
|
718
|
+
task_delegation=False,
|
|
719
|
+
step_limit_config=StepLimitConfig(max_steps=max(1, execution_plan.child_budget)),
|
|
720
|
+
)
|
|
721
|
+
response = await self._run_worker_with_stream_trace(
|
|
722
|
+
worker,
|
|
723
|
+
query=query,
|
|
724
|
+
worker_kwargs=execution_plan.worker_kwargs,
|
|
725
|
+
stream_writer=stream_writer,
|
|
726
|
+
)
|
|
727
|
+
worker_output = response.get("output", "") if isinstance(response, dict) else str(response)
|
|
728
|
+
result = TaskResult(
|
|
729
|
+
success=True,
|
|
730
|
+
result=str(worker_output),
|
|
731
|
+
error=None,
|
|
732
|
+
worker_response=response if isinstance(response, dict) else None,
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
logger.debug(
|
|
736
|
+
"task_delegation_output task_id=%s worker_response=%r",
|
|
737
|
+
task_id,
|
|
738
|
+
response,
|
|
739
|
+
)
|
|
740
|
+
return self._success(task_id, result, start_time)
|
|
741
|
+
except asyncio.CancelledError:
|
|
742
|
+
raise
|
|
743
|
+
except Exception as exc:
|
|
744
|
+
logger.exception("task_delegation_exception task_id=%s error=%s", task_id, str(exc))
|
|
745
|
+
return self._failure(task_id, str(exc), start_time)
|
|
746
|
+
|
|
747
|
+
async def _reserve_queue_slot(self) -> bool:
|
|
748
|
+
"""Reserve a bounded slot for pending work.
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
bool: True when combined active and pending capacity remains
|
|
752
|
+
available, False when delegation capacity is saturated.
|
|
753
|
+
"""
|
|
754
|
+
async with self._pending_lock:
|
|
755
|
+
if self._active_workers + self._pending_workers >= self.max_concurrent_workers + self.max_pending_workers:
|
|
756
|
+
return False
|
|
757
|
+
self._pending_workers += 1
|
|
758
|
+
return True
|
|
759
|
+
|
|
760
|
+
def _derive_child_budget(self) -> int:
|
|
761
|
+
"""Compute worker step budget from current delegation context.
|
|
762
|
+
|
|
763
|
+
Returns:
|
|
764
|
+
int: Child step budget. Values less than or equal to zero are
|
|
765
|
+
considered insufficient for worker execution.
|
|
766
|
+
"""
|
|
767
|
+
current_depth = _DELEGATION_DEPTH_CVAR.get() or 0
|
|
768
|
+
current_chain = list(_DELEGATION_CHAIN_CVAR.get() or ())
|
|
769
|
+
remaining_budget = _REMAINING_STEP_BUDGET_CVAR.get()
|
|
770
|
+
parent_config = _STEP_LIMIT_CONFIG_CVAR.get()
|
|
771
|
+
|
|
772
|
+
manager = StepLimitManager(
|
|
773
|
+
config=parent_config,
|
|
774
|
+
initial_delegation_depth=current_depth,
|
|
775
|
+
parent_step_budget=remaining_budget,
|
|
776
|
+
)
|
|
777
|
+
manager.context.delegation_chain = current_chain
|
|
778
|
+
return manager.get_child_budget()
|
|
779
|
+
|
|
780
|
+
def _resolve_worker_tools(self, allowed_tools: list[str] | None) -> tuple[list[BaseTool], str | None]:
|
|
781
|
+
"""Resolve effective worker tools under narrowing constraints.
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
allowed_tools (list[str] | None): Optional narrowing-only allowlist.
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
tuple[list[BaseTool], str | None]: A tuple of ``(tools, error)``.
|
|
788
|
+
``error`` is ``None`` on success; otherwise it contains a
|
|
789
|
+
deterministic error message.
|
|
790
|
+
"""
|
|
791
|
+
candidate_tools = self._collect_candidate_worker_tools()
|
|
792
|
+
|
|
793
|
+
tool_by_name = {tool.name: tool for tool in candidate_tools}
|
|
794
|
+
|
|
795
|
+
if allowed_tools is None:
|
|
796
|
+
return candidate_tools, None
|
|
797
|
+
|
|
798
|
+
canonical_names, unknown = self._resolve_requested_allowed_tool_names(allowed_tools, tool_by_name)
|
|
799
|
+
|
|
800
|
+
if unknown:
|
|
801
|
+
return [], f"{UNKNOWN_OR_DISALLOWED_TOOLS}: {', '.join(unknown)}"
|
|
802
|
+
|
|
803
|
+
return [tool_by_name[name] for name in canonical_names], None
|
|
804
|
+
|
|
805
|
+
def _collect_candidate_worker_tools(self) -> list[BaseTool]:
|
|
806
|
+
"""Collect parent tools eligible for worker execution.
|
|
807
|
+
|
|
808
|
+
Returns:
|
|
809
|
+
list[BaseTool]: Parent tools excluding delegation entries.
|
|
810
|
+
"""
|
|
811
|
+
resolved_tools = getattr(self.parent_agent, "resolved_tools", []) or []
|
|
812
|
+
candidate_tools: list[BaseTool] = []
|
|
813
|
+
for tool in resolved_tools:
|
|
814
|
+
if self._is_worker_eligible_tool(tool):
|
|
815
|
+
candidate_tools.append(tool)
|
|
816
|
+
return candidate_tools
|
|
817
|
+
|
|
818
|
+
@staticmethod
|
|
819
|
+
def _is_worker_eligible_tool(tool: Any) -> bool:
|
|
820
|
+
"""Return whether one parent tool is eligible for worker runs.
|
|
821
|
+
|
|
822
|
+
Args:
|
|
823
|
+
tool (Any): Candidate tool.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
bool: True when the tool is non-delegation with valid name.
|
|
827
|
+
"""
|
|
828
|
+
tool_name = getattr(tool, "name", "")
|
|
829
|
+
if not isinstance(tool_name, str) or not tool_name:
|
|
830
|
+
return False
|
|
831
|
+
if tool_name == "task" or tool_name.startswith("delegate_to_"):
|
|
832
|
+
return False
|
|
833
|
+
tool_metadata = getattr(tool, "metadata", None)
|
|
834
|
+
return not (isinstance(tool_metadata, dict) and tool_metadata.get("is_delegation_tool") is True)
|
|
835
|
+
|
|
836
|
+
def _resolve_requested_allowed_tool_names(
|
|
837
|
+
self,
|
|
838
|
+
allowed_tools: list[str],
|
|
839
|
+
tool_by_name: dict[str, BaseTool],
|
|
840
|
+
) -> tuple[list[str], list[str]]:
|
|
841
|
+
"""Resolve requested allowlist names into canonical tool names.
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
allowed_tools (list[str]): Requested tool names.
|
|
845
|
+
tool_by_name (dict[str, BaseTool]): Candidate tools keyed by canonical name.
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
tuple[list[str], list[str]]: Canonical names and unknown names.
|
|
849
|
+
"""
|
|
850
|
+
canonical_names: list[str] = []
|
|
851
|
+
unknown: list[str] = []
|
|
852
|
+
seen: set[str] = set()
|
|
853
|
+
|
|
854
|
+
normalized_names = [name for name in allowed_tools if isinstance(name, str) and name]
|
|
855
|
+
for requested_name in normalized_names:
|
|
856
|
+
canonical_name = self._normalize_allowed_tool_name(requested_name, tool_by_name)
|
|
857
|
+
if canonical_name is None:
|
|
858
|
+
unknown.append(requested_name)
|
|
859
|
+
continue
|
|
860
|
+
if canonical_name in seen:
|
|
861
|
+
continue # pragma: no cover
|
|
862
|
+
seen.add(canonical_name)
|
|
863
|
+
canonical_names.append(canonical_name)
|
|
864
|
+
return canonical_names, unknown
|
|
865
|
+
|
|
866
|
+
@staticmethod
|
|
867
|
+
def _normalize_allowed_tool_name(requested_name: str, tool_by_name: dict[str, BaseTool]) -> str | None:
|
|
868
|
+
"""Normalize requested tool name against known tool map.
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
requested_name (str): Raw tool name requested by caller.
|
|
872
|
+
tool_by_name (dict[str, BaseTool]): Candidate tools keyed by name.
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
str | None: Canonical tool name or ``None`` when not resolvable.
|
|
876
|
+
"""
|
|
877
|
+
if requested_name in tool_by_name:
|
|
878
|
+
return requested_name
|
|
879
|
+
|
|
880
|
+
for prefix in ALLOWED_TOOL_NAME_PREFIXES:
|
|
881
|
+
if requested_name.startswith(prefix):
|
|
882
|
+
stripped_name = requested_name[len(prefix) :]
|
|
883
|
+
if stripped_name in tool_by_name:
|
|
884
|
+
return stripped_name
|
|
885
|
+
|
|
886
|
+
return None
|
|
887
|
+
|
|
888
|
+
def _get_parent_filesystem_context(self) -> tuple[Any | None, set[str]]:
|
|
889
|
+
"""Return parent filesystem backend and filesystem tool names.
|
|
890
|
+
|
|
891
|
+
Returns:
|
|
892
|
+
tuple[Any | None, set[str]]: Parent filesystem backend (if any) and
|
|
893
|
+
the set of filesystem tool names exposed by parent middleware.
|
|
894
|
+
"""
|
|
895
|
+
backend = getattr(self.parent_agent, "_filesystem_backend", None)
|
|
896
|
+
middleware = getattr(self.parent_agent, "_filesystem_middleware", None)
|
|
897
|
+
|
|
898
|
+
if backend is None or middleware is None:
|
|
899
|
+
return None, set()
|
|
900
|
+
|
|
901
|
+
names: set[str] = set()
|
|
902
|
+
for tool in getattr(middleware, "tools", []) or []:
|
|
903
|
+
name = getattr(tool, "name", None)
|
|
904
|
+
if isinstance(name, str) and name:
|
|
905
|
+
names.add(name)
|
|
906
|
+
|
|
907
|
+
return backend, names
|
|
908
|
+
|
|
909
|
+
def _build_worker_instruction(self, prompt: str | None) -> str:
|
|
910
|
+
"""Build worker instruction text.
|
|
911
|
+
|
|
912
|
+
Args:
|
|
913
|
+
prompt (str | None): Optional additional instruction context.
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
str: Worker system instruction.
|
|
917
|
+
"""
|
|
918
|
+
base = (
|
|
919
|
+
f"You are a Worker for parent agent '{self.parent_agent.name}'.\n"
|
|
920
|
+
"Execute exactly one delegated track with bounded tools and limits.\n"
|
|
921
|
+
"Focus on direct execution and concise, source-backed output for the requested track.\n"
|
|
922
|
+
"Do not delegate further tasks."
|
|
923
|
+
)
|
|
924
|
+
if prompt:
|
|
925
|
+
return f"{base}\n\nTASK INSTRUCTION CONTEXT:\n{prompt}"
|
|
926
|
+
return base
|
|
927
|
+
|
|
928
|
+
def _build_child_run_kwargs(self, config: RunnableConfig | None) -> dict[str, Any]:
|
|
929
|
+
"""Build worker run kwargs from parent config.
|
|
930
|
+
|
|
931
|
+
Args:
|
|
932
|
+
config (RunnableConfig | None): Parent runnable config.
|
|
933
|
+
|
|
934
|
+
Returns:
|
|
935
|
+
dict[str, Any]: Child run kwargs with ``configurable.thread_id`` and
|
|
936
|
+
optional metadata subset.
|
|
937
|
+
"""
|
|
938
|
+
thread_key = getattr(self.parent_agent, "thread_id_key", "thread_id")
|
|
939
|
+
parent_thread_id = self._resolve_parent_thread_id(config, thread_key)
|
|
940
|
+
metadata_subset = self._extract_child_metadata_subset(config)
|
|
941
|
+
|
|
942
|
+
run_kwargs: dict[str, Any] = {
|
|
943
|
+
"configurable": {thread_key: parent_thread_id},
|
|
944
|
+
}
|
|
945
|
+
if metadata_subset:
|
|
946
|
+
run_kwargs["metadata"] = metadata_subset
|
|
947
|
+
|
|
948
|
+
return run_kwargs
|
|
949
|
+
|
|
950
|
+
def _resolve_parent_thread_id(self, config: RunnableConfig | None, thread_key: str) -> str:
|
|
951
|
+
"""Resolve parent thread id from config, parent state, or fallback uuid.
|
|
952
|
+
|
|
953
|
+
Args:
|
|
954
|
+
config (RunnableConfig | None): Parent runnable config.
|
|
955
|
+
thread_key (str): Configurable thread-id key.
|
|
956
|
+
|
|
957
|
+
Returns:
|
|
958
|
+
str: Parent thread id.
|
|
959
|
+
"""
|
|
960
|
+
config_thread_id = self._extract_config_thread_id(config, thread_key)
|
|
961
|
+
if config_thread_id is not None:
|
|
962
|
+
return config_thread_id
|
|
963
|
+
|
|
964
|
+
parent_thread_candidate = getattr(self.parent_agent, thread_key, None)
|
|
965
|
+
if isinstance(parent_thread_candidate, str) and parent_thread_candidate:
|
|
966
|
+
return parent_thread_candidate
|
|
967
|
+
return str(uuid.uuid4())
|
|
968
|
+
|
|
969
|
+
@staticmethod
|
|
970
|
+
def _extract_config_thread_id(config: RunnableConfig | None, thread_key: str) -> str | None:
|
|
971
|
+
"""Extract thread id from runnable config configurable section.
|
|
972
|
+
|
|
973
|
+
Args:
|
|
974
|
+
config (RunnableConfig | None): Parent runnable config.
|
|
975
|
+
thread_key (str): Thread key to lookup.
|
|
976
|
+
|
|
977
|
+
Returns:
|
|
978
|
+
str | None: Config thread id when present and non-empty.
|
|
979
|
+
"""
|
|
980
|
+
if not isinstance(config, dict):
|
|
981
|
+
return None
|
|
982
|
+
configurable = config.get("configurable")
|
|
983
|
+
if not isinstance(configurable, dict):
|
|
984
|
+
return None # pragma: no cover
|
|
985
|
+
candidate = configurable.get(thread_key)
|
|
986
|
+
if isinstance(candidate, str) and candidate:
|
|
987
|
+
return candidate
|
|
988
|
+
return None # pragma: no cover
|
|
989
|
+
|
|
990
|
+
@staticmethod
|
|
991
|
+
def _extract_child_metadata_subset(config: RunnableConfig | None) -> dict[str, Any]:
|
|
992
|
+
"""Extract safe metadata keys forwarded to worker run kwargs.
|
|
993
|
+
|
|
994
|
+
Args:
|
|
995
|
+
config (RunnableConfig | None): Parent runnable config.
|
|
996
|
+
|
|
997
|
+
Returns:
|
|
998
|
+
dict[str, Any]: Metadata subset containing trace/request/run ids.
|
|
999
|
+
"""
|
|
1000
|
+
if not isinstance(config, dict):
|
|
1001
|
+
return {}
|
|
1002
|
+
metadata = config.get("metadata")
|
|
1003
|
+
if not isinstance(metadata, dict):
|
|
1004
|
+
return {}
|
|
1005
|
+
|
|
1006
|
+
metadata_subset: dict[str, Any] = {}
|
|
1007
|
+
for key in ("trace_id", "request_id", "run_id"):
|
|
1008
|
+
value = metadata.get(key)
|
|
1009
|
+
if value is not None:
|
|
1010
|
+
metadata_subset[key] = value
|
|
1011
|
+
return metadata_subset
|
|
1012
|
+
|
|
1013
|
+
@staticmethod
|
|
1014
|
+
def _filter_worker_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
|
|
1015
|
+
"""Filter worker metadata keys before propagating to parent update.
|
|
1016
|
+
|
|
1017
|
+
Args:
|
|
1018
|
+
metadata (dict[str, Any]): Raw worker metadata dictionary.
|
|
1019
|
+
|
|
1020
|
+
Returns:
|
|
1021
|
+
dict[str, Any]: Filtered metadata preserving safe linkage fields.
|
|
1022
|
+
"""
|
|
1023
|
+
filtered: dict[str, Any] = {
|
|
1024
|
+
key: value
|
|
1025
|
+
for key, value in metadata.items()
|
|
1026
|
+
if not any(key.startswith(prefix) for prefix in METADATA_INTERNAL_PREFIXES)
|
|
1027
|
+
and key not in METADATA_INTERNAL_KEYS
|
|
1028
|
+
}
|
|
1029
|
+
previous_step_ids = metadata.get("previous_step_ids")
|
|
1030
|
+
if isinstance(previous_step_ids, list) and previous_step_ids:
|
|
1031
|
+
filtered["previous_step_ids"] = list(previous_step_ids)
|
|
1032
|
+
step_id = metadata.get("step_id")
|
|
1033
|
+
if step_id:
|
|
1034
|
+
filtered.setdefault("step_id", step_id)
|
|
1035
|
+
return filtered
|
|
1036
|
+
|
|
1037
|
+
def _success(self, task_id: str, result: TaskResult, start_time: float) -> TaskResult:
|
|
1038
|
+
"""Log and return a successful task result.
|
|
1039
|
+
|
|
1040
|
+
Args:
|
|
1041
|
+
task_id (str): Delegation task identifier.
|
|
1042
|
+
result (TaskResult): Successful result payload.
|
|
1043
|
+
start_time (float): Perf counter timestamp at task start.
|
|
1044
|
+
|
|
1045
|
+
Returns:
|
|
1046
|
+
TaskResult: Unmodified successful result.
|
|
1047
|
+
"""
|
|
1048
|
+
duration_ms = int((time.perf_counter() - start_time) * 1000)
|
|
1049
|
+
logger.info(
|
|
1050
|
+
"task_delegation_result task_id=%s success=%s duration_ms=%s",
|
|
1051
|
+
task_id,
|
|
1052
|
+
result.success,
|
|
1053
|
+
duration_ms,
|
|
1054
|
+
)
|
|
1055
|
+
return result
|
|
1056
|
+
|
|
1057
|
+
def _failure(self, task_id: str, error: str, start_time: float) -> TaskResult:
|
|
1058
|
+
"""Log and return a failure task result.
|
|
1059
|
+
|
|
1060
|
+
Args:
|
|
1061
|
+
task_id (str): Delegation task identifier.
|
|
1062
|
+
error (str): Failure reason text.
|
|
1063
|
+
start_time (float): Perf counter timestamp at task start.
|
|
1064
|
+
|
|
1065
|
+
Returns:
|
|
1066
|
+
TaskResult: Failure result with bounded error text.
|
|
1067
|
+
"""
|
|
1068
|
+
bounded_error = error[:MAX_ERROR_MESSAGE_LENGTH]
|
|
1069
|
+
duration_ms = int((time.perf_counter() - start_time) * 1000)
|
|
1070
|
+
logger.error(
|
|
1071
|
+
"task_delegation_result task_id=%s success=%s duration_ms=%s error=%s",
|
|
1072
|
+
task_id,
|
|
1073
|
+
False,
|
|
1074
|
+
duration_ms,
|
|
1075
|
+
bounded_error,
|
|
1076
|
+
)
|
|
1077
|
+
return TaskResult(success=False, result="", error=bounded_error)
|
|
1078
|
+
|
|
1079
|
+
async def _run_worker_with_stream_trace(
|
|
1080
|
+
self,
|
|
1081
|
+
worker: Any,
|
|
1082
|
+
*,
|
|
1083
|
+
query: str,
|
|
1084
|
+
worker_kwargs: dict[str, Any],
|
|
1085
|
+
stream_writer: StreamWriter | None,
|
|
1086
|
+
) -> dict[str, Any]:
|
|
1087
|
+
"""Execute worker stream and collect normalized final response payload.
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
worker (Any): Worker agent exposing ``arun_a2a_stream``.
|
|
1091
|
+
query (str): Delegated task mission text.
|
|
1092
|
+
worker_kwargs (dict[str, Any]): Child runnable kwargs.
|
|
1093
|
+
stream_writer (StreamWriter | None): Optional parent stream writer.
|
|
1094
|
+
|
|
1095
|
+
Returns:
|
|
1096
|
+
dict[str, Any]: Worker response with ``output`` and ``full_final_state``.
|
|
1097
|
+
|
|
1098
|
+
Raises:
|
|
1099
|
+
RuntimeError: If worker stream API is missing, invalid, or lacks
|
|
1100
|
+
``final_response`` event.
|
|
1101
|
+
"""
|
|
1102
|
+
stream_method = getattr(worker, "arun_a2a_stream", None)
|
|
1103
|
+
if not callable(stream_method):
|
|
1104
|
+
raise RuntimeError("Worker does not support arun_a2a_stream; task delegation requires stream mode")
|
|
1105
|
+
|
|
1106
|
+
stream = stream_method(query, **worker_kwargs)
|
|
1107
|
+
if not hasattr(stream, "__aiter__"):
|
|
1108
|
+
raise RuntimeError("Worker stream is not async-iterable; task delegation requires async stream mode")
|
|
1109
|
+
async_stream = cast(AsyncIterator[Any], stream)
|
|
1110
|
+
|
|
1111
|
+
stream_records: list[dict[str, Any]] = []
|
|
1112
|
+
final_event: dict[str, Any] | None = None
|
|
1113
|
+
parent_thread_id = self._extract_parent_thread_id(worker_kwargs)
|
|
1114
|
+
|
|
1115
|
+
async for chunk in async_stream:
|
|
1116
|
+
if not isinstance(chunk, dict):
|
|
1117
|
+
continue
|
|
1118
|
+
normalized = self._normalize_worker_stream_chunk(chunk)
|
|
1119
|
+
stream_records.append(normalized)
|
|
1120
|
+
self._forward_sub_agent_chunk(normalized, stream_writer, parent_thread_id)
|
|
1121
|
+
|
|
1122
|
+
event_type = self._normalize_event_type(normalized.get("event_type"))
|
|
1123
|
+
if event_type == EVENT_TYPE_FINAL_RESPONSE:
|
|
1124
|
+
final_event = normalized
|
|
1125
|
+
|
|
1126
|
+
if not isinstance(final_event, dict):
|
|
1127
|
+
raise RuntimeError(
|
|
1128
|
+
"No final_response event captured from worker stream (likely model/context/tool execution failure)"
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
response = self._build_worker_response_from_stream_final(final_event, stream_records)
|
|
1132
|
+
return response
|
|
1133
|
+
|
|
1134
|
+
def _forward_sub_agent_chunk(
|
|
1135
|
+
self,
|
|
1136
|
+
chunk: dict[str, Any],
|
|
1137
|
+
writer: StreamWriter | None,
|
|
1138
|
+
parent_thread_id: str | None,
|
|
1139
|
+
) -> None:
|
|
1140
|
+
"""Forward one normalized sub-agent chunk to the parent stream.
|
|
1141
|
+
|
|
1142
|
+
Args:
|
|
1143
|
+
chunk (dict[str, Any]): Normalized worker stream chunk.
|
|
1144
|
+
writer (StreamWriter | None): Parent stream writer.
|
|
1145
|
+
parent_thread_id (str | None): Parent thread identifier.
|
|
1146
|
+
"""
|
|
1147
|
+
if writer is None:
|
|
1148
|
+
return
|
|
1149
|
+
|
|
1150
|
+
event_type = self._normalize_event_type(chunk.get("event_type"))
|
|
1151
|
+
if event_type == EVENT_TYPE_TOOL_CALL:
|
|
1152
|
+
self._forward_tool_call_event(chunk, writer, parent_thread_id)
|
|
1153
|
+
return
|
|
1154
|
+
if event_type == EVENT_TYPE_TOOL_RESULT:
|
|
1155
|
+
self._forward_tool_result_event(chunk, writer)
|
|
1156
|
+
return
|
|
1157
|
+
|
|
1158
|
+
marked_chunk = dict(chunk)
|
|
1159
|
+
metadata = chunk.get("metadata")
|
|
1160
|
+
marked_metadata = dict(metadata) if isinstance(metadata, dict) else {}
|
|
1161
|
+
marked_metadata[SUB_AGENT_EVENT_METADATA_KEY] = True
|
|
1162
|
+
marked_chunk["metadata"] = marked_metadata
|
|
1163
|
+
normalized_chunk = self._normalize_forwarded_sub_agent_chunk(marked_chunk)
|
|
1164
|
+
writer(normalized_chunk)
|
|
1165
|
+
|
|
1166
|
+
@staticmethod
|
|
1167
|
+
def _extract_parent_thread_id(worker_kwargs: dict[str, Any]) -> str | None:
|
|
1168
|
+
"""Extract parent thread id from child runnable kwargs.
|
|
1169
|
+
|
|
1170
|
+
Args:
|
|
1171
|
+
worker_kwargs (dict[str, Any]): Child runnable kwargs.
|
|
1172
|
+
|
|
1173
|
+
Returns:
|
|
1174
|
+
str | None: Parent thread id when present.
|
|
1175
|
+
"""
|
|
1176
|
+
configurable = worker_kwargs.get("configurable") if isinstance(worker_kwargs.get("configurable"), dict) else {}
|
|
1177
|
+
for value in configurable.values():
|
|
1178
|
+
if isinstance(value, str) and value:
|
|
1179
|
+
return value
|
|
1180
|
+
return None
|
|
1181
|
+
|
|
1182
|
+
def _forward_tool_call_event(
|
|
1183
|
+
self,
|
|
1184
|
+
chunk: dict[str, Any],
|
|
1185
|
+
writer: StreamWriter,
|
|
1186
|
+
parent_thread_id: str | None,
|
|
1187
|
+
) -> None:
|
|
1188
|
+
"""Forward tool-call event using delegation-compatible envelope.
|
|
1189
|
+
|
|
1190
|
+
Args:
|
|
1191
|
+
chunk (dict[str, Any]): Normalized stream chunk.
|
|
1192
|
+
writer (StreamWriter): Parent stream writer.
|
|
1193
|
+
parent_thread_id (str | None): Parent thread identifier.
|
|
1194
|
+
"""
|
|
1195
|
+
tool_info = chunk.get("tool_info") if isinstance(chunk.get("tool_info"), dict) else {}
|
|
1196
|
+
message = self._create_tool_call_message(tool_info)
|
|
1197
|
+
metadata = self._prepare_tool_call_metadata(chunk, tool_info, parent_thread_id)
|
|
1198
|
+
|
|
1199
|
+
a2a_event: dict[str, Any] = {
|
|
1200
|
+
"event_type": EVENT_TYPE_TOOL_CALL,
|
|
1201
|
+
"content": message,
|
|
1202
|
+
"metadata": metadata,
|
|
1203
|
+
"tool_info": tool_info,
|
|
1204
|
+
"is_final": False,
|
|
1205
|
+
"artifacts": chunk.get("artifacts"),
|
|
1206
|
+
MetadataFieldKeys.REFERENCES: chunk.get(MetadataFieldKeys.REFERENCES),
|
|
1207
|
+
STEP_USAGE_KEY: chunk.get(STEP_USAGE_KEY),
|
|
1208
|
+
TOTAL_USAGE_KEY: chunk.get(TOTAL_USAGE_KEY),
|
|
1209
|
+
MODEL_USAGE_DELTA_KEY: chunk.get(MODEL_USAGE_DELTA_KEY),
|
|
1210
|
+
MODEL_USAGE_KEY: chunk.get(MODEL_USAGE_KEY),
|
|
1211
|
+
}
|
|
1212
|
+
writer(a2a_event)
|
|
1213
|
+
|
|
1214
|
+
def _prepare_tool_call_metadata(
|
|
1215
|
+
self,
|
|
1216
|
+
chunk: dict[str, Any],
|
|
1217
|
+
tool_info: dict[str, Any],
|
|
1218
|
+
parent_thread_id: str | None,
|
|
1219
|
+
) -> dict[str, Any]:
|
|
1220
|
+
"""Prepare tool-call metadata with linkage and generated step id.
|
|
1221
|
+
|
|
1222
|
+
Args:
|
|
1223
|
+
chunk (dict[str, Any]): Normalized stream chunk.
|
|
1224
|
+
tool_info (dict[str, Any]): Tool info payload.
|
|
1225
|
+
parent_thread_id (str | None): Parent thread identifier.
|
|
1226
|
+
|
|
1227
|
+
Returns:
|
|
1228
|
+
dict[str, Any]: Enriched metadata for forwarded event.
|
|
1229
|
+
"""
|
|
1230
|
+
metadata = dict(chunk.get("metadata") or {})
|
|
1231
|
+
metadata[SUB_AGENT_EVENT_METADATA_KEY] = True
|
|
1232
|
+
self._ensure_step_id_in_metadata(metadata, tool_info)
|
|
1233
|
+
|
|
1234
|
+
agent_name = metadata.get("agent_name")
|
|
1235
|
+
if isinstance(agent_name, str) and agent_name:
|
|
1236
|
+
self._setup_agent_linkage(metadata, agent_name, tool_info, parent_thread_id)
|
|
1237
|
+
else:
|
|
1238
|
+
self._handle_missing_agent_name(metadata)
|
|
1239
|
+
return metadata
|
|
1240
|
+
|
|
1241
|
+
def _ensure_step_id_in_metadata(self, metadata: dict[str, Any], tool_info: dict[str, Any]) -> None:
|
|
1242
|
+
"""Ensure metadata has ``step_id``, generating one when missing.
|
|
1243
|
+
|
|
1244
|
+
Args:
|
|
1245
|
+
metadata (dict[str, Any]): Event metadata to mutate.
|
|
1246
|
+
tool_info (dict[str, Any]): Tool info for deterministic id generation.
|
|
1247
|
+
"""
|
|
1248
|
+
if "step_id" in metadata:
|
|
1249
|
+
return
|
|
1250
|
+
raw_agent_name = metadata.get("agent_name")
|
|
1251
|
+
agent_name_for_id = raw_agent_name if isinstance(raw_agent_name, str) and raw_agent_name else "anon_agent"
|
|
1252
|
+
metadata["step_id"] = self._generate_delegation_step_id(EVENT_TYPE_TOOL_CALL, agent_name_for_id, tool_info)
|
|
1253
|
+
|
|
1254
|
+
def _generate_delegation_step_id(self, event_type: str, agent_name: str, tool_info: dict[str, Any]) -> str:
|
|
1255
|
+
"""Generate deterministic fallback step identifiers for delegated events.
|
|
1256
|
+
|
|
1257
|
+
Args:
|
|
1258
|
+
event_type (str): Event type name.
|
|
1259
|
+
agent_name (str): Sub-agent name.
|
|
1260
|
+
tool_info (dict[str, Any]): Tool info payload.
|
|
1261
|
+
|
|
1262
|
+
Returns:
|
|
1263
|
+
str: Generated step id.
|
|
1264
|
+
"""
|
|
1265
|
+
try:
|
|
1266
|
+
counter = get_next_step_number()
|
|
1267
|
+
if event_type == EVENT_TYPE_TOOL_CALL:
|
|
1268
|
+
candidate = tool_info.get("tool_calls")
|
|
1269
|
+
if isinstance(candidate, list) and candidate:
|
|
1270
|
+
first_call = candidate[0] if isinstance(candidate[0], dict) else {}
|
|
1271
|
+
tool_name = first_call.get("name") if isinstance(first_call.get("name"), str) else "tool"
|
|
1272
|
+
return f"{tool_name}_{counter:03d}"
|
|
1273
|
+
if event_type == EVENT_TYPE_TOOL_RESULT:
|
|
1274
|
+
tool_name = tool_info.get("name") if isinstance(tool_info.get("name"), str) else "result"
|
|
1275
|
+
return f"{tool_name}_{counter:03d}"
|
|
1276
|
+
fallback_prefix = self._build_fallback_prefix(agent_name)
|
|
1277
|
+
return f"{fallback_prefix}_{event_type}_{counter:03d}"
|
|
1278
|
+
except Exception:
|
|
1279
|
+
return f"task_{uuid.uuid4().hex[:8]}"
|
|
1280
|
+
|
|
1281
|
+
@staticmethod
|
|
1282
|
+
def _build_fallback_prefix(agent_name: str) -> str:
|
|
1283
|
+
"""Build sanitized fallback prefix from agent name.
|
|
1284
|
+
|
|
1285
|
+
Args:
|
|
1286
|
+
agent_name (str): Agent name candidate.
|
|
1287
|
+
|
|
1288
|
+
Returns:
|
|
1289
|
+
str: Prefix used for generated fallback ids.
|
|
1290
|
+
"""
|
|
1291
|
+
if not agent_name:
|
|
1292
|
+
return "task"
|
|
1293
|
+
sanitized = "".join(ch for ch in agent_name.lower() if ch.isalnum())[:8]
|
|
1294
|
+
return f"task_{sanitized}" if sanitized else "task"
|
|
1295
|
+
|
|
1296
|
+
def _setup_agent_linkage(
|
|
1297
|
+
self,
|
|
1298
|
+
metadata: dict[str, Any],
|
|
1299
|
+
agent_name: str,
|
|
1300
|
+
tool_info: dict[str, Any],
|
|
1301
|
+
parent_thread_id: str | None,
|
|
1302
|
+
) -> None:
|
|
1303
|
+
"""Attach parent/sub-agent linkage fields for tool-call events.
|
|
1304
|
+
|
|
1305
|
+
Args:
|
|
1306
|
+
metadata (dict[str, Any]): Metadata to mutate.
|
|
1307
|
+
agent_name (str): Sub-agent name.
|
|
1308
|
+
tool_info (dict[str, Any]): Tool info payload.
|
|
1309
|
+
parent_thread_id (str | None): Parent thread identifier.
|
|
1310
|
+
"""
|
|
1311
|
+
parent_step_id = self._get_parent_step_id(tool_info, parent_thread_id)
|
|
1312
|
+
metadata["previous_step_ids"] = [parent_step_id] if parent_step_id else []
|
|
1313
|
+
|
|
1314
|
+
sub_start_map = _TASK_SUB_START_STEP_CVAR.get() or {}
|
|
1315
|
+
step_id = metadata.get("step_id")
|
|
1316
|
+
if isinstance(step_id, str) and step_id:
|
|
1317
|
+
sub_start_map[agent_name] = step_id
|
|
1318
|
+
_TASK_SUB_START_STEP_CVAR.set(sub_start_map)
|
|
1319
|
+
|
|
1320
|
+
def _get_parent_step_id(self, tool_info: dict[str, Any], parent_thread_id: str | None) -> str | None:
|
|
1321
|
+
"""Resolve parent step id from tool parent map when available.
|
|
1322
|
+
|
|
1323
|
+
Args:
|
|
1324
|
+
tool_info (dict[str, Any]): Tool info payload.
|
|
1325
|
+
parent_thread_id (str | None): Parent thread identifier.
|
|
1326
|
+
|
|
1327
|
+
Returns:
|
|
1328
|
+
str | None: Parent step id when found.
|
|
1329
|
+
"""
|
|
1330
|
+
if (
|
|
1331
|
+
self.parent_agent is None
|
|
1332
|
+
or not hasattr(self.parent_agent, "_tool_parent_map_by_thread")
|
|
1333
|
+
or not parent_thread_id
|
|
1334
|
+
):
|
|
1335
|
+
return None
|
|
1336
|
+
return self._lookup_parent_step_from_agent(tool_info, parent_thread_id)
|
|
1337
|
+
|
|
1338
|
+
def _lookup_parent_step_from_agent(self, tool_info: dict[str, Any], parent_thread_id: str) -> str | None:
|
|
1339
|
+
"""Lookup parent step id using parent agent thread-scoped map.
|
|
1340
|
+
|
|
1341
|
+
Args:
|
|
1342
|
+
tool_info (dict[str, Any]): Tool info payload.
|
|
1343
|
+
parent_thread_id (str): Parent thread identifier.
|
|
1344
|
+
|
|
1345
|
+
Returns:
|
|
1346
|
+
str | None: Mapped parent step id when found.
|
|
1347
|
+
"""
|
|
1348
|
+
try:
|
|
1349
|
+
parent_map = self.parent_agent._tool_parent_map_by_thread.get(parent_thread_id, {})
|
|
1350
|
+
if not isinstance(parent_map, dict):
|
|
1351
|
+
return None
|
|
1352
|
+
|
|
1353
|
+
tool_call_id = tool_info.get("id")
|
|
1354
|
+
if not isinstance(tool_call_id, str) and isinstance(tool_info.get("tool_calls"), list):
|
|
1355
|
+
for call in tool_info.get("tool_calls", []):
|
|
1356
|
+
if isinstance(call, dict) and isinstance(call.get("id"), str):
|
|
1357
|
+
tool_call_id = call.get("id")
|
|
1358
|
+
break
|
|
1359
|
+
|
|
1360
|
+
if isinstance(tool_call_id, str) and tool_call_id:
|
|
1361
|
+
parent_step = parent_map.get(str(tool_call_id))
|
|
1362
|
+
if isinstance(parent_step, str) and parent_step:
|
|
1363
|
+
return parent_step
|
|
1364
|
+
except Exception:
|
|
1365
|
+
logger.debug("task_delegation_parent_step_lookup_failed", exc_info=True)
|
|
1366
|
+
return None
|
|
1367
|
+
|
|
1368
|
+
@staticmethod
|
|
1369
|
+
def _handle_missing_agent_name(metadata: dict[str, Any]) -> None:
|
|
1370
|
+
"""Mark metadata for missing agent-name linkage scenario.
|
|
1371
|
+
|
|
1372
|
+
Args:
|
|
1373
|
+
metadata (dict[str, Any]): Metadata to mutate.
|
|
1374
|
+
"""
|
|
1375
|
+
metadata["previous_step_ids"] = []
|
|
1376
|
+
metadata["agent_name_missing"] = True
|
|
1377
|
+
|
|
1378
|
+
def _forward_tool_result_event(self, chunk: dict[str, Any], writer: StreamWriter) -> None:
|
|
1379
|
+
"""Forward tool-result event using delegation-compatible envelope.
|
|
1380
|
+
|
|
1381
|
+
Args:
|
|
1382
|
+
chunk (dict[str, Any]): Normalized stream chunk.
|
|
1383
|
+
writer (StreamWriter): Parent stream writer.
|
|
1384
|
+
"""
|
|
1385
|
+
tool_info = chunk.get("tool_info") if isinstance(chunk.get("tool_info"), dict) else {}
|
|
1386
|
+
deduped_tool_names = self._extract_tool_result_names(tool_info)
|
|
1387
|
+
metadata = self._build_tool_result_metadata(chunk)
|
|
1388
|
+
|
|
1389
|
+
content = self._build_completion_content(deduped_tool_names)
|
|
1390
|
+
a2a_event: dict[str, Any] = {
|
|
1391
|
+
"event_type": EVENT_TYPE_TOOL_RESULT,
|
|
1392
|
+
"content": content,
|
|
1393
|
+
"metadata": metadata,
|
|
1394
|
+
"tool_info": tool_info,
|
|
1395
|
+
"is_final": False,
|
|
1396
|
+
"artifacts": chunk.get("artifacts"),
|
|
1397
|
+
MetadataFieldKeys.REFERENCES: chunk.get(MetadataFieldKeys.REFERENCES),
|
|
1398
|
+
STEP_USAGE_KEY: chunk.get(STEP_USAGE_KEY),
|
|
1399
|
+
TOTAL_USAGE_KEY: chunk.get(TOTAL_USAGE_KEY),
|
|
1400
|
+
MODEL_USAGE_DELTA_KEY: chunk.get(MODEL_USAGE_DELTA_KEY),
|
|
1401
|
+
MODEL_USAGE_KEY: chunk.get(MODEL_USAGE_KEY),
|
|
1402
|
+
}
|
|
1403
|
+
writer(a2a_event)
|
|
1404
|
+
|
|
1405
|
+
@staticmethod
|
|
1406
|
+
def _extract_tool_result_names(tool_info: dict[str, Any]) -> list[str]:
|
|
1407
|
+
"""Extract deterministic tool-name list from tool-result payload.
|
|
1408
|
+
|
|
1409
|
+
Args:
|
|
1410
|
+
tool_info (dict[str, Any]): Tool result payload.
|
|
1411
|
+
|
|
1412
|
+
Returns:
|
|
1413
|
+
list[str]: Deduplicated non-empty tool names.
|
|
1414
|
+
"""
|
|
1415
|
+
primary_name = tool_info.get("name")
|
|
1416
|
+
if isinstance(primary_name, str) and primary_name:
|
|
1417
|
+
return [primary_name]
|
|
1418
|
+
|
|
1419
|
+
tool_calls = tool_info.get("tool_calls")
|
|
1420
|
+
if not isinstance(tool_calls, list):
|
|
1421
|
+
return ["unknown_tool"] # pragma: no cover
|
|
1422
|
+
|
|
1423
|
+
tool_names: list[str] = []
|
|
1424
|
+
for call in tool_calls:
|
|
1425
|
+
if not isinstance(call, dict):
|
|
1426
|
+
continue
|
|
1427
|
+
name = call.get("name")
|
|
1428
|
+
if isinstance(name, str) and name:
|
|
1429
|
+
tool_names.append(name)
|
|
1430
|
+
|
|
1431
|
+
deduped_names = list(dict.fromkeys(tool_names))
|
|
1432
|
+
return deduped_names or ["unknown_tool"]
|
|
1433
|
+
|
|
1434
|
+
def _build_tool_result_metadata(self, chunk: dict[str, Any]) -> dict[str, Any]:
|
|
1435
|
+
"""Build delegated metadata envelope for forwarded tool-result event.
|
|
1436
|
+
|
|
1437
|
+
Args:
|
|
1438
|
+
chunk (dict[str, Any]): Normalized worker chunk.
|
|
1439
|
+
|
|
1440
|
+
Returns:
|
|
1441
|
+
dict[str, Any]: Metadata with sub-agent linkage markers.
|
|
1442
|
+
"""
|
|
1443
|
+
metadata = dict(chunk.get("metadata") or {})
|
|
1444
|
+
metadata[SUB_AGENT_EVENT_METADATA_KEY] = True
|
|
1445
|
+
|
|
1446
|
+
agent_name = metadata.get("agent_name")
|
|
1447
|
+
if not isinstance(agent_name, str) or not agent_name:
|
|
1448
|
+
self._handle_missing_agent_name(metadata)
|
|
1449
|
+
return metadata
|
|
1450
|
+
|
|
1451
|
+
sub_start_map = _TASK_SUB_START_STEP_CVAR.get() or {}
|
|
1452
|
+
start_step_id = sub_start_map.get(agent_name)
|
|
1453
|
+
metadata["previous_step_ids"] = [start_step_id] if isinstance(start_step_id, str) and start_step_id else []
|
|
1454
|
+
return metadata
|
|
1455
|
+
|
|
1456
|
+
def _build_completion_content(self, tool_names: list[str]) -> str:
|
|
1457
|
+
"""Build parent-facing completion content for tool-result events.
|
|
1458
|
+
|
|
1459
|
+
Args:
|
|
1460
|
+
tool_names (list[str]): Completed tool names.
|
|
1461
|
+
|
|
1462
|
+
Returns:
|
|
1463
|
+
str: Completion text matching delegation manager style.
|
|
1464
|
+
"""
|
|
1465
|
+
if self.parent_agent and hasattr(self.parent_agent, "_get_tool_completion_content"):
|
|
1466
|
+
try:
|
|
1467
|
+
return self.parent_agent._get_tool_completion_content(tool_names)
|
|
1468
|
+
except Exception:
|
|
1469
|
+
logger.debug("task_delegation_parent_completion_format_failed", exc_info=True)
|
|
1470
|
+
|
|
1471
|
+
has_delegation = any(name.startswith("delegate_to") for name in tool_names)
|
|
1472
|
+
prefix = "Completed sub-agents:" if has_delegation else "Completed tools:"
|
|
1473
|
+
return f"{prefix} {', '.join(tool_names)}"
|
|
1474
|
+
|
|
1475
|
+
@staticmethod
|
|
1476
|
+
def _create_tool_call_message(tool_info: dict[str, Any]) -> str:
|
|
1477
|
+
"""Build human-readable message for forwarded tool-call events.
|
|
1478
|
+
|
|
1479
|
+
Args:
|
|
1480
|
+
tool_info (dict[str, Any]): Tool info payload.
|
|
1481
|
+
|
|
1482
|
+
Returns:
|
|
1483
|
+
str: Formatted message with tool names.
|
|
1484
|
+
"""
|
|
1485
|
+
tool_calls = tool_info.get("tool_calls", [])
|
|
1486
|
+
tool_names = [tc.get("name", "unknown") for tc in tool_calls if isinstance(tc, dict)]
|
|
1487
|
+
if not tool_names:
|
|
1488
|
+
tool_names = ["unknown"]
|
|
1489
|
+
return f"Processing with tools: {', '.join(tool_names)}"
|
|
1490
|
+
|
|
1491
|
+
def _normalize_forwarded_sub_agent_chunk(self, chunk: dict[str, Any]) -> dict[str, Any]:
|
|
1492
|
+
"""Normalize forwarded sub-agent chunk for parent stream safety.
|
|
1493
|
+
|
|
1494
|
+
Args:
|
|
1495
|
+
chunk (dict[str, Any]): Marked sub-agent chunk.
|
|
1496
|
+
|
|
1497
|
+
Returns:
|
|
1498
|
+
dict[str, Any]: Normalized chunk with terminal remapping applied.
|
|
1499
|
+
"""
|
|
1500
|
+
normalized_chunk = dict(chunk)
|
|
1501
|
+
if normalized_chunk.get("is_final") is True:
|
|
1502
|
+
normalized_chunk["is_final"] = False
|
|
1503
|
+
normalized_chunk.pop("final", None)
|
|
1504
|
+
|
|
1505
|
+
event_type = self._normalize_event_type(normalized_chunk.get("event_type"))
|
|
1506
|
+
remapped_event_type = SUB_AGENT_TERMINAL_EVENT_REMAP.get(event_type)
|
|
1507
|
+
if remapped_event_type is not None:
|
|
1508
|
+
normalized_chunk["event_type"] = remapped_event_type
|
|
1509
|
+
return normalized_chunk
|
|
1510
|
+
|
|
1511
|
+
@staticmethod
|
|
1512
|
+
def _build_worker_response_from_stream_final(
|
|
1513
|
+
final_event: dict[str, Any],
|
|
1514
|
+
stream_records: list[dict[str, Any]],
|
|
1515
|
+
) -> dict[str, Any]:
|
|
1516
|
+
"""Build canonical worker response payload from final stream event.
|
|
1517
|
+
|
|
1518
|
+
Args:
|
|
1519
|
+
final_event (dict[str, Any]): Final stream event payload.
|
|
1520
|
+
stream_records (list[dict[str, Any]]): Normalized stream records.
|
|
1521
|
+
|
|
1522
|
+
Returns:
|
|
1523
|
+
dict[str, Any]: Canonical worker response with ``full_final_state``.
|
|
1524
|
+
"""
|
|
1525
|
+
final_metadata = final_event.get("metadata") if isinstance(final_event.get("metadata"), dict) else {}
|
|
1526
|
+
merged_metadata = dict(final_metadata)
|
|
1527
|
+
|
|
1528
|
+
timeout_delegation = TaskDelegationMiddleware._extract_timeout_delegation_metadata(stream_records)
|
|
1529
|
+
if timeout_delegation is not None:
|
|
1530
|
+
existing_delegation = merged_metadata.get(DELEGATION_METADATA_KEY)
|
|
1531
|
+
if isinstance(existing_delegation, dict):
|
|
1532
|
+
merged_delegation = dict(existing_delegation) # pragma: no cover
|
|
1533
|
+
merged_delegation.update(timeout_delegation) # pragma: no cover
|
|
1534
|
+
merged_metadata[DELEGATION_METADATA_KEY] = merged_delegation # pragma: no cover
|
|
1535
|
+
else:
|
|
1536
|
+
merged_metadata[DELEGATION_METADATA_KEY] = timeout_delegation
|
|
1537
|
+
|
|
1538
|
+
return {
|
|
1539
|
+
"output": str(final_event.get("content", "")),
|
|
1540
|
+
"full_final_state": {
|
|
1541
|
+
"total_usage": final_event.get("total_usage")
|
|
1542
|
+
if isinstance(final_event.get("total_usage"), dict)
|
|
1543
|
+
else {},
|
|
1544
|
+
"references": final_event.get("references") if isinstance(final_event.get("references"), list) else [],
|
|
1545
|
+
"metadata": merged_metadata,
|
|
1546
|
+
"stream_trace": stream_records,
|
|
1547
|
+
"artifacts": final_event.get("artifacts") if isinstance(final_event.get("artifacts"), list) else [],
|
|
1548
|
+
},
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
@staticmethod
|
|
1552
|
+
def _extract_timeout_delegation_metadata(stream_records: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
1553
|
+
"""Extract timeout delegation metadata from normalized worker stream records."""
|
|
1554
|
+
timeout_execution = TaskDelegationMiddleware._extract_timeout_tool_execution(stream_records)
|
|
1555
|
+
if timeout_execution is None:
|
|
1556
|
+
return None
|
|
1557
|
+
|
|
1558
|
+
return {
|
|
1559
|
+
"status": DELEGATION_FAILED_STATUS,
|
|
1560
|
+
"reason": DELEGATION_TOOL_TIMEOUT_REASON,
|
|
1561
|
+
"tool_execution": timeout_execution,
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
@staticmethod
|
|
1565
|
+
def _extract_timeout_tool_execution(stream_records: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
1566
|
+
"""Return the last timeout-marked tool_execution payload from stream records."""
|
|
1567
|
+
for record in reversed(stream_records):
|
|
1568
|
+
if not isinstance(record, dict):
|
|
1569
|
+
continue # pragma: no cover
|
|
1570
|
+
if record.get("event_type") != EVENT_TYPE_TOOL_RESULT:
|
|
1571
|
+
continue
|
|
1572
|
+
|
|
1573
|
+
metadata = record.get("metadata")
|
|
1574
|
+
if not isinstance(metadata, dict):
|
|
1575
|
+
continue # pragma: no cover
|
|
1576
|
+
|
|
1577
|
+
tool_execution = metadata.get("tool_execution")
|
|
1578
|
+
if not isinstance(tool_execution, dict):
|
|
1579
|
+
continue
|
|
1580
|
+
if tool_execution.get("status") != "timeout":
|
|
1581
|
+
continue # pragma: no cover
|
|
1582
|
+
|
|
1583
|
+
return dict(tool_execution)
|
|
1584
|
+
|
|
1585
|
+
return None
|
|
1586
|
+
|
|
1587
|
+
@staticmethod
|
|
1588
|
+
def _normalize_event_type(raw: Any) -> str:
|
|
1589
|
+
"""Normalize event type values from enum or arbitrary objects.
|
|
1590
|
+
|
|
1591
|
+
Args:
|
|
1592
|
+
raw (Any): Raw event type value.
|
|
1593
|
+
|
|
1594
|
+
Returns:
|
|
1595
|
+
str: Normalized event type string.
|
|
1596
|
+
"""
|
|
1597
|
+
if hasattr(raw, "value"):
|
|
1598
|
+
return str(raw.value)
|
|
1599
|
+
if isinstance(raw, str):
|
|
1600
|
+
return raw
|
|
1601
|
+
return str(raw)
|
|
1602
|
+
|
|
1603
|
+
def _normalize_worker_stream_chunk(self, chunk: dict[str, Any]) -> dict[str, Any]:
|
|
1604
|
+
"""Normalize raw worker stream chunk into stable internal structure.
|
|
1605
|
+
|
|
1606
|
+
Args:
|
|
1607
|
+
chunk (dict[str, Any]): Raw worker stream chunk.
|
|
1608
|
+
|
|
1609
|
+
Returns:
|
|
1610
|
+
dict[str, Any]: Normalized stream chunk.
|
|
1611
|
+
"""
|
|
1612
|
+
event_type = self._normalize_event_type(chunk.get("event_type"))
|
|
1613
|
+
metadata = chunk.get("metadata") if isinstance(chunk.get("metadata"), dict) else {}
|
|
1614
|
+
tool_info = chunk.get("tool_info") if isinstance(chunk.get("tool_info"), dict) else {}
|
|
1615
|
+
record: dict[str, Any] = {
|
|
1616
|
+
"event_type": event_type,
|
|
1617
|
+
"step_id": metadata.get("step_id"),
|
|
1618
|
+
"previous_step_ids": metadata.get("previous_step_ids"),
|
|
1619
|
+
"is_final": bool(chunk.get("is_final")),
|
|
1620
|
+
"content": chunk.get("content", ""),
|
|
1621
|
+
"metadata": metadata,
|
|
1622
|
+
"tool_info": tool_info,
|
|
1623
|
+
"step_usage": chunk.get("step_usage") if isinstance(chunk.get("step_usage"), dict) else None,
|
|
1624
|
+
"total_usage": chunk.get("total_usage") if isinstance(chunk.get("total_usage"), dict) else None,
|
|
1625
|
+
MODEL_USAGE_DELTA_KEY: chunk.get(MODEL_USAGE_DELTA_KEY)
|
|
1626
|
+
if isinstance(chunk.get(MODEL_USAGE_DELTA_KEY), dict)
|
|
1627
|
+
else None,
|
|
1628
|
+
MODEL_USAGE_KEY: chunk.get(MODEL_USAGE_KEY) if isinstance(chunk.get(MODEL_USAGE_KEY), list) else None,
|
|
1629
|
+
"references": chunk.get("references") if isinstance(chunk.get("references"), list) else None,
|
|
1630
|
+
"artifacts": chunk.get("artifacts") if isinstance(chunk.get("artifacts"), list) else None,
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if event_type == EVENT_TYPE_TOOL_CALL:
|
|
1634
|
+
tool_calls: list[dict[str, Any]] = (
|
|
1635
|
+
[tc for tc in tool_info.get("tool_calls", []) if isinstance(tc, dict)]
|
|
1636
|
+
if isinstance(tool_info.get("tool_calls"), list)
|
|
1637
|
+
else []
|
|
1638
|
+
)
|
|
1639
|
+
record["tool_calls"] = [
|
|
1640
|
+
{
|
|
1641
|
+
"id": tc.get("id"),
|
|
1642
|
+
"name": tc.get("name"),
|
|
1643
|
+
"args": tc.get("args"),
|
|
1644
|
+
}
|
|
1645
|
+
for tc in tool_calls
|
|
1646
|
+
]
|
|
1647
|
+
elif event_type == EVENT_TYPE_TOOL_RESULT:
|
|
1648
|
+
record["tool_result"] = {
|
|
1649
|
+
"id": tool_info.get("id"),
|
|
1650
|
+
"name": tool_info.get("name"),
|
|
1651
|
+
"execution_time": tool_info.get("execution_time"),
|
|
1652
|
+
"args": tool_info.get("args"),
|
|
1653
|
+
"output": tool_info.get("output"),
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
return record
|