langchain-agentx-python 0.1__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.
- langchain_agentx/__init__.py +46 -0
- langchain_agentx/command/__init__.py +28 -0
- langchain_agentx/command/builtin/__init__.py +25 -0
- langchain_agentx/command/builtin/clear.py +33 -0
- langchain_agentx/command/builtin/compact.py +33 -0
- langchain_agentx/command/builtin/memory.py +37 -0
- langchain_agentx/command/builtin/reload_plugins.py +42 -0
- langchain_agentx/command/context.py +30 -0
- langchain_agentx/command/dispatcher.py +183 -0
- langchain_agentx/command/registry.py +110 -0
- langchain_agentx/command/result.py +25 -0
- langchain_agentx/command/types.py +41 -0
- langchain_agentx/config/__init__.py +14 -0
- langchain_agentx/loop/__init__.py +47 -0
- langchain_agentx/loop/config/__init__.py +20 -0
- langchain_agentx/loop/config/agent_config.py +66 -0
- langchain_agentx/loop/config/agent_loop_config.py +72 -0
- langchain_agentx/loop/config/model_context_resolver.py +105 -0
- langchain_agentx/loop/config/runtime_settings.py +50 -0
- langchain_agentx/loop/config/token_estimator.py +133 -0
- langchain_agentx/loop/context/__init__.py +66 -0
- langchain_agentx/loop/context/blocking_guard.py +97 -0
- langchain_agentx/loop/context/compaction_service.py +60 -0
- langchain_agentx/loop/context/message_utils.py +56 -0
- langchain_agentx/loop/context/pipeline.py +127 -0
- langchain_agentx/loop/context/settings.py +103 -0
- langchain_agentx/loop/context/stages/__init__.py +29 -0
- langchain_agentx/loop/context/stages/autocompact.py +140 -0
- langchain_agentx/loop/context/stages/base.py +32 -0
- langchain_agentx/loop/context/stages/collapse.py +76 -0
- langchain_agentx/loop/context/stages/microcompact.py +76 -0
- langchain_agentx/loop/context/stages/noop.py +33 -0
- langchain_agentx/loop/context/stages/snip.py +71 -0
- langchain_agentx/loop/context/stages/tool_result_budget.py +69 -0
- langchain_agentx/loop/context/types.py +79 -0
- langchain_agentx/loop/exit/__init__.py +1 -0
- langchain_agentx/loop/exit/exit_logic.py +320 -0
- langchain_agentx/loop/exit/reason_codes.py +39 -0
- langchain_agentx/loop/graph/__init__.py +5 -0
- langchain_agentx/loop/graph/builtin_loop_control.py +197 -0
- langchain_agentx/loop/graph/factory.py +1409 -0
- langchain_agentx/loop/graph/graph_edges.py +820 -0
- langchain_agentx/loop/hook/__init__.py +48 -0
- langchain_agentx/loop/hook/async_hook_runner.py +62 -0
- langchain_agentx/loop/hook/config.py +280 -0
- langchain_agentx/loop/hook/engine.py +321 -0
- langchain_agentx/loop/hook/executors/__init__.py +9 -0
- langchain_agentx/loop/hook/executors/agent.py +107 -0
- langchain_agentx/loop/hook/executors/command.py +230 -0
- langchain_agentx/loop/hook/executors/http.py +114 -0
- langchain_agentx/loop/hook/executors/prompt.py +92 -0
- langchain_agentx/loop/hook/graph_wiring.py +134 -0
- langchain_agentx/loop/hook/registry.py +262 -0
- langchain_agentx/loop/hook/trust.py +43 -0
- langchain_agentx/loop/hook/types.py +110 -0
- langchain_agentx/loop/injection/__init__.py +13 -0
- langchain_agentx/loop/injection/dedup.py +74 -0
- langchain_agentx/loop/loop_abort.py +36 -0
- langchain_agentx/loop/model/__init__.py +1 -0
- langchain_agentx/loop/model/model_node.py +648 -0
- langchain_agentx/loop/model/model_nodes.py +661 -0
- langchain_agentx/loop/model/orphan_tool_results.py +38 -0
- langchain_agentx/loop/model/retrier.py +307 -0
- langchain_agentx/loop/model/retry_bridge.py +58 -0
- langchain_agentx/loop/model/retry_events.py +35 -0
- langchain_agentx/loop/model/retry_policy.py +56 -0
- langchain_agentx/loop/model/schema_and_format.py +153 -0
- langchain_agentx/loop/model/tool_and_model_binding.py +227 -0
- langchain_agentx/loop/model/tool_call_degradation_corrector.py +443 -0
- langchain_agentx/loop/model/tool_transcript_guard.py +225 -0
- langchain_agentx/loop/prompt/__init__.py +95 -0
- langchain_agentx/loop/prompt/builder.py +61 -0
- langchain_agentx/loop/prompt/builtin.py +218 -0
- langchain_agentx/loop/prompt/compact.py +408 -0
- langchain_agentx/loop/prompt/sections.py +120 -0
- langchain_agentx/loop/runtime/__init__.py +19 -0
- langchain_agentx/loop/runtime/context.py +34 -0
- langchain_agentx/loop/runtime/context_factory.py +107 -0
- langchain_agentx/loop/runtime/subagent_execution_paths.py +68 -0
- langchain_agentx/loop/subagent/__init__.py +53 -0
- langchain_agentx/loop/subagent/async_runner.py +215 -0
- langchain_agentx/loop/subagent/context.py +209 -0
- langchain_agentx/loop/subagent/fork_worktree_notice.py +25 -0
- langchain_agentx/loop/subagent/graph.py +72 -0
- langchain_agentx/loop/subagent/orchestrator.py +391 -0
- langchain_agentx/loop/subagent/progress.py +30 -0
- langchain_agentx/loop/subagent/prompt.py +52 -0
- langchain_agentx/loop/subagent/runner.py +504 -0
- langchain_agentx/loop/subagent/transcript.py +172 -0
- langchain_agentx/memory/__init__.py +2 -0
- langchain_agentx/memory/instruction/__init__.py +12 -0
- langchain_agentx/memory/instruction/loader.py +325 -0
- langchain_agentx/memory/instruction/resolver.py +24 -0
- langchain_agentx/memory/instruction/runtime.py +83 -0
- langchain_agentx/memory/instruction/sections.py +83 -0
- langchain_agentx/memory/instruction/types.py +59 -0
- langchain_agentx/memory/memdir/__init__.py +77 -0
- langchain_agentx/memory/memdir/age.py +36 -0
- langchain_agentx/memory/memdir/agent_memory.py +380 -0
- langchain_agentx/memory/memdir/extractor.py +309 -0
- langchain_agentx/memory/memdir/loader.py +187 -0
- langchain_agentx/memory/memdir/paths.py +63 -0
- langchain_agentx/memory/memdir/recall.py +45 -0
- langchain_agentx/memory/memdir/runtime.py +43 -0
- langchain_agentx/memory/memdir/scan.py +135 -0
- langchain_agentx/memory/memdir/types.py +104 -0
- langchain_agentx/memory/session/__init__.py +76 -0
- langchain_agentx/memory/session/compact_bridge.py +208 -0
- langchain_agentx/memory/session/prompts.py +172 -0
- langchain_agentx/memory/session/session_memory.py +282 -0
- langchain_agentx/observability/__init__.py +67 -0
- langchain_agentx/observability/evaluation/__init__.py +17 -0
- langchain_agentx/observability/evaluation/checkers/__init__.py +18 -0
- langchain_agentx/observability/evaluation/checkers/base.py +34 -0
- langchain_agentx/observability/evaluation/checkers/compaction.py +38 -0
- langchain_agentx/observability/evaluation/checkers/degradation.py +50 -0
- langchain_agentx/observability/evaluation/checkers/exit_quality.py +42 -0
- langchain_agentx/observability/evaluation/checkers/session_memory.py +45 -0
- langchain_agentx/observability/evaluation/checkers/tool_behavior.py +53 -0
- langchain_agentx/observability/evaluation/retention_scheduler.py +67 -0
- langchain_agentx/observability/evaluation/service.py +102 -0
- langchain_agentx/observability/evaluation/state.py +32 -0
- langchain_agentx/observability/evaluation/store.py +258 -0
- langchain_agentx/observability/events/__init__.py +15 -0
- langchain_agentx/observability/events/langchain_agentx_event_adapter.py +832 -0
- langchain_agentx/observability/logging/__init__.py +15 -0
- langchain_agentx/observability/logging/debug_burst.py +95 -0
- langchain_agentx/observability/logging/logging_config.py +178 -0
- langchain_agentx/observability/logging/logging_contract.py +65 -0
- langchain_agentx/observability/replay/__init__.py +35 -0
- langchain_agentx/observability/replay/cli.py +91 -0
- langchain_agentx/observability/replay/service.py +83 -0
- langchain_agentx/observability/replay/store.py +278 -0
- langchain_agentx/observability/replay/ui.py +47 -0
- langchain_agentx/observability/trace/__init__.py +25 -0
- langchain_agentx/observability/trace/collector.py +560 -0
- langchain_agentx/observability/trace/event_emitter.py +183 -0
- langchain_agentx/observability/trace/hook_event_emitter.py +49 -0
- langchain_agentx/observability/trace/models.py +144 -0
- langchain_agentx/observability/trace/sqlite_store.py +873 -0
- langchain_agentx/observability/trace/trace_callback.py +295 -0
- langchain_agentx/observability/trace/trace_lifecycle_collector.py +114 -0
- langchain_agentx/plugin/__init__.py +26 -0
- langchain_agentx/plugin/builtin.py +53 -0
- langchain_agentx/plugin/config.py +113 -0
- langchain_agentx/plugin/loader.py +386 -0
- langchain_agentx/plugin/manifest.py +154 -0
- langchain_agentx/plugin/registries.py +211 -0
- langchain_agentx/plugin/types.py +142 -0
- langchain_agentx/provider/__init__.py +27 -0
- langchain_agentx/provider/anthropic.py +121 -0
- langchain_agentx/provider/compatible_chat_openai.py +86 -0
- langchain_agentx/provider/env.py +45 -0
- langchain_agentx/provider/model_profile.py +156 -0
- langchain_agentx/provider/openai.py +89 -0
- langchain_agentx/session/__init__.py +17 -0
- langchain_agentx/session/agent_session.py +320 -0
- langchain_agentx/session/conversation_factory.py +87 -0
- langchain_agentx/session/conversation_recovery.py +156 -0
- langchain_agentx/session/conversation_session.py +198 -0
- langchain_agentx/session/factory.py +143 -0
- langchain_agentx/session/protocol.py +25 -0
- langchain_agentx/task_runtime/__init__.py +113 -0
- langchain_agentx/task_runtime/core/__init__.py +51 -0
- langchain_agentx/task_runtime/core/ids.py +33 -0
- langchain_agentx/task_runtime/core/interfaces.py +115 -0
- langchain_agentx/task_runtime/core/notification_priority.py +19 -0
- langchain_agentx/task_runtime/core/types.py +136 -0
- langchain_agentx/task_runtime/integrations/__init__.py +33 -0
- langchain_agentx/task_runtime/integrations/loop_adapter.py +91 -0
- langchain_agentx/task_runtime/integrations/loop_integration.py +61 -0
- langchain_agentx/task_runtime/integrations/prefetch_providers.py +108 -0
- langchain_agentx/task_runtime/integrations/provider_factory.py +103 -0
- langchain_agentx/task_runtime/integrations/queued_command_provider.py +184 -0
- langchain_agentx/task_runtime/integrations/sqlite_queued_command_provider.py +338 -0
- langchain_agentx/task_runtime/integrations/tool_use_summary_provider.py +254 -0
- langchain_agentx/task_runtime/orchestrator/__init__.py +5 -0
- langchain_agentx/task_runtime/orchestrator/runtime.py +386 -0
- langchain_agentx/task_runtime/output/__init__.py +5 -0
- langchain_agentx/task_runtime/output/sink.py +64 -0
- langchain_agentx/task_runtime/policy/__init__.py +11 -0
- langchain_agentx/task_runtime/policy/withhold_visibility.py +32 -0
- langchain_agentx/task_runtime/queue/__init__.py +5 -0
- langchain_agentx/task_runtime/queue/in_memory.py +55 -0
- langchain_agentx/task_runtime/skill_prefetch/__init__.py +4 -0
- langchain_agentx/task_runtime/skill_prefetch/attachments.py +46 -0
- langchain_agentx/task_runtime/skill_prefetch/models.py +37 -0
- langchain_agentx/task_runtime/skill_prefetch/provider.py +344 -0
- langchain_agentx/task_runtime/store/__init__.py +6 -0
- langchain_agentx/task_runtime/store/in_memory.py +81 -0
- langchain_agentx/task_runtime/store/sqlite_store.py +281 -0
- langchain_agentx/task_runtime/tasks/__init__.py +76 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/__init__.py +15 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/base.py +41 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/evaluation.py +67 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/registry.py +36 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/scheduler.py +70 -0
- langchain_agentx/task_runtime/tasks/base/__init__.py +6 -0
- langchain_agentx/task_runtime/tasks/base/contracts.py +24 -0
- langchain_agentx/task_runtime/tasks/custom/__init__.py +7 -0
- langchain_agentx/task_runtime/tasks/custom/executor.py +60 -0
- langchain_agentx/task_runtime/tasks/custom/notification.py +7 -0
- langchain_agentx/task_runtime/tasks/custom/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/custom/spec.py +33 -0
- langchain_agentx/task_runtime/tasks/dream_task/__init__.py +15 -0
- langchain_agentx/task_runtime/tasks/dream_task/executor.py +61 -0
- langchain_agentx/task_runtime/tasks/dream_task/notification.py +19 -0
- langchain_agentx/task_runtime/tasks/dream_task/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/dream_task/spec.py +35 -0
- langchain_agentx/task_runtime/tasks/dream_task/state.py +17 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/__init__.py +12 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/executor.py +36 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/notification.py +25 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/spec.py +63 -0
- langchain_agentx/task_runtime/tasks/local_agent/__init__.py +14 -0
- langchain_agentx/task_runtime/tasks/local_agent/executor.py +33 -0
- langchain_agentx/task_runtime/tasks/local_agent/notification.py +21 -0
- langchain_agentx/task_runtime/tasks/local_agent/runner.py +43 -0
- langchain_agentx/task_runtime/tasks/local_agent/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/local_agent/spec.py +31 -0
- langchain_agentx/task_runtime/tasks/local_bash/__init__.py +13 -0
- langchain_agentx/task_runtime/tasks/local_bash/executor.py +95 -0
- langchain_agentx/task_runtime/tasks/local_bash/notification.py +22 -0
- langchain_agentx/task_runtime/tasks/local_bash/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/local_bash/spec.py +55 -0
- langchain_agentx/task_runtime/tasks/remote_agent/__init__.py +19 -0
- langchain_agentx/task_runtime/tasks/remote_agent/backend.py +76 -0
- langchain_agentx/task_runtime/tasks/remote_agent/executor.py +37 -0
- langchain_agentx/task_runtime/tasks/remote_agent/notification.py +22 -0
- langchain_agentx/task_runtime/tasks/remote_agent/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/remote_agent/spec.py +34 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/__init__.py +19 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/bootstrap.py +95 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/executor.py +66 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/scheduler.py +169 -0
- langchain_agentx/tool_runtime/__init__.py +90 -0
- langchain_agentx/tool_runtime/adapter.py +365 -0
- langchain_agentx/tool_runtime/base.py +319 -0
- langchain_agentx/tool_runtime/errors.py +190 -0
- langchain_agentx/tool_runtime/identical_call_cache.py +110 -0
- langchain_agentx/tool_runtime/loader.py +195 -0
- langchain_agentx/tool_runtime/models.py +260 -0
- langchain_agentx/tool_runtime/permission_context.py +78 -0
- langchain_agentx/tool_runtime/pipeline.py +621 -0
- langchain_agentx/tool_runtime/policy.py +447 -0
- langchain_agentx/tool_runtime/registry.py +81 -0
- langchain_agentx/tool_runtime/resolvers/__init__.py +27 -0
- langchain_agentx/tool_runtime/resolvers/agent_session.py +125 -0
- langchain_agentx/tool_runtime/resolvers/background.py +32 -0
- langchain_agentx/tool_runtime/resolvers/base.py +20 -0
- langchain_agentx/tool_runtime/resolvers/conversation.py +22 -0
- langchain_agentx/tool_runtime/resolvers/workflow.py +73 -0
- langchain_agentx/tool_runtime/session_store.py +132 -0
- langchain_agentx/tool_runtime/smoke_test_runtime.py +294 -0
- langchain_agentx/tool_runtime/state_bridge.py +164 -0
- langchain_agentx/tools/__init__.py +26 -0
- langchain_agentx/tools/agent/__init__.py +9 -0
- langchain_agentx/tools/agent/backend.py +53 -0
- langchain_agentx/tools/agent/built_in/__init__.py +19 -0
- langchain_agentx/tools/agent/built_in/agentx_guide.py +65 -0
- langchain_agentx/tools/agent/built_in/explore.py +80 -0
- langchain_agentx/tools/agent/built_in/general.py +57 -0
- langchain_agentx/tools/agent/built_in/plan.py +89 -0
- langchain_agentx/tools/agent/built_in/statusline_setup.py +64 -0
- langchain_agentx/tools/agent/built_in/verification.py +120 -0
- langchain_agentx/tools/agent/builtin_subagent_loader.py +89 -0
- langchain_agentx/tools/agent/cwd_resolution.py +119 -0
- langchain_agentx/tools/agent/limits.py +26 -0
- langchain_agentx/tools/agent/loader.py +270 -0
- langchain_agentx/tools/agent/models.py +85 -0
- langchain_agentx/tools/agent/prompt.py +120 -0
- langchain_agentx/tools/agent/registry/__init__.py +18 -0
- langchain_agentx/tools/agent/registry/config.py +29 -0
- langchain_agentx/tools/agent/registry/registry.py +47 -0
- langchain_agentx/tools/agent/scope.py +137 -0
- langchain_agentx/tools/agent/tool.py +256 -0
- langchain_agentx/tools/bash/__init__.py +9 -0
- langchain_agentx/tools/bash/ast_security.py +571 -0
- langchain_agentx/tools/bash/backend.py +1447 -0
- langchain_agentx/tools/bash/bash_hardening.py +734 -0
- langchain_agentx/tools/bash/bash_runtime_contract.py +41 -0
- langchain_agentx/tools/bash/cwd_reporter.py +95 -0
- langchain_agentx/tools/bash/limits.py +71 -0
- langchain_agentx/tools/bash/mode_validation.py +282 -0
- langchain_agentx/tools/bash/models.py +131 -0
- langchain_agentx/tools/bash/observability.py +148 -0
- langchain_agentx/tools/bash/output_utils.py +200 -0
- langchain_agentx/tools/bash/path_security.py +2429 -0
- langchain_agentx/tools/bash/prompt.py +68 -0
- langchain_agentx/tools/bash/read_only_validation.py +589 -0
- langchain_agentx/tools/bash/result_presenter.py +324 -0
- langchain_agentx/tools/bash/sandbox_decision.py +133 -0
- langchain_agentx/tools/bash/security.py +311 -0
- langchain_agentx/tools/bash/sed_edit_parser.py +243 -0
- langchain_agentx/tools/bash/sed_validation.py +163 -0
- langchain_agentx/tools/bash/semantics.py +111 -0
- langchain_agentx/tools/bash/session_manager.py +205 -0
- langchain_agentx/tools/bash/session_runtime.py +290 -0
- langchain_agentx/tools/bash/shell_locator.py +191 -0
- langchain_agentx/tools/bash/task_runtime.py +91 -0
- langchain_agentx/tools/bash/tool.py +939 -0
- langchain_agentx/tools/bash/windows_shell_quoting.py +45 -0
- langchain_agentx/tools/glob/__init__.py +9 -0
- langchain_agentx/tools/glob/models.py +57 -0
- langchain_agentx/tools/glob/pagination.py +30 -0
- langchain_agentx/tools/glob/prompt.py +24 -0
- langchain_agentx/tools/glob/rg_list_backend.py +139 -0
- langchain_agentx/tools/glob/rg_pattern.py +44 -0
- langchain_agentx/tools/glob/tool.py +327 -0
- langchain_agentx/tools/grep/__init__.py +7 -0
- langchain_agentx/tools/grep/backend.py +375 -0
- langchain_agentx/tools/grep/models.py +127 -0
- langchain_agentx/tools/grep/prompt.py +30 -0
- langchain_agentx/tools/grep/rg_subprocess_controller.py +114 -0
- langchain_agentx/tools/grep/tool.py +475 -0
- langchain_agentx/tools/read/__init__.py +9 -0
- langchain_agentx/tools/read/backend.py +415 -0
- langchain_agentx/tools/read/limits.py +67 -0
- langchain_agentx/tools/read/models.py +156 -0
- langchain_agentx/tools/read/prompt.py +73 -0
- langchain_agentx/tools/read/tool.py +494 -0
- langchain_agentx/tools/ripgrep_plugin_exclusions.py +137 -0
- langchain_agentx/tools/skill/__init__.py +4 -0
- langchain_agentx/tools/skill/argument_substitution.py +80 -0
- langchain_agentx/tools/skill/loader.py +196 -0
- langchain_agentx/tools/skill/models.py +88 -0
- langchain_agentx/tools/skill/policy.py +80 -0
- langchain_agentx/tools/skill/prompt.py +35 -0
- langchain_agentx/tools/skill/tool.py +222 -0
- langchain_agentx/utils/__init__.py +0 -0
- langchain_agentx/utils/cwd.py +124 -0
- langchain_agentx/utils/host_platform.py +112 -0
- langchain_agentx/utils/path_hierarchy.py +48 -0
- langchain_agentx/utils/path_user_input.py +66 -0
- langchain_agentx/utils/rg_executable.py +18 -0
- langchain_agentx/utils/subprocess_text.py +101 -0
- langchain_agentx/utils/temp_paths.py +77 -0
- langchain_agentx/utils/unc_path.py +25 -0
- langchain_agentx/utils/win_reserved_paths.py +51 -0
- langchain_agentx/workflow/__init__.py +7 -0
- langchain_agentx/workflow/base.py +97 -0
- langchain_agentx/workflow/batch.py +55 -0
- langchain_agentx/workflow/dag.py +54 -0
- langchain_agentx/workspace/__init__.py +13 -0
- langchain_agentx/workspace/config.py +140 -0
- langchain_agentx/workspace/path_key_normalizer.py +30 -0
- langchain_agentx/workspace/resolver.py +74 -0
- langchain_agentx/workspace/validators.py +41 -0
- langchain_agentx_python-0.1.dist-info/LICENSE +201 -0
- langchain_agentx_python-0.1.dist-info/METADATA +513 -0
- langchain_agentx_python-0.1.dist-info/RECORD +354 -0
- langchain_agentx_python-0.1.dist-info/WHEEL +5 -0
- langchain_agentx_python-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
"""
|
|
2
|
+
runtime/pipeline.py — 工具执行管道与输出大小管控
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
ToolOutputManager:工具输出大小管控,超限时截断并持久化到文件,
|
|
6
|
+
向模型提供摘要 + overflow_file 路径,防止超大输出膨胀 context window。
|
|
7
|
+
|
|
8
|
+
ToolExecutorPipeline:统一编排工具生命周期的 10 步执行顺序,
|
|
9
|
+
处理所有短路场景(schema 错误、validate 失败、权限拒绝、执行异常),
|
|
10
|
+
确保始终返回结构化的 ToolResultEnvelope。
|
|
11
|
+
|
|
12
|
+
PermissionAskInterrupt:check_permissions 返回 ask 且处于交互模式时抛出,
|
|
13
|
+
由 LangGraph checkpointer + HumanInTheLoopMiddleware 捕获并弹出人机确认。
|
|
14
|
+
headless=True 时 ask 自动降级为 deny,不抛异常。
|
|
15
|
+
|
|
16
|
+
与 CC 对比:
|
|
17
|
+
CC 的 maxResultSizeChars 方案:当工具输出超过阈值时,将完整内容写入文件,
|
|
18
|
+
Claude 收到 preview + 文件路径。本框架 ToolOutputManager 实现等价逻辑。
|
|
19
|
+
CC 中 Read 工具将 maxResultSizeChars 设为 Infinity(自带分页),
|
|
20
|
+
对应本框架 RuntimeTool.never_truncate=True。
|
|
21
|
+
|
|
22
|
+
CC 中工具执行由 ToolNode 直接调用工具函数,异常处理分散在各工具内。
|
|
23
|
+
本框架将完整生命周期收敛到 ToolExecutorPipeline,工具只需实现业务逻辑。
|
|
24
|
+
|
|
25
|
+
CC 中 headless/print 模式通过 shouldAvoidPermissionPrompts=True 使 ask→deny。
|
|
26
|
+
本框架等价实现:ToolExecutorPipeline(headless=True) 时 ask 结果直接转 blocked。
|
|
27
|
+
|
|
28
|
+
同参重复调用:
|
|
29
|
+
RuntimeTool.dedupe_identical_calls=True 时,在权限通过后、invoke 前查询
|
|
30
|
+
IdenticalCallMemo;命中则直接返回带 Hint 的 ToolResultEnvelope(不执行 invoke),
|
|
31
|
+
作为“软提示防重复”策略的执行层硬约束替代。
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import asyncio
|
|
37
|
+
import logging
|
|
38
|
+
import os
|
|
39
|
+
import time
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
from pydantic import ValidationError
|
|
43
|
+
|
|
44
|
+
from .base import RuntimeTool
|
|
45
|
+
from .errors import (
|
|
46
|
+
blocked_envelope,
|
|
47
|
+
exception_to_envelope,
|
|
48
|
+
validation_error_envelope,
|
|
49
|
+
)
|
|
50
|
+
from .identical_call_cache import (
|
|
51
|
+
IdenticalCallMemo,
|
|
52
|
+
dedup_cache_key,
|
|
53
|
+
envelope_with_dedup_notice,
|
|
54
|
+
)
|
|
55
|
+
from .models import AuthorizationDecision, ToolExecutionContext, ToolResultEnvelope
|
|
56
|
+
from .resolvers import PermissionResolver
|
|
57
|
+
from langchain_agentx.observability.replay.store import OBS_SCHEMA_VERSION
|
|
58
|
+
from langchain_agentx.observability.logging import (
|
|
59
|
+
ObservabilityLoggerAdapter,
|
|
60
|
+
build_log_context,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# PermissionAskInterrupt
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
class PermissionAskInterrupt(Exception):
|
|
71
|
+
"""
|
|
72
|
+
已废弃(v2):
|
|
73
|
+
ask 分支迁移为 permission_resolver 回调,不再使用异常承载常态控制流。
|
|
74
|
+
|
|
75
|
+
保留该类型仅用于兼容历史导入;新代码不应再 raise/catch 此异常。
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
tool_name: str,
|
|
81
|
+
ask_prompt: str | None,
|
|
82
|
+
decision: "AuthorizationDecision",
|
|
83
|
+
) -> None:
|
|
84
|
+
self.tool_name = tool_name
|
|
85
|
+
self.ask_prompt = ask_prompt
|
|
86
|
+
self.decision = decision
|
|
87
|
+
super().__init__(f"[{tool_name}] requires user permission: {ask_prompt or 'confirm to proceed'}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# ToolOutputManager
|
|
92
|
+
# Named Spec: 工具输出大小管控器
|
|
93
|
+
# 对应 CC maxResultSizeChars 方案
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
class ToolOutputManager:
|
|
97
|
+
"""
|
|
98
|
+
工具输出大小管控器。
|
|
99
|
+
|
|
100
|
+
当 envelope 的文本输出超过 max_result_size_chars 时:
|
|
101
|
+
1. 将完整内容写入 overflow_dir/<tool_call_id>.txt
|
|
102
|
+
2. 截断 payload 到前 N 字符
|
|
103
|
+
3. 设置 envelope.truncated = True
|
|
104
|
+
4. 设置 envelope.overflow_file = 持久化路径
|
|
105
|
+
5. 在 summary 中追加 "[Full result saved to: <path>]"
|
|
106
|
+
|
|
107
|
+
对应 CC:
|
|
108
|
+
maxResultSizeChars — 本框架 max_result_size_chars
|
|
109
|
+
Infinity(Read 工具)— 本框架 RuntimeTool.never_truncate=True
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
DEFAULT_MAX_CHARS = 20_000
|
|
113
|
+
OVERFLOW_DIR = ".agent_tool_output"
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
max_result_size_chars: int = DEFAULT_MAX_CHARS,
|
|
118
|
+
overflow_dir: str = OVERFLOW_DIR,
|
|
119
|
+
) -> None:
|
|
120
|
+
self._max_chars = max_result_size_chars
|
|
121
|
+
self._overflow_dir = overflow_dir
|
|
122
|
+
|
|
123
|
+
def truncate_if_needed(
|
|
124
|
+
self,
|
|
125
|
+
envelope: ToolResultEnvelope,
|
|
126
|
+
tool: RuntimeTool,
|
|
127
|
+
tool_call_id: str | None = None,
|
|
128
|
+
) -> ToolResultEnvelope:
|
|
129
|
+
"""
|
|
130
|
+
检查 envelope 输出大小,超限时截断并持久化。
|
|
131
|
+
|
|
132
|
+
never_truncate=True 的工具(如 ReadRuntimeTool)直接原样返回。
|
|
133
|
+
"""
|
|
134
|
+
if tool.never_truncate:
|
|
135
|
+
return envelope
|
|
136
|
+
|
|
137
|
+
max_chars = tool.max_result_size_chars
|
|
138
|
+
|
|
139
|
+
# 将 payload 序列化为文本估算大小
|
|
140
|
+
text = self._envelope_to_text(envelope)
|
|
141
|
+
if len(text) <= max_chars:
|
|
142
|
+
return envelope
|
|
143
|
+
|
|
144
|
+
# 超限:持久化完整内容
|
|
145
|
+
overflow_path = self._persist(text, tool_call_id)
|
|
146
|
+
|
|
147
|
+
# 截断 payload
|
|
148
|
+
truncated_text = text[:max_chars]
|
|
149
|
+
truncated_summary = (
|
|
150
|
+
envelope.summary
|
|
151
|
+
+ f"\n[Output truncated at {max_chars} chars."
|
|
152
|
+
+ f" Full result saved to: {overflow_path}]"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return ToolResultEnvelope(
|
|
156
|
+
status=envelope.status,
|
|
157
|
+
tool_name=envelope.tool_name,
|
|
158
|
+
summary=truncated_summary,
|
|
159
|
+
payload=truncated_text,
|
|
160
|
+
artifacts=envelope.artifacts,
|
|
161
|
+
hints=envelope.hints,
|
|
162
|
+
meta=envelope.meta,
|
|
163
|
+
error=envelope.error,
|
|
164
|
+
truncated=True,
|
|
165
|
+
overflow_file=overflow_path,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _envelope_to_text(self, envelope: ToolResultEnvelope) -> str:
|
|
169
|
+
"""将 envelope payload 转为文本用于大小估算。"""
|
|
170
|
+
if envelope.payload is None:
|
|
171
|
+
return envelope.summary
|
|
172
|
+
if isinstance(envelope.payload, str):
|
|
173
|
+
return envelope.payload
|
|
174
|
+
# dict payload:简单序列化
|
|
175
|
+
import json
|
|
176
|
+
try:
|
|
177
|
+
return json.dumps(envelope.payload, ensure_ascii=False)
|
|
178
|
+
except Exception:
|
|
179
|
+
return str(envelope.payload)
|
|
180
|
+
|
|
181
|
+
def _persist(self, text: str, tool_call_id: str | None) -> str:
|
|
182
|
+
"""将完整内容写入 overflow_dir,返回文件路径。"""
|
|
183
|
+
os.makedirs(self._overflow_dir, exist_ok=True)
|
|
184
|
+
filename = f"{tool_call_id or int(time.time() * 1000)}.txt"
|
|
185
|
+
path = os.path.join(self._overflow_dir, filename)
|
|
186
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
187
|
+
f.write(text)
|
|
188
|
+
return path
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# ToolExecutorPipeline
|
|
193
|
+
# Named Spec: 工具执行管道
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
class ToolExecutorPipeline:
|
|
197
|
+
"""
|
|
198
|
+
工具执行管道,统一编排工具生命周期的 10 步执行顺序。
|
|
199
|
+
|
|
200
|
+
执行顺序:
|
|
201
|
+
1. normalize_input — 输入预处理
|
|
202
|
+
2. schema parse — Pydantic 校验,失败 → error envelope
|
|
203
|
+
3. validate_input — 语义校验,失败 → error envelope(模型可重试)
|
|
204
|
+
4. check_permissions — 权限控制:
|
|
205
|
+
· deny → blocked envelope(不可绕过)
|
|
206
|
+
· ask + headless=True → blocked envelope(ask 降级)
|
|
207
|
+
· ask + headless=False → 抛 PermissionAskInterrupt
|
|
208
|
+
5. before_invoke — 执行前钩子
|
|
209
|
+
6. invoke / ainvoke — 核心业务,异常 → error envelope
|
|
210
|
+
7. after_invoke — 执行后钩子(状态写入、审计)
|
|
211
|
+
8. state_bridge.update — 状态桥接(如有)
|
|
212
|
+
9. present — 结果呈现 → ToolResultEnvelope
|
|
213
|
+
10. output_manager — 输出大小管控
|
|
214
|
+
|
|
215
|
+
headless 模式(API 调用、CI、批处理):
|
|
216
|
+
- check_permissions 返回 ask 时自动降级为 deny(不等待人工确认)
|
|
217
|
+
- 对应 CC 的 shouldAvoidPermissionPrompts=True / isNonInteractiveSession=True
|
|
218
|
+
|
|
219
|
+
交互模式(headless=False,默认):
|
|
220
|
+
- check_permissions 返回 ask 时抛出 PermissionAskInterrupt
|
|
221
|
+
- 由 LangGraph checkpointer + HumanInTheLoopMiddleware 捕获
|
|
222
|
+
|
|
223
|
+
pipeline 不负责:
|
|
224
|
+
- 将 envelope 映射为 LangChain tool return(由 LangChainAdapter 负责)
|
|
225
|
+
- 决定 ToolNode 路由
|
|
226
|
+
- 管理 agent loop
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
_ASK_HEADLESS_DENY_MSG = (
|
|
230
|
+
"Operation requires interactive user approval, "
|
|
231
|
+
"which is not available in headless/non-interactive mode."
|
|
232
|
+
)
|
|
233
|
+
_ASK_SYNC_DENY_MSG = (
|
|
234
|
+
"Synchronous pipeline does not support interactive permission ask. "
|
|
235
|
+
"Use async pipeline with permission_resolver or run in headless mode."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
def __init__(
|
|
239
|
+
self,
|
|
240
|
+
*,
|
|
241
|
+
output_manager: ToolOutputManager | None = None,
|
|
242
|
+
headless: bool = False,
|
|
243
|
+
identical_call_memo: IdenticalCallMemo | None = None,
|
|
244
|
+
permission_resolver: PermissionResolver | None = None,
|
|
245
|
+
) -> None:
|
|
246
|
+
self._output_manager = output_manager or ToolOutputManager()
|
|
247
|
+
self._headless = headless
|
|
248
|
+
self._identical_call_memo = identical_call_memo if identical_call_memo is not None else IdenticalCallMemo()
|
|
249
|
+
self._permission_resolver = permission_resolver
|
|
250
|
+
|
|
251
|
+
def run(
|
|
252
|
+
self,
|
|
253
|
+
*,
|
|
254
|
+
tool: RuntimeTool,
|
|
255
|
+
raw_input: dict[str, Any],
|
|
256
|
+
ctx: ToolExecutionContext,
|
|
257
|
+
) -> ToolResultEnvelope:
|
|
258
|
+
"""同步执行完整 pipeline,始终返回 ToolResultEnvelope。"""
|
|
259
|
+
log = ObservabilityLoggerAdapter(
|
|
260
|
+
logger,
|
|
261
|
+
build_log_context(
|
|
262
|
+
run_id=getattr(ctx, "run_id", None),
|
|
263
|
+
session_id=getattr(ctx, "thread_id", None),
|
|
264
|
+
tool_call_id=getattr(ctx, "tool_call_id", None),
|
|
265
|
+
agent_id=getattr(ctx, "agent_id", None),
|
|
266
|
+
).as_extra(),
|
|
267
|
+
)
|
|
268
|
+
log.info("tool pipeline run start tool=%s", tool.name)
|
|
269
|
+
try:
|
|
270
|
+
out = self._run(tool=tool, raw_input=raw_input, ctx=ctx)
|
|
271
|
+
log.info("tool pipeline run done tool=%s status=%s", tool.name, out.status)
|
|
272
|
+
return out
|
|
273
|
+
except Exception as exc:
|
|
274
|
+
log.error("tool pipeline run failed tool=%s err=%s", tool.name, exc)
|
|
275
|
+
return exception_to_envelope(exc, tool.name)
|
|
276
|
+
|
|
277
|
+
def set_permission_resolver(self, resolver: PermissionResolver | None) -> None:
|
|
278
|
+
"""运行时更新 permission_resolver。"""
|
|
279
|
+
self._permission_resolver = resolver
|
|
280
|
+
|
|
281
|
+
async def arun(
|
|
282
|
+
self,
|
|
283
|
+
*,
|
|
284
|
+
tool: RuntimeTool,
|
|
285
|
+
raw_input: dict[str, Any],
|
|
286
|
+
ctx: ToolExecutionContext,
|
|
287
|
+
) -> ToolResultEnvelope:
|
|
288
|
+
"""异步执行完整 pipeline,始终返回 ToolResultEnvelope。"""
|
|
289
|
+
log = ObservabilityLoggerAdapter(
|
|
290
|
+
logger,
|
|
291
|
+
build_log_context(
|
|
292
|
+
run_id=getattr(ctx, "run_id", None),
|
|
293
|
+
session_id=getattr(ctx, "thread_id", None),
|
|
294
|
+
tool_call_id=getattr(ctx, "tool_call_id", None),
|
|
295
|
+
agent_id=getattr(ctx, "agent_id", None),
|
|
296
|
+
).as_extra(),
|
|
297
|
+
)
|
|
298
|
+
log.info("tool pipeline arun start tool=%s", tool.name)
|
|
299
|
+
try:
|
|
300
|
+
out = await self._arun(tool=tool, raw_input=raw_input, ctx=ctx)
|
|
301
|
+
log.info("tool pipeline arun done tool=%s status=%s", tool.name, out.status)
|
|
302
|
+
return out
|
|
303
|
+
except Exception as exc:
|
|
304
|
+
log.error("tool pipeline arun failed tool=%s err=%s", tool.name, exc)
|
|
305
|
+
return exception_to_envelope(exc, tool.name)
|
|
306
|
+
|
|
307
|
+
# ---- 内部实现 ----
|
|
308
|
+
|
|
309
|
+
def _run(
|
|
310
|
+
self,
|
|
311
|
+
*,
|
|
312
|
+
tool: RuntimeTool,
|
|
313
|
+
raw_input: dict[str, Any],
|
|
314
|
+
ctx: ToolExecutionContext,
|
|
315
|
+
) -> ToolResultEnvelope:
|
|
316
|
+
# Step 1: normalize_input
|
|
317
|
+
normalized = tool.normalize_input(raw_input, ctx)
|
|
318
|
+
|
|
319
|
+
# Step 2: schema parse
|
|
320
|
+
parsed_data, schema_error = self._parse_schema(tool, normalized)
|
|
321
|
+
if schema_error is not None:
|
|
322
|
+
return schema_error
|
|
323
|
+
|
|
324
|
+
# Step 3: validate_input
|
|
325
|
+
validation = tool.validate_input(parsed_data, ctx)
|
|
326
|
+
if not validation.ok:
|
|
327
|
+
return validation_error_envelope(
|
|
328
|
+
tool.name,
|
|
329
|
+
validation.message or "Input validation failed.",
|
|
330
|
+
code=validation.code,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Step 4: check_permissions
|
|
334
|
+
auth = tool.check_permissions(parsed_data, ctx)
|
|
335
|
+
if auth.behavior == "deny":
|
|
336
|
+
logger.warning("tool permission denied tool=%s policy_id=%s", tool.name, auth.policy_id)
|
|
337
|
+
env = blocked_envelope(
|
|
338
|
+
tool.name,
|
|
339
|
+
auth.message or "Permission denied.",
|
|
340
|
+
policy_id=auth.policy_id,
|
|
341
|
+
)
|
|
342
|
+
self._attach_runtime_observability(
|
|
343
|
+
env,
|
|
344
|
+
ctx=ctx,
|
|
345
|
+
events=[
|
|
346
|
+
{"event": "permission_checked", "behavior": "deny", "policy_id": auth.policy_id},
|
|
347
|
+
],
|
|
348
|
+
decision_trace=(auth.metadata or {}).get("decision_trace"),
|
|
349
|
+
)
|
|
350
|
+
return env
|
|
351
|
+
if auth.behavior == "ask":
|
|
352
|
+
logger.warning(
|
|
353
|
+
"tool permission requires ask tool=%s headless=%s policy_id=%s",
|
|
354
|
+
tool.name,
|
|
355
|
+
self._headless,
|
|
356
|
+
auth.policy_id,
|
|
357
|
+
)
|
|
358
|
+
env = blocked_envelope(
|
|
359
|
+
tool.name,
|
|
360
|
+
self._ASK_HEADLESS_DENY_MSG if self._headless else self._ASK_SYNC_DENY_MSG,
|
|
361
|
+
policy_id=auth.policy_id,
|
|
362
|
+
)
|
|
363
|
+
self._attach_runtime_observability(
|
|
364
|
+
env,
|
|
365
|
+
ctx=ctx,
|
|
366
|
+
events=[
|
|
367
|
+
{"event": "permission_checked", "behavior": "ask_sync_denied", "policy_id": auth.policy_id},
|
|
368
|
+
],
|
|
369
|
+
decision_trace=(auth.metadata or {}).get("decision_trace"),
|
|
370
|
+
)
|
|
371
|
+
return env
|
|
372
|
+
# allow:可能有 updated_input(策略层路径规范化回写)
|
|
373
|
+
if auth.updated_input:
|
|
374
|
+
parsed_data = auth.updated_input
|
|
375
|
+
|
|
376
|
+
dedup_key: str | None = None
|
|
377
|
+
if getattr(tool, "dedupe_identical_calls", False):
|
|
378
|
+
dedup_key = dedup_cache_key(ctx, tool.name, parsed_data)
|
|
379
|
+
cached = self._identical_call_memo.get(dedup_key)
|
|
380
|
+
if cached is not None:
|
|
381
|
+
out = envelope_with_dedup_notice(cached, ctx=ctx)
|
|
382
|
+
self._attach_runtime_observability(
|
|
383
|
+
out,
|
|
384
|
+
ctx=ctx,
|
|
385
|
+
events=[
|
|
386
|
+
{"event": "identical_call_dedup", "behavior": "cache_hit"},
|
|
387
|
+
],
|
|
388
|
+
decision_trace=(auth.metadata or {}).get("decision_trace"),
|
|
389
|
+
)
|
|
390
|
+
return self._output_manager.truncate_if_needed(
|
|
391
|
+
out, tool, ctx.tool_call_id
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Step 5: before_invoke
|
|
395
|
+
tool.before_invoke(parsed_data, ctx)
|
|
396
|
+
|
|
397
|
+
# Step 6: invoke
|
|
398
|
+
result = tool.invoke(parsed_data, ctx)
|
|
399
|
+
|
|
400
|
+
# Step 7: after_invoke
|
|
401
|
+
tool.after_invoke(parsed_data, result, ctx)
|
|
402
|
+
|
|
403
|
+
# Step 9: present
|
|
404
|
+
envelope = tool.present(parsed_data, result, ctx)
|
|
405
|
+
self._attach_runtime_observability(
|
|
406
|
+
envelope,
|
|
407
|
+
ctx=ctx,
|
|
408
|
+
events=[
|
|
409
|
+
{"event": "permission_checked", "behavior": "allow", "policy_id": auth.policy_id},
|
|
410
|
+
{"event": "invoke_completed", "tool_name": tool.name},
|
|
411
|
+
],
|
|
412
|
+
decision_trace=(auth.metadata or {}).get("decision_trace"),
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Step 10: output_manager
|
|
416
|
+
final = self._output_manager.truncate_if_needed(
|
|
417
|
+
envelope, tool, ctx.tool_call_id
|
|
418
|
+
)
|
|
419
|
+
if dedup_key is not None:
|
|
420
|
+
self._identical_call_memo.put(dedup_key, final)
|
|
421
|
+
return final
|
|
422
|
+
|
|
423
|
+
async def _arun(
|
|
424
|
+
self,
|
|
425
|
+
*,
|
|
426
|
+
tool: RuntimeTool,
|
|
427
|
+
raw_input: dict[str, Any],
|
|
428
|
+
ctx: ToolExecutionContext,
|
|
429
|
+
) -> ToolResultEnvelope:
|
|
430
|
+
# Step 1: normalize_input
|
|
431
|
+
normalized = tool.normalize_input(raw_input, ctx)
|
|
432
|
+
|
|
433
|
+
# Step 2: schema parse
|
|
434
|
+
parsed_data, schema_error = self._parse_schema(tool, normalized)
|
|
435
|
+
if schema_error is not None:
|
|
436
|
+
return schema_error
|
|
437
|
+
|
|
438
|
+
# Step 3: validate_input
|
|
439
|
+
validation = tool.validate_input(parsed_data, ctx)
|
|
440
|
+
if not validation.ok:
|
|
441
|
+
return validation_error_envelope(
|
|
442
|
+
tool.name,
|
|
443
|
+
validation.message or "Input validation failed.",
|
|
444
|
+
code=validation.code,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Step 4: check_permissions(支持异步 policy)
|
|
448
|
+
if tool._policy is not None and hasattr(tool._policy, "aauthorize"):
|
|
449
|
+
auth = await tool._policy.aauthorize(
|
|
450
|
+
tool_name=tool.name, input_data=parsed_data, ctx=ctx
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
auth = tool.check_permissions(parsed_data, ctx)
|
|
454
|
+
|
|
455
|
+
if auth.behavior == "deny":
|
|
456
|
+
logger.warning("tool permission denied tool=%s policy_id=%s", tool.name, auth.policy_id)
|
|
457
|
+
env = blocked_envelope(
|
|
458
|
+
tool.name,
|
|
459
|
+
auth.message or "Permission denied.",
|
|
460
|
+
policy_id=auth.policy_id,
|
|
461
|
+
)
|
|
462
|
+
self._attach_runtime_observability(
|
|
463
|
+
env,
|
|
464
|
+
ctx=ctx,
|
|
465
|
+
events=[
|
|
466
|
+
{"event": "permission_checked", "behavior": "deny", "policy_id": auth.policy_id},
|
|
467
|
+
],
|
|
468
|
+
decision_trace=(auth.metadata or {}).get("decision_trace"),
|
|
469
|
+
)
|
|
470
|
+
return env
|
|
471
|
+
if auth.behavior == "ask":
|
|
472
|
+
logger.warning(
|
|
473
|
+
"tool permission requires ask tool=%s headless=%s policy_id=%s",
|
|
474
|
+
tool.name,
|
|
475
|
+
self._headless,
|
|
476
|
+
auth.policy_id,
|
|
477
|
+
)
|
|
478
|
+
if self._headless or self._permission_resolver is None:
|
|
479
|
+
env = blocked_envelope(
|
|
480
|
+
tool.name,
|
|
481
|
+
self._ASK_HEADLESS_DENY_MSG,
|
|
482
|
+
policy_id=auth.policy_id,
|
|
483
|
+
)
|
|
484
|
+
self._attach_runtime_observability(
|
|
485
|
+
env,
|
|
486
|
+
ctx=ctx,
|
|
487
|
+
events=[
|
|
488
|
+
{"event": "permission_checked", "behavior": "ask_auto_denied", "policy_id": auth.policy_id},
|
|
489
|
+
],
|
|
490
|
+
decision_trace=(auth.metadata or {}).get("decision_trace"),
|
|
491
|
+
)
|
|
492
|
+
return env
|
|
493
|
+
allowed = await self._permission_resolver(tool.name, auth.ask_prompt)
|
|
494
|
+
if not allowed:
|
|
495
|
+
env = blocked_envelope(
|
|
496
|
+
tool.name,
|
|
497
|
+
auth.ask_prompt or "User rejected the operation.",
|
|
498
|
+
policy_id=auth.policy_id,
|
|
499
|
+
)
|
|
500
|
+
self._attach_runtime_observability(
|
|
501
|
+
env,
|
|
502
|
+
ctx=ctx,
|
|
503
|
+
events=[
|
|
504
|
+
{
|
|
505
|
+
"event": "permission_checked",
|
|
506
|
+
"behavior": "ask_rejected_by_resolver",
|
|
507
|
+
"policy_id": auth.policy_id,
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
decision_trace=(auth.metadata or {}).get("decision_trace"),
|
|
511
|
+
)
|
|
512
|
+
return env
|
|
513
|
+
if auth.updated_input:
|
|
514
|
+
parsed_data = auth.updated_input
|
|
515
|
+
|
|
516
|
+
dedup_key: str | None = None
|
|
517
|
+
if getattr(tool, "dedupe_identical_calls", False):
|
|
518
|
+
dedup_key = dedup_cache_key(ctx, tool.name, parsed_data)
|
|
519
|
+
cached = self._identical_call_memo.get(dedup_key)
|
|
520
|
+
if cached is not None:
|
|
521
|
+
out = envelope_with_dedup_notice(cached, ctx=ctx)
|
|
522
|
+
self._attach_runtime_observability(
|
|
523
|
+
out,
|
|
524
|
+
ctx=ctx,
|
|
525
|
+
events=[
|
|
526
|
+
{"event": "identical_call_dedup", "behavior": "cache_hit"},
|
|
527
|
+
],
|
|
528
|
+
decision_trace=(auth.metadata or {}).get("decision_trace"),
|
|
529
|
+
)
|
|
530
|
+
return self._output_manager.truncate_if_needed(
|
|
531
|
+
out, tool, ctx.tool_call_id
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# Step 5: before_invoke
|
|
535
|
+
tool.before_invoke(parsed_data, ctx)
|
|
536
|
+
|
|
537
|
+
# Step 6: ainvoke
|
|
538
|
+
result = await tool.ainvoke(parsed_data, ctx)
|
|
539
|
+
|
|
540
|
+
# Step 7: after_invoke
|
|
541
|
+
tool.after_invoke(parsed_data, result, ctx)
|
|
542
|
+
|
|
543
|
+
# Step 9: present
|
|
544
|
+
envelope = tool.present(parsed_data, result, ctx)
|
|
545
|
+
self._attach_runtime_observability(
|
|
546
|
+
envelope,
|
|
547
|
+
ctx=ctx,
|
|
548
|
+
events=[
|
|
549
|
+
{"event": "permission_checked", "behavior": "allow", "policy_id": auth.policy_id},
|
|
550
|
+
{"event": "invoke_completed", "tool_name": tool.name},
|
|
551
|
+
],
|
|
552
|
+
decision_trace=(auth.metadata or {}).get("decision_trace"),
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Step 10: output_manager
|
|
556
|
+
final = self._output_manager.truncate_if_needed(
|
|
557
|
+
envelope, tool, ctx.tool_call_id
|
|
558
|
+
)
|
|
559
|
+
if dedup_key is not None:
|
|
560
|
+
self._identical_call_memo.put(dedup_key, final)
|
|
561
|
+
return final
|
|
562
|
+
|
|
563
|
+
@staticmethod
|
|
564
|
+
def _parse_schema(
|
|
565
|
+
tool: RuntimeTool,
|
|
566
|
+
data: dict[str, Any],
|
|
567
|
+
) -> tuple[dict[str, Any], ToolResultEnvelope | None]:
|
|
568
|
+
"""
|
|
569
|
+
用 tool.input_model 做 Pydantic schema 解析。
|
|
570
|
+
成功返回 (parsed_dict, None),失败返回 ({}, error_envelope)。
|
|
571
|
+
"""
|
|
572
|
+
try:
|
|
573
|
+
parsed = tool.input_model.model_validate(data)
|
|
574
|
+
return parsed.model_dump(), None
|
|
575
|
+
except ValidationError as exc:
|
|
576
|
+
detail = "; ".join(
|
|
577
|
+
f"{'.'.join(str(l) for l in e['loc'])}: {e['msg']}"
|
|
578
|
+
for e in exc.errors()
|
|
579
|
+
)
|
|
580
|
+
envelope = validation_error_envelope(
|
|
581
|
+
tool.name,
|
|
582
|
+
f"Input schema error: {detail}",
|
|
583
|
+
code="INPUT_SCHEMA_ERROR",
|
|
584
|
+
)
|
|
585
|
+
return {}, envelope
|
|
586
|
+
except Exception as exc:
|
|
587
|
+
return {}, exception_to_envelope(exc, tool.name)
|
|
588
|
+
|
|
589
|
+
@staticmethod
|
|
590
|
+
def _attach_runtime_observability(
|
|
591
|
+
envelope: ToolResultEnvelope,
|
|
592
|
+
*,
|
|
593
|
+
ctx: ToolExecutionContext,
|
|
594
|
+
events: list[dict[str, Any]],
|
|
595
|
+
decision_trace: list[dict[str, Any]] | None = None,
|
|
596
|
+
) -> None:
|
|
597
|
+
meta = dict(envelope.meta or {})
|
|
598
|
+
obs = meta.get("observability")
|
|
599
|
+
if not isinstance(obs, dict):
|
|
600
|
+
obs = {
|
|
601
|
+
"schema_version": OBS_SCHEMA_VERSION,
|
|
602
|
+
"trace_context": {
|
|
603
|
+
"thread_id": ctx.thread_id,
|
|
604
|
+
"run_id": ctx.run_id,
|
|
605
|
+
"tool_call_id": ctx.tool_call_id,
|
|
606
|
+
"tool_name": ctx.tool_name,
|
|
607
|
+
},
|
|
608
|
+
"scenario": "runtime_pipeline",
|
|
609
|
+
"events": [],
|
|
610
|
+
}
|
|
611
|
+
merged_events = list(obs.get("events", []))
|
|
612
|
+
start_seq = len(merged_events)
|
|
613
|
+
for i, ev in enumerate(events, start=1):
|
|
614
|
+
merged_events.append({"seq": start_seq + i, **ev})
|
|
615
|
+
obs["events"] = merged_events
|
|
616
|
+
meta["observability"] = obs
|
|
617
|
+
if decision_trace:
|
|
618
|
+
meta.setdefault("decision_trace", decision_trace)
|
|
619
|
+
if ctx.tool_call_id:
|
|
620
|
+
meta.setdefault("tool_call_id", ctx.tool_call_id)
|
|
621
|
+
envelope.meta = meta
|