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,832 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangChain AgentX Event Adapter
|
|
3
|
+
============================
|
|
4
|
+
|
|
5
|
+
将 LangGraph `astream_events` 的原始事件流,转换为 LangChain AgentX 风格的统一事件。
|
|
6
|
+
|
|
7
|
+
核心目标:
|
|
8
|
+
- 保持 LangGraph 流式特性(AsyncIterator)
|
|
9
|
+
- 按文档中定义的事件语义进行 1:1 / 1:N 映射
|
|
10
|
+
- 提供简单、前端友好的结构化事件
|
|
11
|
+
|
|
12
|
+
与 CC(Claude Code)式主循环对齐时的扩展方向(withhold、terminal_reason、步骤边界与新图节点)见
|
|
13
|
+
docs/design-docs/observability/event-system.md 中「CC 对齐:事件层改进方向」。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import time
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from typing import Any, AsyncIterator, Optional, Dict, AsyncGenerator
|
|
23
|
+
|
|
24
|
+
from langchain_core.messages import AIMessage
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LangchainAgentEventType(Enum):
|
|
28
|
+
# 会话级别
|
|
29
|
+
START = "start"
|
|
30
|
+
FINISH = "finish"
|
|
31
|
+
|
|
32
|
+
# 推理/思考
|
|
33
|
+
REASONING_START = "reasoning-start"
|
|
34
|
+
REASONING_DELTA = "reasoning-delta"
|
|
35
|
+
REASONING_END = "reasoning-end"
|
|
36
|
+
|
|
37
|
+
# 文字生成
|
|
38
|
+
TEXT_START = "text-start"
|
|
39
|
+
TEXT_DELTA = "text-delta"
|
|
40
|
+
TEXT_END = "text-end"
|
|
41
|
+
|
|
42
|
+
# 工具调用
|
|
43
|
+
TOOL_INPUT = "tool-input"
|
|
44
|
+
TOOL_INPUT_START = "tool-input-start"
|
|
45
|
+
TOOL_INPUT_DELTA = "tool-input-delta"
|
|
46
|
+
TOOL_CALL = "tool-call"
|
|
47
|
+
TOOL_RESULT = "tool-result"
|
|
48
|
+
TOOL_ERROR = "tool-error"
|
|
49
|
+
|
|
50
|
+
# 步骤/循环
|
|
51
|
+
START_STEP = "start-step"
|
|
52
|
+
FINISH_STEP = "finish-step"
|
|
53
|
+
|
|
54
|
+
# Hook 执行
|
|
55
|
+
HOOK_STARTED = "hook-started"
|
|
56
|
+
HOOK_FINISHED = "hook-finished"
|
|
57
|
+
|
|
58
|
+
# 上下文压缩
|
|
59
|
+
COMPACT_START = "compact-start"
|
|
60
|
+
COMPACT_END = "compact-end"
|
|
61
|
+
|
|
62
|
+
# 子 Agent
|
|
63
|
+
SUBAGENT_START = "subagent-start"
|
|
64
|
+
SUBAGENT_END = "subagent-end"
|
|
65
|
+
|
|
66
|
+
# 错误
|
|
67
|
+
ERROR = "error"
|
|
68
|
+
|
|
69
|
+
# Task Runtime(Phase D)
|
|
70
|
+
TASK_NOTIFICATION_ENQUEUED = "task-notification-enqueued"
|
|
71
|
+
TASK_NOTIFICATION_CONSUMED = "task-notification-consumed"
|
|
72
|
+
TASK_TERMINATED = "task-terminated"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class LangchainAgentEvent:
|
|
77
|
+
"""LangChain AgentX 风格事件"""
|
|
78
|
+
|
|
79
|
+
event_type: LangchainAgentEventType
|
|
80
|
+
data: Dict[str, Any]
|
|
81
|
+
step_index: Optional[int] = None
|
|
82
|
+
tool_name: Optional[str] = None
|
|
83
|
+
session_id: Optional[str] = None
|
|
84
|
+
timestamp: float = field(default_factory=lambda: time.time())
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class EventAdapterState:
|
|
89
|
+
"""适配器状态"""
|
|
90
|
+
|
|
91
|
+
# 图级别
|
|
92
|
+
graph_name: str = ""
|
|
93
|
+
session_id: Optional[str] = None
|
|
94
|
+
is_graph_started: bool = False
|
|
95
|
+
|
|
96
|
+
# 步骤级别
|
|
97
|
+
step_index: int = 0
|
|
98
|
+
is_step_started: bool = False
|
|
99
|
+
step_start_node: str = ""
|
|
100
|
+
|
|
101
|
+
# 模型级别
|
|
102
|
+
is_model_started: bool = False
|
|
103
|
+
is_reasoning_active: bool = False
|
|
104
|
+
is_text_active: bool = False
|
|
105
|
+
has_reasoning: bool = False # 是否是推理模型
|
|
106
|
+
|
|
107
|
+
# 工具级别
|
|
108
|
+
current_tool: Optional[str] = None
|
|
109
|
+
tool_input_chunks: list[str] = field(default_factory=list)
|
|
110
|
+
|
|
111
|
+
# 本轮答案累积(用于在 finish 事件中携带完整 answer;优先 text,其次 reason,最后 on_chain_end)
|
|
112
|
+
accumulated_answer: str = "" # 仅累积 text(最后一轮 model)
|
|
113
|
+
accumulated_reasoning: str = "" # 仅累积 reasoning(最后一轮 model,CoT 无 text 时作 answer)
|
|
114
|
+
|
|
115
|
+
# 辅助判断
|
|
116
|
+
last_chain_end_name: str = ""
|
|
117
|
+
|
|
118
|
+
# Loop 语义观测(对齐 CC / 方案文档的增量字段)
|
|
119
|
+
#
|
|
120
|
+
# 说明:
|
|
121
|
+
# - LangGraph astream_events 的每个事件只携带局部 data(input/output/chunk)。
|
|
122
|
+
# - 为了在 LangchainAgent 事件里稳定暴露“本轮 loop 的原因/终止语义”等字段,
|
|
123
|
+
# 我们缓存最近一次 on_chain_end 输出里出现的 loop 状态片段,供 start/finish-step/finish 事件携带。
|
|
124
|
+
last_loop_snapshot: Dict[str, Any] = field(default_factory=dict)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class LangGraphToLangchainAgentEventAdapter:
|
|
128
|
+
"""LangChain AgentX 风格事件适配器(基于 LangGraph astream_events)"""
|
|
129
|
+
|
|
130
|
+
def __init__(self, graph_name: str = "LangGraph", config: Optional[dict] = None) -> None:
|
|
131
|
+
self.graph_name = graph_name
|
|
132
|
+
self.state = EventAdapterState()
|
|
133
|
+
|
|
134
|
+
config = config or {}
|
|
135
|
+
|
|
136
|
+
# 步骤边界配置
|
|
137
|
+
self.loop_entry_nodes = set(config.get("loop_entry_nodes", {"hook_before_model"}))
|
|
138
|
+
self.loop_end_nodes = set(
|
|
139
|
+
config.get("loop_end_nodes", {"prune_and_compress", "loop_controller"})
|
|
140
|
+
)
|
|
141
|
+
self.hook_node_names = set(
|
|
142
|
+
config.get(
|
|
143
|
+
"hook_node_names",
|
|
144
|
+
{
|
|
145
|
+
"hook_loop_start",
|
|
146
|
+
"hook_before_model",
|
|
147
|
+
"hook_after_model",
|
|
148
|
+
"hook_stop",
|
|
149
|
+
"hook_loop_end",
|
|
150
|
+
},
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
self.compact_node_names = set(config.get("compact_node_names", {"prune_and_compress"}))
|
|
154
|
+
self.agent_tool_names = set(config.get("agent_tool_names", {"agent", "AgentTool"}))
|
|
155
|
+
|
|
156
|
+
# 开关配置
|
|
157
|
+
self.enable_reasoning_events: bool = config.get("enable_reasoning_events", True)
|
|
158
|
+
self.enable_step_events: bool = config.get("enable_step_events", True)
|
|
159
|
+
self.enable_hook_events: bool = config.get("enable_hook_events", True)
|
|
160
|
+
self.enable_compact_events: bool = config.get("enable_compact_events", True)
|
|
161
|
+
self.enable_subagent_events: bool = config.get("enable_subagent_events", True)
|
|
162
|
+
self.synthesize_tool_call: bool = config.get("synthesize_tool_call", True)
|
|
163
|
+
self.enable_tool_input_start: bool = config.get("enable_tool_input_start", False)
|
|
164
|
+
|
|
165
|
+
async def adapt(
|
|
166
|
+
self, events: AsyncIterator[Dict[str, Any]]
|
|
167
|
+
) -> AsyncIterator[LangchainAgentEvent]:
|
|
168
|
+
"""主转换函数:将 LangGraph 事件流转换为 LangChain AgentX 风格事件流。"""
|
|
169
|
+
|
|
170
|
+
async for event in events:
|
|
171
|
+
event_type = event.get("event")
|
|
172
|
+
name = event.get("name", "") or ""
|
|
173
|
+
data = event.get("data", {}) or {}
|
|
174
|
+
|
|
175
|
+
if event_type == "on_chain_start":
|
|
176
|
+
async for oc_event in self._handle_chain_start(name, data):
|
|
177
|
+
yield oc_event
|
|
178
|
+
|
|
179
|
+
elif event_type == "on_chain_end":
|
|
180
|
+
async for oc_event in self._handle_chain_end(name, data):
|
|
181
|
+
yield oc_event
|
|
182
|
+
|
|
183
|
+
elif event_type == "on_chat_model_start":
|
|
184
|
+
async for oc_event in self._handle_model_start(name, data):
|
|
185
|
+
if oc_event is not None:
|
|
186
|
+
yield oc_event
|
|
187
|
+
|
|
188
|
+
elif event_type == "on_chat_model_stream":
|
|
189
|
+
async for oc_event in self._handle_model_stream(name, data):
|
|
190
|
+
if oc_event is not None:
|
|
191
|
+
yield oc_event
|
|
192
|
+
|
|
193
|
+
elif event_type == "on_chat_model_end":
|
|
194
|
+
async for oc_event in self._handle_model_end(name, data):
|
|
195
|
+
if oc_event is not None:
|
|
196
|
+
yield oc_event
|
|
197
|
+
|
|
198
|
+
elif event_type == "on_tool_start":
|
|
199
|
+
async for oc_event in self._handle_tool_start(name, data):
|
|
200
|
+
yield oc_event
|
|
201
|
+
|
|
202
|
+
elif event_type == "on_tool_end":
|
|
203
|
+
async for oc_event in self._handle_tool_end(name, data):
|
|
204
|
+
yield oc_event
|
|
205
|
+
|
|
206
|
+
elif event_type == "on_chain_error":
|
|
207
|
+
async for oc_event in self._handle_error(name, data):
|
|
208
|
+
yield oc_event
|
|
209
|
+
|
|
210
|
+
def _event(
|
|
211
|
+
self,
|
|
212
|
+
*,
|
|
213
|
+
event_type: LangchainAgentEventType,
|
|
214
|
+
data: Dict[str, Any],
|
|
215
|
+
step_index: Optional[int] = None,
|
|
216
|
+
tool_name: Optional[str] = None,
|
|
217
|
+
session_id: Optional[str] = None,
|
|
218
|
+
) -> LangchainAgentEvent:
|
|
219
|
+
return LangchainAgentEvent(
|
|
220
|
+
event_type=event_type,
|
|
221
|
+
data=data,
|
|
222
|
+
step_index=step_index,
|
|
223
|
+
tool_name=tool_name,
|
|
224
|
+
session_id=session_id if session_id is not None else self.state.session_id,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def _reset_state(self) -> None:
|
|
228
|
+
"""重置内部状态(会话结束后调用)。"""
|
|
229
|
+
self.state = EventAdapterState()
|
|
230
|
+
|
|
231
|
+
# ===== Chain 事件处理 =====
|
|
232
|
+
|
|
233
|
+
async def _handle_chain_start(
|
|
234
|
+
self, name: str, data: Dict[str, Any]
|
|
235
|
+
) -> AsyncGenerator[LangchainAgentEvent, None]:
|
|
236
|
+
"""处理 on_chain_start"""
|
|
237
|
+
|
|
238
|
+
# 情况 1:图开始
|
|
239
|
+
if not self.state.is_graph_started:
|
|
240
|
+
self.state.graph_name = name or self.graph_name
|
|
241
|
+
input_data = data.get("input") if isinstance(data, dict) else None
|
|
242
|
+
if isinstance(input_data, dict):
|
|
243
|
+
sid = input_data.get("_session_id")
|
|
244
|
+
if isinstance(sid, str) and sid:
|
|
245
|
+
self.state.session_id = sid
|
|
246
|
+
self.state.is_graph_started = True
|
|
247
|
+
yield self._event(event_type=LangchainAgentEventType.START, data={"graph_name": self.state.graph_name})
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
# 情况 2:步骤开始(loop_entry_node)
|
|
251
|
+
#
|
|
252
|
+
# 严格成对策略(与 factory.py 的循环语义对齐):
|
|
253
|
+
# - 一个循环(step)定义为:从一次进入 loop_entry_node 开始,
|
|
254
|
+
# 到下一次进入 loop_entry_node 之前(或图结束)为止。
|
|
255
|
+
# - 因此,finish-step 不应依赖 loop_controller / prune_and_compress 等中间节点,
|
|
256
|
+
# 而应在“下一次 loop_entry_node 发生前”或“图结束时”补齐。
|
|
257
|
+
if self.enable_step_events and self._is_loop_entry_node(name):
|
|
258
|
+
# 如果上一个 step 仍处于打开状态,先在本次 step 开始前关闭它
|
|
259
|
+
if self.state.is_step_started and self.state.step_index > 0:
|
|
260
|
+
yield self._event(
|
|
261
|
+
event_type=LangchainAgentEventType.FINISH_STEP,
|
|
262
|
+
data={
|
|
263
|
+
"step_index": self.state.step_index,
|
|
264
|
+
"loop": self._build_loop_observation_payload(),
|
|
265
|
+
},
|
|
266
|
+
step_index=self.state.step_index,
|
|
267
|
+
)
|
|
268
|
+
self.state.is_step_started = False
|
|
269
|
+
|
|
270
|
+
self.state.step_index += 1
|
|
271
|
+
self.state.is_step_started = True
|
|
272
|
+
self.state.step_start_node = name
|
|
273
|
+
# 每进入新 step 清空累积,只保留「最后一轮」model 的 text + reason
|
|
274
|
+
self.state.accumulated_answer = ""
|
|
275
|
+
self.state.accumulated_reasoning = ""
|
|
276
|
+
|
|
277
|
+
yield self._event(
|
|
278
|
+
event_type=LangchainAgentEventType.START_STEP,
|
|
279
|
+
data={
|
|
280
|
+
"step_index": self.state.step_index,
|
|
281
|
+
"loop": self._build_loop_observation_payload(),
|
|
282
|
+
},
|
|
283
|
+
step_index=self.state.step_index,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if self.enable_hook_events and self._is_hook_node(name):
|
|
287
|
+
yield self._event(
|
|
288
|
+
event_type=LangchainAgentEventType.HOOK_STARTED,
|
|
289
|
+
data={
|
|
290
|
+
"hook_event": self._hook_event_from_name(name),
|
|
291
|
+
"hook_name": name,
|
|
292
|
+
},
|
|
293
|
+
step_index=self.state.step_index or None,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if self.enable_compact_events and self._is_compact_node(name):
|
|
297
|
+
yield self._event(
|
|
298
|
+
event_type=LangchainAgentEventType.COMPACT_START,
|
|
299
|
+
data={},
|
|
300
|
+
step_index=self.state.step_index or None,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def _build_loop_observation_payload(self) -> Dict[str, Any]:
|
|
304
|
+
"""构造可稳定输出的 loop 观测字段(即使为空也输出键)。"""
|
|
305
|
+
|
|
306
|
+
snap = self.state.last_loop_snapshot or {}
|
|
307
|
+
|
|
308
|
+
def _get(key: str) -> Any:
|
|
309
|
+
return snap.get(key)
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
"turn_count": _get("turn_count"),
|
|
313
|
+
"transition": _get("transition"),
|
|
314
|
+
"terminal_reason": _get("terminal_reason"),
|
|
315
|
+
"should_end": _get("should_end"),
|
|
316
|
+
"finish_reason": _get("finish_reason"),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
def _maybe_capture_loop_snapshot(self, data: Dict[str, Any]) -> None:
|
|
320
|
+
"""从 on_chain_end 的 output 中捕获 loop 状态片段(best-effort)。"""
|
|
321
|
+
|
|
322
|
+
output = data.get("output") if isinstance(data, dict) else None
|
|
323
|
+
if not isinstance(output, dict):
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
keys = {
|
|
327
|
+
"turn_count",
|
|
328
|
+
"transition",
|
|
329
|
+
"terminal_reason",
|
|
330
|
+
"should_end",
|
|
331
|
+
"finish_reason",
|
|
332
|
+
}
|
|
333
|
+
captured = {k: output.get(k) for k in keys if k in output}
|
|
334
|
+
if captured:
|
|
335
|
+
self.state.last_loop_snapshot.update(captured)
|
|
336
|
+
|
|
337
|
+
def _is_loop_entry_node(self, name: str) -> bool:
|
|
338
|
+
"""判断是否是循环入口节点"""
|
|
339
|
+
if name in self.loop_entry_nodes:
|
|
340
|
+
return True
|
|
341
|
+
if name.endswith(".before_model"):
|
|
342
|
+
return True
|
|
343
|
+
return False
|
|
344
|
+
|
|
345
|
+
def _is_hook_node(self, name: str) -> bool:
|
|
346
|
+
return name in self.hook_node_names
|
|
347
|
+
|
|
348
|
+
def _is_compact_node(self, name: str) -> bool:
|
|
349
|
+
return name in self.compact_node_names
|
|
350
|
+
|
|
351
|
+
def _is_agent_tool(self, name: str) -> bool:
|
|
352
|
+
return name in self.agent_tool_names
|
|
353
|
+
|
|
354
|
+
def _hook_event_from_name(self, name: str) -> str:
|
|
355
|
+
mapping = {
|
|
356
|
+
"hook_loop_start": "loop_start",
|
|
357
|
+
"hook_before_model": "before_model",
|
|
358
|
+
"hook_after_model": "after_model",
|
|
359
|
+
"hook_stop": "stop",
|
|
360
|
+
"hook_loop_end": "loop_end",
|
|
361
|
+
}
|
|
362
|
+
return mapping.get(name, name)
|
|
363
|
+
|
|
364
|
+
def _extract_answer_from_chain_end_output(self, data: Dict[str, Any]) -> str:
|
|
365
|
+
"""从 on_chain_end 的 output 中取最后一条 AIMessage.content。"""
|
|
366
|
+
output = data.get("output") if isinstance(data, dict) else None
|
|
367
|
+
if not isinstance(output, dict) or "messages" not in output:
|
|
368
|
+
return ""
|
|
369
|
+
messages = output.get("messages")
|
|
370
|
+
if not isinstance(messages, (list, tuple)):
|
|
371
|
+
return ""
|
|
372
|
+
for msg in reversed(messages):
|
|
373
|
+
if isinstance(msg, AIMessage):
|
|
374
|
+
content = getattr(msg, "content", None)
|
|
375
|
+
if isinstance(content, str):
|
|
376
|
+
return content
|
|
377
|
+
return ""
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def _extract_text_from_message_content(content: Any) -> str:
|
|
381
|
+
"""兼容 OpenAI(str) 与 Anthropic(content blocks list) 的最终文本提取。"""
|
|
382
|
+
if isinstance(content, str):
|
|
383
|
+
return content
|
|
384
|
+
if isinstance(content, list):
|
|
385
|
+
parts: list[str] = []
|
|
386
|
+
for item in content:
|
|
387
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
388
|
+
text = item.get("text")
|
|
389
|
+
if isinstance(text, str):
|
|
390
|
+
parts.append(text)
|
|
391
|
+
return "".join(parts)
|
|
392
|
+
return str(content) if content is not None else ""
|
|
393
|
+
|
|
394
|
+
def _extract_tool_result_text(self, output: Any) -> str:
|
|
395
|
+
"""多层降级提取工具结果正文,避免打印 ToolMessage/Envelope 的完整 repr。
|
|
396
|
+
|
|
397
|
+
降级顺序:.content → .payload → .summary → dict 键查找 → str() 兜底
|
|
398
|
+
"""
|
|
399
|
+
if output is None:
|
|
400
|
+
return ""
|
|
401
|
+
if isinstance(output, str):
|
|
402
|
+
return output
|
|
403
|
+
content = getattr(output, "content", None)
|
|
404
|
+
if content is not None:
|
|
405
|
+
return self._extract_text_from_message_content(content)
|
|
406
|
+
payload = getattr(output, "payload", None)
|
|
407
|
+
if payload is not None:
|
|
408
|
+
return str(payload)
|
|
409
|
+
summary = getattr(output, "summary", None)
|
|
410
|
+
if isinstance(summary, str) and summary:
|
|
411
|
+
return summary
|
|
412
|
+
if isinstance(output, dict):
|
|
413
|
+
for key in ("payload", "content", "summary", "output"):
|
|
414
|
+
if key in output:
|
|
415
|
+
return str(output[key])
|
|
416
|
+
return str(output)
|
|
417
|
+
|
|
418
|
+
async def _handle_chain_end(
|
|
419
|
+
self, name: str, data: Dict[str, Any]
|
|
420
|
+
) -> AsyncGenerator[LangchainAgentEvent, None]:
|
|
421
|
+
"""处理 on_chain_end"""
|
|
422
|
+
|
|
423
|
+
# task 事件:从节点 output.task_events 映射到 LangchainAgent 扩展事件
|
|
424
|
+
output = data.get("output") if isinstance(data, dict) else None
|
|
425
|
+
if isinstance(output, dict):
|
|
426
|
+
task_events = output.get("task_events")
|
|
427
|
+
if isinstance(task_events, list):
|
|
428
|
+
for task_event in task_events:
|
|
429
|
+
if not isinstance(task_event, dict):
|
|
430
|
+
continue
|
|
431
|
+
task_event_type = str(task_event.get("event_type") or "")
|
|
432
|
+
mapped = None
|
|
433
|
+
if task_event_type == "task-notification-enqueued":
|
|
434
|
+
mapped = LangchainAgentEventType.TASK_NOTIFICATION_ENQUEUED
|
|
435
|
+
elif task_event_type == "task-notification-consumed":
|
|
436
|
+
mapped = LangchainAgentEventType.TASK_NOTIFICATION_CONSUMED
|
|
437
|
+
elif task_event_type == "task-terminated":
|
|
438
|
+
mapped = LangchainAgentEventType.TASK_TERMINATED
|
|
439
|
+
if mapped is not None:
|
|
440
|
+
yield self._event(
|
|
441
|
+
event_type=mapped,
|
|
442
|
+
data=task_event,
|
|
443
|
+
step_index=self.state.step_index,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# 先捕获 loop 状态片段(供后续 step/finish 事件携带)
|
|
447
|
+
self._maybe_capture_loop_snapshot(data)
|
|
448
|
+
|
|
449
|
+
if self.enable_hook_events and self._is_hook_node(name):
|
|
450
|
+
output = data.get("output") if isinstance(data, dict) else None
|
|
451
|
+
outcome = "success"
|
|
452
|
+
if isinstance(output, dict):
|
|
453
|
+
outcome = str(output.get("outcome") or outcome)
|
|
454
|
+
yield self._event(
|
|
455
|
+
event_type=LangchainAgentEventType.HOOK_FINISHED,
|
|
456
|
+
data={
|
|
457
|
+
"hook_event": self._hook_event_from_name(name),
|
|
458
|
+
"hook_name": name,
|
|
459
|
+
"outcome": outcome,
|
|
460
|
+
},
|
|
461
|
+
step_index=self.state.step_index or None,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
if self.enable_compact_events and self._is_compact_node(name):
|
|
465
|
+
yield self._event(
|
|
466
|
+
event_type=LangchainAgentEventType.COMPACT_END,
|
|
467
|
+
data={},
|
|
468
|
+
step_index=self.state.step_index or None,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# 图结束:如果仍有打开的 step,先补齐 finish-step,再发 finish
|
|
472
|
+
if name == self.state.graph_name:
|
|
473
|
+
if self.enable_step_events and self.state.is_step_started and self.state.step_index > 0:
|
|
474
|
+
yield self._event(
|
|
475
|
+
event_type=LangchainAgentEventType.FINISH_STEP,
|
|
476
|
+
data={
|
|
477
|
+
"step_index": self.state.step_index,
|
|
478
|
+
"loop": self._build_loop_observation_payload(),
|
|
479
|
+
},
|
|
480
|
+
step_index=self.state.step_index,
|
|
481
|
+
)
|
|
482
|
+
self.state.is_step_started = False
|
|
483
|
+
|
|
484
|
+
# answer:优先 text,其次 reason(CoT 无 text 时),最后 on_chain_end 兜底
|
|
485
|
+
answer = self.state.accumulated_answer.strip()
|
|
486
|
+
if not answer:
|
|
487
|
+
answer = self.state.accumulated_reasoning.strip()
|
|
488
|
+
if not answer:
|
|
489
|
+
answer = self._extract_answer_from_chain_end_output(data)
|
|
490
|
+
loop_payload = self._build_loop_observation_payload()
|
|
491
|
+
yield self._event(
|
|
492
|
+
event_type=LangchainAgentEventType.FINISH,
|
|
493
|
+
data={
|
|
494
|
+
"graph_name": name,
|
|
495
|
+
"answer": answer,
|
|
496
|
+
"terminal_reason": loop_payload.get("terminal_reason"),
|
|
497
|
+
"loop": loop_payload,
|
|
498
|
+
},
|
|
499
|
+
)
|
|
500
|
+
self._reset_state()
|
|
501
|
+
|
|
502
|
+
def _is_step_end_node(self, name: str) -> bool:
|
|
503
|
+
"""判断是否是步骤结束节点"""
|
|
504
|
+
return name in self.loop_end_nodes
|
|
505
|
+
|
|
506
|
+
# ===== Model 事件处理 =====
|
|
507
|
+
|
|
508
|
+
async def _handle_model_start(
|
|
509
|
+
self, name: str, data: Dict[str, Any]
|
|
510
|
+
) -> AsyncGenerator[Optional[LangchainAgentEvent], None]:
|
|
511
|
+
"""处理 on_chat_model_start"""
|
|
512
|
+
self.state.is_model_started = True
|
|
513
|
+
# 每次 model 开始时清空累积,只保留最后一次 model 的 text + reason
|
|
514
|
+
self.state.accumulated_answer = ""
|
|
515
|
+
self.state.accumulated_reasoning = ""
|
|
516
|
+
# 暂不生成事件,等待 stream 时判断是否有 reasoning/text
|
|
517
|
+
if False: # pragma: no cover - 仅用于保持 async generator 形式
|
|
518
|
+
yield None
|
|
519
|
+
|
|
520
|
+
async def _handle_model_stream(
|
|
521
|
+
self, name: str, data: Dict[str, Any]
|
|
522
|
+
) -> AsyncGenerator[Optional[LangchainAgentEvent], None]:
|
|
523
|
+
"""处理 on_chat_model_stream"""
|
|
524
|
+
chunk = data.get("chunk")
|
|
525
|
+
if not chunk:
|
|
526
|
+
return
|
|
527
|
+
|
|
528
|
+
# ===== 处理 reasoning / text(对齐 LangChain content_blocks + additional_kwargs 语义)=====
|
|
529
|
+
|
|
530
|
+
reasoning_segments: list[str] = []
|
|
531
|
+
text_segments: list[str] = []
|
|
532
|
+
|
|
533
|
+
# 1. 优先解析 content_blocks(LangChain v1 标准)
|
|
534
|
+
try:
|
|
535
|
+
blocks = None
|
|
536
|
+
if hasattr(chunk, "content_blocks") and getattr(chunk, "content_blocks"):
|
|
537
|
+
blocks = getattr(chunk, "content_blocks")
|
|
538
|
+
elif isinstance(chunk, dict) and chunk.get("content_blocks"):
|
|
539
|
+
blocks = chunk["content_blocks"]
|
|
540
|
+
|
|
541
|
+
if blocks and isinstance(blocks, (list, tuple)):
|
|
542
|
+
for block in blocks:
|
|
543
|
+
if not isinstance(block, dict):
|
|
544
|
+
continue
|
|
545
|
+
btype = block.get("type", "")
|
|
546
|
+
if btype in ("reasoning", "thinking"):
|
|
547
|
+
r = block.get("reasoning") or block.get("thinking") or ""
|
|
548
|
+
if isinstance(r, str) and r:
|
|
549
|
+
reasoning_segments.append(r)
|
|
550
|
+
elif btype == "text":
|
|
551
|
+
t = block.get("text", "")
|
|
552
|
+
if isinstance(t, str) and t:
|
|
553
|
+
text_segments.append(t)
|
|
554
|
+
except Exception:
|
|
555
|
+
# content_blocks 解析失败则退回后续路径
|
|
556
|
+
pass
|
|
557
|
+
|
|
558
|
+
# 2. 若没有从 content_blocks 中拿到 reasoning,则补充检查 additional_kwargs
|
|
559
|
+
# - reasoning_content (str): Ollama, DeepSeek, XAI, Groq 等
|
|
560
|
+
# - reasoning (dict): OpenAI Responses 风格结构
|
|
561
|
+
if not reasoning_segments and hasattr(chunk, "additional_kwargs"):
|
|
562
|
+
ak = getattr(chunk, "additional_kwargs") or {}
|
|
563
|
+
if isinstance(ak, dict):
|
|
564
|
+
rc = ak.get("reasoning_content")
|
|
565
|
+
if isinstance(rc, str) and rc:
|
|
566
|
+
reasoning_segments.append(rc)
|
|
567
|
+
else:
|
|
568
|
+
robj = ak.get("reasoning")
|
|
569
|
+
if isinstance(robj, dict):
|
|
570
|
+
r = robj.get("reasoning")
|
|
571
|
+
if isinstance(r, str) and r:
|
|
572
|
+
reasoning_segments.append(r)
|
|
573
|
+
else:
|
|
574
|
+
summary = robj.get("summary")
|
|
575
|
+
if isinstance(summary, list):
|
|
576
|
+
for part in summary:
|
|
577
|
+
if isinstance(part, dict) and isinstance(
|
|
578
|
+
part.get("text"), str
|
|
579
|
+
):
|
|
580
|
+
reasoning_segments.append(part["text"])
|
|
581
|
+
|
|
582
|
+
# 3. 兼容 chunk.reasoning / chunk.content(如部分自定义 ChatModel)
|
|
583
|
+
if self.enable_reasoning_events and not reasoning_segments:
|
|
584
|
+
if hasattr(chunk, "reasoning") and getattr(chunk, "reasoning"):
|
|
585
|
+
r = getattr(chunk, "reasoning")
|
|
586
|
+
if isinstance(r, str) and r:
|
|
587
|
+
reasoning_segments.append(r)
|
|
588
|
+
|
|
589
|
+
if not text_segments and hasattr(chunk, "content"):
|
|
590
|
+
c = getattr(chunk, "content")
|
|
591
|
+
if isinstance(c, str) and c:
|
|
592
|
+
text_segments.append(c)
|
|
593
|
+
|
|
594
|
+
# ===== 发出 LangChain AgentX 风格 reasoning-* / text-* 事件 =====
|
|
595
|
+
|
|
596
|
+
# reasoning 流(推理模型)
|
|
597
|
+
if self.enable_reasoning_events and reasoning_segments:
|
|
598
|
+
combined_reasoning = "".join(reasoning_segments)
|
|
599
|
+
if combined_reasoning:
|
|
600
|
+
if not self.state.is_reasoning_active:
|
|
601
|
+
self.state.is_reasoning_active = True
|
|
602
|
+
self.state.has_reasoning = True
|
|
603
|
+
yield self._event(
|
|
604
|
+
event_type=LangchainAgentEventType.REASONING_START,
|
|
605
|
+
data={},
|
|
606
|
+
step_index=self.state.step_index,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
yield self._event(
|
|
610
|
+
event_type=LangchainAgentEventType.REASONING_DELTA,
|
|
611
|
+
data={"text": combined_reasoning},
|
|
612
|
+
step_index=self.state.step_index,
|
|
613
|
+
)
|
|
614
|
+
self.state.accumulated_reasoning += combined_reasoning
|
|
615
|
+
|
|
616
|
+
# 文本流(所有模型)
|
|
617
|
+
if text_segments:
|
|
618
|
+
combined_text = "".join(text_segments)
|
|
619
|
+
if combined_text:
|
|
620
|
+
if not self.state.is_text_active:
|
|
621
|
+
# 推理模型:reasoning 结束后才开始 text
|
|
622
|
+
if self.enable_reasoning_events and self.state.is_reasoning_active:
|
|
623
|
+
yield self._event(
|
|
624
|
+
event_type=LangchainAgentEventType.REASONING_END,
|
|
625
|
+
data={},
|
|
626
|
+
step_index=self.state.step_index,
|
|
627
|
+
)
|
|
628
|
+
self.state.is_reasoning_active = False
|
|
629
|
+
|
|
630
|
+
self.state.is_text_active = True
|
|
631
|
+
yield self._event(
|
|
632
|
+
event_type=LangchainAgentEventType.TEXT_START,
|
|
633
|
+
data={},
|
|
634
|
+
step_index=self.state.step_index,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
yield self._event(
|
|
638
|
+
event_type=LangchainAgentEventType.TEXT_DELTA,
|
|
639
|
+
data={"text": combined_text},
|
|
640
|
+
step_index=self.state.step_index,
|
|
641
|
+
)
|
|
642
|
+
self.state.accumulated_answer += combined_text
|
|
643
|
+
|
|
644
|
+
async def _handle_model_end(
|
|
645
|
+
self, name: str, data: Dict[str, Any]
|
|
646
|
+
) -> AsyncGenerator[Optional[LangchainAgentEvent], None]:
|
|
647
|
+
"""处理 on_chat_model_end"""
|
|
648
|
+
|
|
649
|
+
# 结束 reasoning(如果还在活跃)
|
|
650
|
+
if self.enable_reasoning_events and self.state.is_reasoning_active:
|
|
651
|
+
yield self._event(
|
|
652
|
+
event_type=LangchainAgentEventType.REASONING_END,
|
|
653
|
+
data={},
|
|
654
|
+
step_index=self.state.step_index,
|
|
655
|
+
)
|
|
656
|
+
self.state.is_reasoning_active = False
|
|
657
|
+
|
|
658
|
+
# 结束 text(如果还在活跃)
|
|
659
|
+
if self.state.is_text_active:
|
|
660
|
+
yield self._event(
|
|
661
|
+
event_type=LangchainAgentEventType.TEXT_END,
|
|
662
|
+
data={},
|
|
663
|
+
step_index=self.state.step_index,
|
|
664
|
+
)
|
|
665
|
+
self.state.is_text_active = False
|
|
666
|
+
|
|
667
|
+
self.state.is_model_started = False
|
|
668
|
+
|
|
669
|
+
# ===== Tool 事件处理 =====
|
|
670
|
+
|
|
671
|
+
async def _handle_tool_start(
|
|
672
|
+
self, name: str, data: Dict[str, Any]
|
|
673
|
+
) -> AsyncGenerator[LangchainAgentEvent, None]:
|
|
674
|
+
"""处理 on_tool_start - 一对多映射"""
|
|
675
|
+
|
|
676
|
+
self.state.current_tool = name
|
|
677
|
+
|
|
678
|
+
# 可选:tool-input-start
|
|
679
|
+
if self.enable_tool_input_start:
|
|
680
|
+
yield self._event(
|
|
681
|
+
event_type=LangchainAgentEventType.TOOL_INPUT_START,
|
|
682
|
+
data={"tool_name": name},
|
|
683
|
+
step_index=self.state.step_index,
|
|
684
|
+
tool_name=name,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
# 1. tool-input:完整的工具输入(归一化 input 为 dict,便于前端展示参数)
|
|
688
|
+
raw_input = data.get("input", data.get("inputs", {}))
|
|
689
|
+
if isinstance(raw_input, str):
|
|
690
|
+
try:
|
|
691
|
+
tool_input = json.loads(raw_input) if (raw_input and raw_input.strip()) else {}
|
|
692
|
+
except Exception:
|
|
693
|
+
tool_input = {}
|
|
694
|
+
elif isinstance(raw_input, dict):
|
|
695
|
+
tool_input = raw_input
|
|
696
|
+
else:
|
|
697
|
+
tool_input = {}
|
|
698
|
+
# 脱敏:去掉 tool_runtime 等内部 runtime 字段(避免前端序列化巨大对象)
|
|
699
|
+
if isinstance(tool_input, dict):
|
|
700
|
+
tool_input.pop("tool_runtime", None)
|
|
701
|
+
yield self._event(
|
|
702
|
+
event_type=LangchainAgentEventType.TOOL_INPUT,
|
|
703
|
+
data={
|
|
704
|
+
"tool_name": name,
|
|
705
|
+
"input": tool_input,
|
|
706
|
+
"params": tool_input, # 前端兼容:同时提供 params 便于 getToolDisplayText 读取
|
|
707
|
+
},
|
|
708
|
+
step_index=self.state.step_index,
|
|
709
|
+
tool_name=name,
|
|
710
|
+
)
|
|
711
|
+
if self.enable_subagent_events and self._is_agent_tool(name):
|
|
712
|
+
yield self._event(
|
|
713
|
+
event_type=LangchainAgentEventType.SUBAGENT_START,
|
|
714
|
+
data={"tool_name": name},
|
|
715
|
+
step_index=self.state.step_index,
|
|
716
|
+
tool_name=name,
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
# 2. tool-input-delta:LangGraph 不支持,跳过(仅保留类型以兼容枚举)
|
|
720
|
+
|
|
721
|
+
# 3. tool-call:合成事件(表示工具正在执行)
|
|
722
|
+
if self.synthesize_tool_call:
|
|
723
|
+
yield self._event(
|
|
724
|
+
event_type=LangchainAgentEventType.TOOL_CALL,
|
|
725
|
+
data={
|
|
726
|
+
"tool_name": name,
|
|
727
|
+
"status": "running",
|
|
728
|
+
},
|
|
729
|
+
step_index=self.state.step_index,
|
|
730
|
+
tool_name=name,
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
async def _handle_tool_end(
|
|
734
|
+
self, name: str, data: Dict[str, Any]
|
|
735
|
+
) -> AsyncGenerator[LangchainAgentEvent, None]:
|
|
736
|
+
"""处理 on_tool_end - 条件映射"""
|
|
737
|
+
|
|
738
|
+
output = data.get("output")
|
|
739
|
+
|
|
740
|
+
# 判断是否是错误
|
|
741
|
+
is_error = self._is_tool_error(output)
|
|
742
|
+
|
|
743
|
+
if is_error:
|
|
744
|
+
yield self._event(
|
|
745
|
+
event_type=LangchainAgentEventType.TOOL_ERROR,
|
|
746
|
+
data={
|
|
747
|
+
"tool_name": name,
|
|
748
|
+
"error": str(output),
|
|
749
|
+
"error_type": self._get_error_type(output),
|
|
750
|
+
"status": "blocked" if isinstance(output, PermissionError) else "error",
|
|
751
|
+
},
|
|
752
|
+
step_index=self.state.step_index,
|
|
753
|
+
tool_name=name,
|
|
754
|
+
)
|
|
755
|
+
else:
|
|
756
|
+
# 多层降级提取工具结果正文,避免打印 ToolMessage/Envelope 的完整 repr
|
|
757
|
+
# 降级顺序:.content → .payload → .summary → dict 键查找 → str() 兜底
|
|
758
|
+
out_val = self._extract_tool_result_text(output)
|
|
759
|
+
yield self._event(
|
|
760
|
+
event_type=LangchainAgentEventType.TOOL_RESULT,
|
|
761
|
+
data={
|
|
762
|
+
"tool_name": name,
|
|
763
|
+
"output": out_val,
|
|
764
|
+
},
|
|
765
|
+
step_index=self.state.step_index,
|
|
766
|
+
tool_name=name,
|
|
767
|
+
)
|
|
768
|
+
if self.enable_subagent_events and self._is_agent_tool(name):
|
|
769
|
+
child_session_id = None
|
|
770
|
+
if isinstance(output, dict):
|
|
771
|
+
meta = output.get("meta")
|
|
772
|
+
if isinstance(meta, dict):
|
|
773
|
+
child_session_id = meta.get("child_session_id")
|
|
774
|
+
if child_session_id is None:
|
|
775
|
+
child_session_id = output.get("session_id")
|
|
776
|
+
yield self._event(
|
|
777
|
+
event_type=LangchainAgentEventType.SUBAGENT_END,
|
|
778
|
+
data={"tool_name": name, "child_session_id": child_session_id},
|
|
779
|
+
step_index=self.state.step_index,
|
|
780
|
+
tool_name=name,
|
|
781
|
+
session_id=str(child_session_id) if child_session_id else None,
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
self.state.current_tool = None
|
|
785
|
+
|
|
786
|
+
def _is_tool_error(self, output: Any) -> bool:
|
|
787
|
+
"""判断工具输出是否是错误"""
|
|
788
|
+
if output is None:
|
|
789
|
+
return False
|
|
790
|
+
if isinstance(output, Exception):
|
|
791
|
+
return True
|
|
792
|
+
if isinstance(output, dict):
|
|
793
|
+
return bool(output.get("error") is not None or output.get("status") == "error")
|
|
794
|
+
return False
|
|
795
|
+
|
|
796
|
+
def _get_error_type(self, output: Any) -> str:
|
|
797
|
+
"""获取错误类型"""
|
|
798
|
+
if isinstance(output, PermissionError):
|
|
799
|
+
return "permission_denied"
|
|
800
|
+
if isinstance(output, TimeoutError):
|
|
801
|
+
return "timeout"
|
|
802
|
+
if isinstance(output, ValueError):
|
|
803
|
+
return "invalid_input"
|
|
804
|
+
return "unknown"
|
|
805
|
+
|
|
806
|
+
# ===== Error 事件处理 =====
|
|
807
|
+
|
|
808
|
+
async def _handle_error(
|
|
809
|
+
self, name: str, data: Dict[str, Any]
|
|
810
|
+
) -> AsyncGenerator[LangchainAgentEvent, None]:
|
|
811
|
+
"""处理 on_chain_error"""
|
|
812
|
+
|
|
813
|
+
error = data.get("error", "Unknown error")
|
|
814
|
+
|
|
815
|
+
yield self._event(
|
|
816
|
+
event_type=LangchainAgentEventType.ERROR,
|
|
817
|
+
data={
|
|
818
|
+
"error": str(error),
|
|
819
|
+
"error_type": type(error).__name__ if isinstance(error, Exception) else "Unknown",
|
|
820
|
+
"node_name": name,
|
|
821
|
+
},
|
|
822
|
+
step_index=self.state.step_index,
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
__all__ = [
|
|
827
|
+
"LangchainAgentEventType",
|
|
828
|
+
"LangchainAgentEvent",
|
|
829
|
+
"EventAdapterState",
|
|
830
|
+
"LangGraphToLangchainAgentEventAdapter",
|
|
831
|
+
]
|
|
832
|
+
|